Compare commits

...

173 Commits

Author SHA1 Message Date
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
https://github.com/ngosang/trackerslist/commit/8ba874e69da0532166644922ea207da3084dff66
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
Bogdan 0c0cbdac2f Fixed: (FileList) Add alternative URL and return only FL results when fl-only is set 2023-01-31 02:06:50 +02:00
Bogdan 0685c2eb04 Fixed: (PirateTheNet) Changed the login path 2023-01-30 04:45:25 +02:00
Bogdan e8c132e908 New: Add PirateTheNet 2023-01-29 04:27:58 +02:00
Bogdan bea9bd39ff Fixed: (LazyLibrarian) Sync priority 2023-01-27 03:07:43 +02:00
Bogdan 077e4727f2 New: Add aro.lol 2023-01-27 03:07:05 +02:00
Bogdan 5f7bc82eb5 Fixed: (Anidex) Update indexer capabilities 2023-01-26 04:42:21 +02:00
Bogdan 0dd5c56175 New: Add XSpeeds 2023-01-26 04:11:24 +02:00
Bogdan 409a218379 Fixed: (ImmortalSeed) Parse pre-release dates correctly 2023-01-26 04:11:24 +02:00
Bogdan 07cc1e03c8 Fixed: (Nebulance) Use single wildcard in search 2023-01-25 19:13:43 -06:00
Qstick 560cda8ba0 Bump dotnet to 6.0.13 2023-01-25 19:07:47 -06:00
Weblate 934f566359 Translated using Weblate (Greek)
Currently translated at 73.2% (345 of 471 strings)

Translated using Weblate (French)

Currently translated at 95.1% (448 of 471 strings)

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

Currently translated at 100.0% (471 of 471 strings)

Co-authored-by: Vasilis Ieropoulos <kirav96@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: aenron <1414004038@qq.com>
Co-authored-by: josehggr <jose.hogger@hotmail.fr>
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/zh_CN/
Translation: Servarr/Prowlarr
2023-01-23 23:43:36 -06:00
Bogdan 89ae5ceaa6 Fixed: Remove NotWhatCD 2023-01-24 05:50:19 +02:00
Bogdan c7d5889e59 Fixed: (Newznab) Use NameValueCollection.Set 2023-01-24 04:07:24 +02:00
Bogdan bea3c051b9 Fixed: (Newznab) Remove extra ampersand 2023-01-24 04:07:24 +02:00
Bogdan c0b1675627 Fixed: (AlphaRatio/GreatPosterWall) Add freeleech only and exclude scene settings 2023-01-23 19:56:30 -06:00
Qstick 906d09e162 Bump version to 1.1.2 2023-01-22 16:07:45 -06:00
Bogdan 8cd9ad01c2 Fixed: (Indexers) Use the defined names for C# indexers 2023-01-22 20:08:30 +02:00
Bogdan ce2f322478 New: Add Anidex 2023-01-22 19:50:41 +02:00
Bogdan 0487309ee8 New: Add Toloka.to 2023-01-22 19:49:51 +02:00
Qstick 9862584611 Fixed: Catch InvalidDataException during initial config to prevent boot loop 2023-01-21 17:19:06 -06:00
Qstick 6a00e0db90 Filter useless PG Errors from coming to Sentry 2023-01-21 17:16:54 -06:00
Qstick c93831dd8b Fixed: (TorrentBytes) Avoid null exception in DoLogin error handling 2023-01-21 16:51:19 -06:00
Bogdan 6546ba773c New: (Notification) Apprise 2023-01-21 20:24:28 +02:00
Qstick 4c3484a898 New: (Notifications) Add Ntfy 2023-01-21 12:14:21 -06:00
Qstick 8561b862f9 New: (Notifications) Add Simplepush 2023-01-21 12:13:50 -06:00
Bogdan e1032fb0f5 New: Add optional app minimum seeders per indexer 2023-01-21 11:26:09 -06:00
Bogdan 4063219430 Fixed: (Orpheus) Title improvements to include ReleaseType and fix categories 2023-01-21 11:25:35 -06:00
Bogdan e008be8581 Fixed: (Redacted) Search requests, title improvements 2023-01-21 11:25:35 -06:00
Bogdan d6b379df64 Fixed: Validation inheritance 2023-01-19 21:04:08 -06:00
Bogdan 27094ccf62 Fixed: (ImmortalSeed) Improve tv search with season+ep and parsing, add MR/MST 2023-01-18 18:46:27 -06:00
Bogdan edf9473e9a Fixed: (TorrentDay) Add freeleech only setting 2023-01-18 18:44:22 -06:00
Qstick a0d11e7e33 Bump version to 1.1.1 2023-01-16 21:06:48 -06:00
Bogdan 7729eb398a Fixed: (Nebulance) CS cleanup 2023-01-16 19:48:04 -06:00
Bogdan 989564dbce Fixed: (IPTorrents) Improve clean title 2023-01-16 19:48:04 -06:00
Bogdan c1f917f1ac Fixed: (SpeedCD) Improve clean title 2023-01-16 19:48:04 -06:00
Bogdan 4b7e47c397 Fixed: (RetroFlix) Update description and improve clean title 2023-01-16 19:48:04 -06:00
Bogdan 1529527af9 Fixed: (Cardigann) Bump to v8 2023-01-15 18:20:31 -06:00
Bogdan a11bd1c3c7 Fixed: (GreatPosterWall) Revert category to default to Movies 2023-01-14 22:20:06 -06:00
Bogdan 915b320a4a Fixed: (Shizaproject) Obsolete: Site unavailable 2023-01-14 18:43:32 -06:00
Bogdan 155f72cc45 Fixed: (AvistaZ/CinemaZ) Remove Music category mapping 2023-01-14 18:43:15 -06:00
Bogdan 3f73fec5c3 Fixed: (Rarbg) Add slash to IndexerUrl, increase RateLimit to 5s 2023-01-14 18:06:44 -06:00
Bogdan 8515623ceb Fixed: (SpeedApp) Fix cleanse token from response when it's the only field 2023-01-14 11:53:47 -06:00
Bogdan 963cddb582 Fixed: (SpeedCD) Add wildcard to season in tvsearch, add freeleech toggle, improve query selectors 2023-01-14 10:52:55 -06:00
Bogdan ede323b8ed Fixed: (IPTorrents/SceneTime) Remove advanced from freeleech only setting 2023-01-13 22:32:37 -06:00
Bogdan 07d7fc98b0 Fixed: (Orpheus) Add remaster title and year to release title 2023-01-12 21:41:45 -06:00
Bogdan 1b78fd38db Fixed: (FileList) RequestGenerator refactoring, append slash to IndexerUrl 2023-01-12 21:41:13 -06:00
Bogdan 5a9d4d6280 Fixed: (UI) Transpile ES6 libs to fix issues on some browsers 2023-01-11 21:23:10 -06:00
Qstick 70685de5d2 Fixed: Correctly handle relative redirects with dot segments 2023-01-11 21:20:30 -06:00
Bogdan 9860183433 Fixed: (AvistaZ/Anthelion) Cleanse pid, api_key and token 2023-01-11 21:04:50 -06:00
Qstick 50331c61ae Fixed: Use selected BaseUrl for external link
Fixes #1310
2023-01-10 22:19:15 -06:00
Bogdan bd3408f170 Fixed: (HD-Torrents) Add more alt domains, add Internal flag and fix Blu-Ray categories 2023-01-10 22:01:44 -06:00
Bogdan c043bf8da9 Fixed: (HD-Space) Use torrent name as release title 2023-01-10 22:00:40 -06:00
Bakerboy448 ea3fa6f28d Fixed: (BeyondHD) Cleanse RSSKey on Grabs 2023-01-10 21:59:35 -06:00
Bogdan 8917347c0b Fixed: (IPTorrents) Fix pagination when limit is zero 2023-01-10 13:30:21 -06:00
Bogdan 2cebdf4a06 Fixed: (AvistaZ) Use different timezone offset than the rest 2023-01-09 20:21:56 -06:00
Bogdan 985110cfb9 Fixed: (ImmortalSeed) Updated categories, improved searchUrl build and fixed auth 2023-01-08 15:44:51 -06:00
Bogdan de876247a3 Fixed: (MyAnonamouse) Added search type options, search in description/series/filenames as settings 2023-01-08 15:39:25 -06:00
Qstick bad6c301f8 More CF cases from FlareSolverrSharp 2023-01-08 13:59:17 -06:00
Weblate fc3b23394a Translated using Weblate (Ukrainian)
Currently translated at 67.5% (318 of 471 strings)

Translated using Weblate (Russian)

Currently translated at 76.4% (360 of 471 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (471 of 471 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (471 of 471 strings)

Translated using Weblate (Dutch)

Currently translated at 88.5% (417 of 471 strings)

Translated using Weblate (Finnish)

Currently translated at 99.5% (469 of 471 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (471 of 471 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (471 of 471 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (471 of 471 strings)

Co-authored-by: AlexR-sf <omg.portal.supp@gmail.com>
Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Davide Palma <github@davidepalma.it>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Iagocds <cdsiago@gmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: andrey4korop <andrey999@i.ua>
Co-authored-by: verhese <sean.verheyen1@telenet.be>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
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-01-07 13:34:30 -06:00
Qstick 92c3656bad New: (HDSpace) Parse Genre and Description 2023-01-07 13:26:49 -06:00
Qstick 1acbee2a57 New: (Notification) Mailgun
Fixes #1297
2023-01-07 13:14:00 -06:00
Qstick c28f9b6bcd Fix Units in Flaresolverr Timeout 2023-01-06 07:21:56 -06:00
Bogdan aa8048968c Fixed: (Cardigann) Apply RateLimit by using RequestDelay from definitions 2023-01-05 22:39:16 -06:00
Qstick 6646734510 Fixed: (Flaresolverr) Ensure Prowlarr Timeout is sufficient for FS setting 2023-01-05 22:29:15 -06:00
Bogdan 71dd8b6d04 Fixed: Use HashSet to prevent duplicated indexer flags 2023-01-05 22:12:24 -06:00
afpak 6d87bd9f8c Fixed: (IPTorrents) use offset to set page field 2023-01-05 21:50:39 -06:00
Weblate 551d969680 Translated using Weblate (Hungarian)
Currently translated at 100.0% (471 of 471 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (471 of 471 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (471 of 471 strings)

Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Davide Palma <github@davidepalma.it>
Co-authored-by: Iagocds <cdsiago@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translation: Servarr/Prowlarr
2023-01-05 21:49:03 -06:00
Bogdan 57dac6afdd Fixed: (GreatPosterWall) Use UTC for PublishDate, order releases and map categories accordingly 2023-01-05 21:40:46 -06:00
bakerboy448 3dfbfd07dd improve authentication required wording 2023-01-04 22:53:38 -06:00
Qstick 842df6913c New: Improve CF Detection
Co-Authored-By: Diego Heras <ngosang@hotmail.es>
2023-01-04 22:27:03 -06:00
Qstick 599eeb4c61 Bump Version to 1.1.0 2023-01-03 18:50:42 -06:00
Bogdan da371dd921 Fixed: (Avistaz) Use type password for PID 2023-01-03 18:49:17 -06:00
Bogdan fc25ba7ac0 Fixed: (Filelist) Use UTC in tests 2023-01-03 18:05:54 -06:00
Qstick 6e1bef13e2 Fixed: Correctly calculate UI age for some indexers 2023-01-02 23:28:36 -06:00
Qstick 72ee413411 Fixed: (BeyondHD) Assume Universal Time for publish dates 2023-01-02 23:23:56 -06:00
Qstick e87b45b47e Fixed: (Filelist) Assume UTC+2 for API Dates 2023-01-02 23:23:17 -06:00
Qstick cc841fe3d1 Remove Preview from Page Title 2023-01-02 18:06:34 -06:00
Qstick 264ffdcc26 Fixup Provider Tests 2023-01-02 17:41:41 -06:00
Qstick 5cc044aa8f Rarbg Rate Limit Tweaks, Additional back-off level 2023-01-02 17:16:46 -06:00
Bogdan de2fd92b6f Fixed: (Avistaz) Workaround for fetching "retry-after" header not present when using "Accept: application/json" 2023-01-01 15:31:26 -06:00
Qstick eff09c1f72 Treat Master as a valid branch 2022-12-31 19:32:06 -06:00
Qstick 9db888c9a3 Bump Version to 1.0.1 2022-12-31 18:49:38 -06:00
Qstick bf78396164 Rewrite test to avoid 6 hours of failures and needing a change every year 2022-12-31 18:22:07 -06:00
Qstick 0e7eaa9221 Simplify logic in HandleRequest 2022-12-31 17:57:54 -06:00
Colin Gagnaire 5b82decc31 New: Add support for native Freebox Download Client 2022-12-31 17:34:17 -06:00
Mark McDowall 38ab533272 Fixed: Only log /proc/mounts exception once per process 2022-12-31 17:11:05 -06:00
Qstick 4914fcd5df Fixed: (UI) Category is None in history if only search by sub category 2022-12-31 17:03:27 -06:00
Qstick 858415b037 Fixed: (Cardigann) Query string gets first letter removed in request handling 2022-12-31 16:37:15 -06:00
Bogdan 43f4899324 New: (Indexer) Torrent Libble with 2FA and pagination 2022-12-31 16:22:06 -06:00
Bogdan c60a94adfb Fixed: (RetroFlix) Set 5 days as MST, return 100 results and remove "[REQUESTED]" from title 2022-12-31 16:19:47 -06:00
Qstick f386ddb806 Fixed: Sorting on mobile search UI 2022-12-31 16:17:54 -06:00
Qstick 4175c2577e Fixed: Link to release page from mobile search 2022-12-31 15:56:29 -06:00
Qstick 6ce9e5ceb9 Fixed: Release Grab not working on Mobile search 2022-12-30 18:28:55 -06:00
Bogdan c15643be39 Fixed: (Cardigann) Allow use of template variables in fields selector 2022-12-30 17:58:24 -06:00
Bogdan a58380031d Fixed: (Indexer) Added TvSearchParam.ImdbId to SpeedApp 2022-12-28 23:56:08 -06:00
Bogdan 73af5c9a72 Fixed: (Indexer) Changed FL to use internal flag 2022-12-28 23:54:48 -06:00
Bogdan d556545e7f Fixed: Changed torznab parser to use additional attributes 2022-12-28 23:54:01 -06:00
Bogdan affde5d7b7 Fixed: (Orpheus) Changed to use filters for categories, label and year 2022-12-27 22:16:49 -06:00
Bakerboy448 518c85dee2 Fixed: (Rarbg) Improve RateLimit Handling
Fixed: (Rarbg) Increase delay to 4s to reduce Rate Limiting

Fixes #1169
2022-12-26 18:26:43 -06:00
Bakerboy448 ba3a240707 Add TooManyRequestsException with var retryWait 2022-12-26 18:26:43 -06:00
Qstick 587a73f3d6 Fixed: (Newznab) Parsing of Ids from non-standard feeds
Fixes #1261
2022-12-26 18:17:50 -06:00
Weblate ae8f017ca8 Translated using Weblate (Ukrainian)
Currently translated at 63.2% (298 of 471 strings)

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

Currently translated at 98.5% (464 of 471 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (471 of 471 strings)

Translated using Weblate (Bengali)

Currently translated at 0.8% (4 of 468 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (468 of 468 strings)

Translated using Weblate (Finnish)

Currently translated at 100.0% (468 of 468 strings)

Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: andrey4korop <andrey999@i.ua>
Co-authored-by: lhquark <lhquark@gmail.com>
Co-authored-by: saambd <me@salimrahman.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/bn/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2022-12-26 17:52:43 -06:00
Bogdan d9098b612e Fixed: (Nebulance) Changed MinimumSeedTime according to their H&R rules 2022-12-26 17:48:29 -06:00
285 changed files with 11221 additions and 5050 deletions
+7 -7
View File
@@ -117,7 +117,6 @@ dotnet_diagnostic.CA1003.severity = suggestion
dotnet_diagnostic.CA1008.severity = suggestion dotnet_diagnostic.CA1008.severity = suggestion
dotnet_diagnostic.CA1010.severity = suggestion dotnet_diagnostic.CA1010.severity = suggestion
dotnet_diagnostic.CA1012.severity = suggestion dotnet_diagnostic.CA1012.severity = suggestion
dotnet_diagnostic.CA1014.severity = suggestion
dotnet_diagnostic.CA1016.severity = suggestion dotnet_diagnostic.CA1016.severity = suggestion
dotnet_diagnostic.CA1017.severity = suggestion dotnet_diagnostic.CA1017.severity = suggestion
dotnet_diagnostic.CA1018.severity = suggestion dotnet_diagnostic.CA1018.severity = suggestion
@@ -163,6 +162,7 @@ dotnet_diagnostic.CA1309.severity = suggestion
dotnet_diagnostic.CA1310.severity = suggestion dotnet_diagnostic.CA1310.severity = suggestion
dotnet_diagnostic.CA1401.severity = suggestion dotnet_diagnostic.CA1401.severity = suggestion
dotnet_diagnostic.CA1416.severity = suggestion dotnet_diagnostic.CA1416.severity = suggestion
dotnet_diagnostic.CA1419.severity = suggestion
dotnet_diagnostic.CA1507.severity = suggestion dotnet_diagnostic.CA1507.severity = suggestion
dotnet_diagnostic.CA1508.severity = suggestion dotnet_diagnostic.CA1508.severity = suggestion
dotnet_diagnostic.CA1707.severity = suggestion dotnet_diagnostic.CA1707.severity = suggestion
@@ -178,9 +178,6 @@ dotnet_diagnostic.CA1720.severity = suggestion
dotnet_diagnostic.CA1721.severity = suggestion dotnet_diagnostic.CA1721.severity = suggestion
dotnet_diagnostic.CA1724.severity = suggestion dotnet_diagnostic.CA1724.severity = suggestion
dotnet_diagnostic.CA1725.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.CA1806.severity = suggestion
dotnet_diagnostic.CA1810.severity = suggestion dotnet_diagnostic.CA1810.severity = suggestion
dotnet_diagnostic.CA1812.severity = suggestion dotnet_diagnostic.CA1812.severity = suggestion
@@ -192,13 +189,14 @@ dotnet_diagnostic.CA1819.severity = suggestion
dotnet_diagnostic.CA1822.severity = suggestion dotnet_diagnostic.CA1822.severity = suggestion
dotnet_diagnostic.CA1823.severity = suggestion dotnet_diagnostic.CA1823.severity = suggestion
dotnet_diagnostic.CA1824.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.CA2000.severity = suggestion
dotnet_diagnostic.CA2002.severity = suggestion dotnet_diagnostic.CA2002.severity = suggestion
dotnet_diagnostic.CA2007.severity = suggestion dotnet_diagnostic.CA2007.severity = suggestion
dotnet_diagnostic.CA2008.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.CA2012.severity = suggestion
dotnet_diagnostic.CA2013.severity = suggestion dotnet_diagnostic.CA2013.severity = suggestion
dotnet_diagnostic.CA2100.severity = suggestion dotnet_diagnostic.CA2100.severity = suggestion
@@ -229,6 +227,7 @@ dotnet_diagnostic.CA2243.severity = suggestion
dotnet_diagnostic.CA2244.severity = suggestion dotnet_diagnostic.CA2244.severity = suggestion
dotnet_diagnostic.CA2245.severity = suggestion dotnet_diagnostic.CA2245.severity = suggestion
dotnet_diagnostic.CA2246.severity = suggestion dotnet_diagnostic.CA2246.severity = suggestion
dotnet_diagnostic.CA2254.severity = suggestion
dotnet_diagnostic.CA3061.severity = suggestion dotnet_diagnostic.CA3061.severity = suggestion
dotnet_diagnostic.CA3075.severity = suggestion dotnet_diagnostic.CA3075.severity = suggestion
dotnet_diagnostic.CA3076.severity = suggestion dotnet_diagnostic.CA3076.severity = suggestion
@@ -255,6 +254,7 @@ dotnet_diagnostic.CA5385.severity = suggestion
dotnet_diagnostic.CA5392.severity = suggestion dotnet_diagnostic.CA5392.severity = suggestion
dotnet_diagnostic.CA5394.severity = suggestion dotnet_diagnostic.CA5394.severity = suggestion
dotnet_diagnostic.CA5397.severity = suggestion dotnet_diagnostic.CA5397.severity = suggestion
dotnet_diagnostic.CA5401.severity = suggestion
dotnet_diagnostic.SYSLIB0014.severity = none dotnet_diagnostic.SYSLIB0014.severity = none
-41
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
+2 -2
View File
@@ -9,13 +9,13 @@ variables:
testsFolder: './_tests' testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '1.0.0' majorVersion: '1.2.2'
minorVersion: $[counter('minorVersion', 1)] minorVersion: $[counter('minorVersion', 1)]
prowlarrVersion: '$(majorVersion).$(minorVersion)' prowlarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)' buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
sentryOrg: 'servarr' sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com' sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.404' dotnetVersion: '6.0.405'
innoVersion: '6.2.0' innoVersion: '6.2.0'
nodeVersion: '16.x' nodeVersion: '16.x'
windowsImage: 'windows-2022' windowsImage: 'windows-2022'
+2 -2
View File
@@ -142,8 +142,8 @@ module.exports = (env) => {
module: { module: {
rules: [ rules: [
{ {
test: /\.js?$/, test: /\.jsx?$/,
exclude: /(node_modules|JsLibraries)/, exclude: /[\\/]node_modules[\\/](?!(@sentry\/browser|@sentry\/integrations|chart.js|filesize|normalize.css)[\\/])/,
use: [ use: [
{ {
loader: 'babel-loader', loader: 'babel-loader',
@@ -50,7 +50,7 @@ function CustomFiltersModalContent(props) {
<div className={styles.addButtonContainer}> <div className={styles.addButtonContainer}>
<Button onPress={onAddCustomFilter}> <Button onPress={onAddCustomFilter}>
Add Custom Filter {translate('AddCustomFilter')}
</Button> </Button>
</div> </div>
</ModalBody> </ModalBody>
@@ -61,15 +61,15 @@ class TagsModalContent extends Component {
} = this.state; } = this.state;
const applyTagsOptions = [ const applyTagsOptions = [
{ key: 'add', value: 'Add' }, { key: 'add', value: translate('Add') },
{ key: 'remove', value: 'Remove' }, { key: 'remove', value: translate('Remove') },
{ key: 'replace', value: 'Replace' } { key: 'replace', value: translate('Replace') }
]; ];
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
Tags {translate('Tags')}
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
@@ -26,7 +26,7 @@ function IndexerIndexSortMenu(props) {
sortDirection={sortDirection} sortDirection={sortDirection}
onPress={onSortSelect} onPress={onSortSelect}
> >
Status {translate('Status')}
</SortMenuItem> </SortMenuItem>
<SortMenuItem <SortMenuItem
@@ -62,7 +62,7 @@ function IndexerIndexSortMenu(props) {
sortDirection={sortDirection} sortDirection={sortDirection}
onPress={onSortSelect} onPress={onSortSelect}
> >
{'Priority'} {translate('Priority')}
</SortMenuItem> </SortMenuItem>
<SortMenuItem <SortMenuItem
@@ -71,7 +71,7 @@ function IndexerIndexSortMenu(props) {
sortDirection={sortDirection} sortDirection={sortDirection}
onPress={onSortSelect} onPress={onSortSelect}
> >
{'Protocol'} {translate('Protocol')}
</SortMenuItem> </SortMenuItem>
<SortMenuItem <SortMenuItem
@@ -80,7 +80,7 @@ function IndexerIndexSortMenu(props) {
sortDirection={sortDirection} sortDirection={sortDirection}
onPress={onSortSelect} onPress={onSortSelect}
> >
{'Privacy'} {translate('Privacy')}
</SortMenuItem> </SortMenuItem>
</MenuContent> </MenuContent>
</SortMenu> </SortMenu>
@@ -14,7 +14,7 @@ function CapabilitiesLabel(props) {
let filteredList = categories.filter((item) => item.id < 100000); let filteredList = categories.filter((item) => item.id < 100000);
if (categoryFilter.length > 0) { if (categoryFilter.length > 0) {
filteredList = filteredList.filter((item) => categoryFilter.includes(item.id)); filteredList = filteredList.filter((item) => categoryFilter.includes(item.id) || (item.subCategories && item.subCategories.some((r) => categoryFilter.includes(r.id))));
} }
const nameList = filteredList.map((item) => item.name).sort(); const nameList = filteredList.map((item) => item.name).sort();
@@ -79,6 +79,7 @@ class IndexerIndexRow extends Component {
privacy, privacy,
priority, priority,
status, status,
fields,
appProfile, appProfile,
added, added,
capabilities, capabilities,
@@ -96,6 +97,8 @@ class IndexerIndexRow extends Component {
isIndexerInfoModalOpen isIndexerInfoModalOpen
} = this.state; } = this.state;
const baseUrl = fields.find((field) => field.name === 'baseUrl')?.value ?? (Array.isArray(indexerUrls) ? indexerUrls[0] : undefined);
return ( return (
<> <>
{ {
@@ -245,12 +248,12 @@ class IndexerIndexRow extends Component {
/> />
{ {
indexerUrls ? baseUrl ?
<IconButton <IconButton
className={styles.externalLink} className={styles.externalLink}
name={icons.EXTERNAL_LINK} name={icons.EXTERNAL_LINK}
title={translate('Website')} title={translate('Website')}
to={indexerUrls[0].replace('api.', '')} to={baseUrl.replace('api.', '')}
/> : null /> : null
} }
@@ -299,6 +302,7 @@ IndexerIndexRow.propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
enable: PropTypes.bool.isRequired, enable: PropTypes.bool.isRequired,
redirect: PropTypes.bool.isRequired, redirect: PropTypes.bool.isRequired,
fields: PropTypes.arrayOf(PropTypes.object).isRequired,
appProfile: PropTypes.object.isRequired, appProfile: PropTypes.object.isRequired,
status: PropTypes.object, status: PropTypes.object,
capabilities: PropTypes.object, capabilities: PropTypes.object,
@@ -20,11 +20,14 @@ function IndexerInfoModalContent(props) {
encoding, encoding,
language, language,
indexerUrls, indexerUrls,
fields,
protocol, protocol,
capabilities, capabilities,
onModalClose onModalClose
} = props; } = props;
const baseUrl = fields.find((field) => field.name === 'baseUrl')?.value ?? indexerUrls[0];
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
@@ -57,7 +60,7 @@ function IndexerInfoModalContent(props) {
/> />
<DescriptionListItemTitle>{translate('IndexerSite')}</DescriptionListItemTitle> <DescriptionListItemTitle>{translate('IndexerSite')}</DescriptionListItemTitle>
<DescriptionListItemDescription> <DescriptionListItemDescription>
<Link to={indexerUrls[0]}>{indexerUrls[0]}</Link> <Link to={baseUrl}>{baseUrl}</Link>
</DescriptionListItemDescription> </DescriptionListItemDescription>
<DescriptionListItemTitle>{`${protocol === 'usenet' ? 'Newznab' : 'Torznab'} Url`}</DescriptionListItemTitle> <DescriptionListItemTitle>{`${protocol === 'usenet' ? 'Newznab' : 'Torznab'} Url`}</DescriptionListItemTitle>
<DescriptionListItemDescription> <DescriptionListItemDescription>
@@ -114,6 +117,7 @@ IndexerInfoModalContent.propTypes = {
encoding: PropTypes.string.isRequired, encoding: PropTypes.string.isRequired,
language: PropTypes.string.isRequired, language: PropTypes.string.isRequired,
indexerUrls: PropTypes.arrayOf(PropTypes.string).isRequired, indexerUrls: PropTypes.arrayOf(PropTypes.string).isRequired,
fields: PropTypes.arrayOf(PropTypes.object).isRequired,
protocol: PropTypes.string.isRequired, protocol: PropTypes.string.isRequired,
capabilities: PropTypes.object.isRequired, capabilities: PropTypes.object.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
@@ -35,9 +35,11 @@ $hoverScale: 1.05;
} }
.title { .title {
overflow: hidden;
width: 85%; width: 85%;
font-weight: 500; font-weight: 500;
font-size: 14px; font-size: 14px;
overflow-wrap: break-word;
} }
.actions { .actions {
@@ -3,6 +3,7 @@ import React, { Component } from 'react';
import TextTruncate from 'react-text-truncate'; import TextTruncate from 'react-text-truncate';
import Label from 'Components/Label'; import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import { icons, kinds } from 'Helpers/Props'; import { icons, kinds } from 'Helpers/Props';
import CategoryLabel from 'Search/Table/CategoryLabel'; import CategoryLabel from 'Search/Table/CategoryLabel';
@@ -52,12 +53,17 @@ class SearchIndexOverview extends Component {
// //
// Listeners // Listeners
onEditSeriesPress = () => { onGrabPress = () => {
this.setState({ isEditSeriesModalOpen: true }); const {
}; guid,
indexerId,
onGrabPress
} = this.props;
onEditSeriesModalClose = () => { onGrabPress({
this.setState({ isEditSeriesModalOpen: false }); guid,
indexerId
});
}; };
// //
@@ -66,6 +72,7 @@ class SearchIndexOverview extends Component {
render() { render() {
const { const {
title, title,
infoUrl,
protocol, protocol,
downloadUrl, downloadUrl,
categories, categories,
@@ -91,10 +98,16 @@ class SearchIndexOverview extends Component {
<div className={styles.info} style={{ height: contentHeight }}> <div className={styles.info} style={{ height: contentHeight }}>
<div className={styles.titleRow}> <div className={styles.titleRow}>
<div className={styles.title}> <div className={styles.title}>
<TextTruncate <Link
line={2} to={infoUrl}
text={title} title={title}
/> >
<TextTruncate
line={2}
text={title}
/>
</Link>
</div> </div>
<div className={styles.actions}> <div className={styles.actions}>
@@ -49,7 +49,7 @@ class SearchIndexOverviews extends Component {
this._grid && this._grid &&
(prevState.width !== width || (prevState.width !== width ||
prevState.rowHeight !== rowHeight || prevState.rowHeight !== rowHeight ||
hasDifferentItemsOrOrder(prevProps.items, items) hasDifferentItemsOrOrder(prevProps.items, items, 'guid')
) )
) { ) {
// recomputeGridSize also forces Grid to discard its cache of rendered cells // recomputeGridSize also forces Grid to discard its cache of rendered cells
@@ -93,7 +93,6 @@ class SearchIndexOverviews extends Component {
cellRenderer = ({ key, rowIndex, style }) => { cellRenderer = ({ key, rowIndex, style }) => {
const { const {
items, items,
sortKey,
showRelativeDates, showRelativeDates,
shortDateFormat, shortDateFormat,
longDateFormat, longDateFormat,
@@ -117,7 +116,6 @@ class SearchIndexOverviews extends Component {
<SearchIndexItemConnector <SearchIndexItemConnector
key={release.guid} key={release.guid}
component={SearchIndexOverview} component={SearchIndexOverview}
sortKey={sortKey}
rowHeight={rowHeight} rowHeight={rowHeight}
showRelativeDates={showRelativeDates} showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat} shortDateFormat={shortDateFormat}
@@ -41,7 +41,7 @@ const mapDispatchToProps = {
dispatchExecuteCommand: executeCommand dispatchExecuteCommand: executeCommand
}; };
class MovieIndexItemConnector extends Component { class SearchIndexItemConnector extends Component {
// //
// Render // Render
@@ -66,9 +66,9 @@ class MovieIndexItemConnector extends Component {
} }
} }
MovieIndexItemConnector.propTypes = { SearchIndexItemConnector.propTypes = {
guid: PropTypes.string, guid: PropTypes.string,
component: PropTypes.elementType.isRequired component: PropTypes.elementType.isRequired
}; };
export default connect(createMapStateToProps, mapDispatchToProps)(MovieIndexItemConnector); export default connect(createMapStateToProps, mapDispatchToProps)(SearchIndexItemConnector);
@@ -28,7 +28,7 @@ class AddApplicationModalContent extends Component {
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
Add Application {translate('AddApplication')}
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
@@ -71,14 +71,14 @@ class Application extends Component {
{ {
syncLevel === 'addOnly' && syncLevel === 'addOnly' &&
<Label kind={kinds.WARNING}> <Label kind={kinds.WARNING}>
Add and Remove Only {translate('AddRemoveOnly')}
</Label> </Label>
} }
{ {
syncLevel === 'fullSync' && syncLevel === 'fullSync' &&
<Label kind={kinds.SUCCESS}> <Label kind={kinds.SUCCESS}>
Full Sync {translate('FullSync')}
</Label> </Label>
} }
@@ -88,7 +88,7 @@ class Application extends Component {
kind={kinds.DISABLED} kind={kinds.DISABLED}
outline={true} outline={true}
> >
Disabled {translate('Disabled')}
</Label> </Label>
} }
@@ -106,7 +106,7 @@ class IndexerProxy extends Component {
kind={kinds.DISABLED} kind={kinds.DISABLED}
outline={true} outline={true}
> >
Disabled {translate('Disabled')}
</Label> : </Label> :
null null
} }
+2 -2
View File
@@ -54,7 +54,7 @@ export const defaultState = {
}, },
{ {
name: 'grabTitle', name: 'grabTitle',
label: translate('Grab Title'), label: translate('GrabTitle'),
isSortable: false, isSortable: false,
isVisible: false isVisible: false
}, },
@@ -78,7 +78,7 @@ export const defaultState = {
}, },
{ {
name: 'elapsedTime', name: 'elapsedTime',
label: translate('Elapsed Time'), label: translate('ElapsedTime'),
isSortable: false, isSortable: false,
isVisible: true isVisible: true
}, },
+1 -1
View File
@@ -25,7 +25,7 @@ const columns = [
}, },
{ {
name: 'size', name: 'size',
label: 'Size', label: translate('Size'),
isVisible: true isVisible: true
}, },
{ {
+1 -1
View File
@@ -113,7 +113,7 @@ class Updates extends Component {
/> />
<div className={styles.message}> <div className={styles.message}>
The latest version of Prowlarr is already installed {translate('TheLatestVersionIsAlreadyInstalled', ['Prowlarr'])}
</div> </div>
{ {
+2 -2
View File
@@ -11,7 +11,7 @@
<!-- Windows Phone --> <!-- Windows Phone -->
<meta name="msapplication-navbutton-color" content="#3a3f51" /> <meta name="msapplication-navbutton-color" content="#3a3f51" />
<meta name="description" content="Prowlarr (Preview)" /> <meta name="description" content="Prowlarr" />
<link <link
rel="apple-touch-icon" rel="apple-touch-icon"
@@ -50,7 +50,7 @@
<link rel="stylesheet" type="text/css" href="/Content/Fonts/fonts.css"> <link rel="stylesheet" type="text/css" href="/Content/Fonts/fonts.css">
<!-- webpack bundles head --> <!-- webpack bundles head -->
<title>Prowlarr (Preview)</title> <title>Prowlarr</title>
<!-- <!--
The super basic styling for .root will live here, The super basic styling for .root will live here,
+4 -2
View File
@@ -11,7 +11,8 @@
"lint": "esprint check", "lint": "esprint check",
"lint-fix": "esprint check --fix", "lint-fix": "esprint check --fix",
"stylelint-linux": "stylelint $(find frontend -name '*.css') --config frontend/.stylelintrc", "stylelint-linux": "stylelint $(find frontend -name '*.css') --config frontend/.stylelintrc",
"stylelint-windows": "stylelint frontend/**/*.css --config frontend/.stylelintrc" "stylelint-windows": "stylelint frontend/**/*.css --config frontend/.stylelintrc",
"check-modules": "are-you-es5 check . -r"
}, },
"repository": "https://github.com/Prowlarr/Prowlarr", "repository": "https://github.com/Prowlarr/Prowlarr",
"author": "Team Prowlarr", "author": "Team Prowlarr",
@@ -30,7 +31,7 @@
"@fortawesome/free-regular-svg-icons": "6.2.1", "@fortawesome/free-regular-svg-icons": "6.2.1",
"@fortawesome/free-solid-svg-icons": "6.2.1", "@fortawesome/free-solid-svg-icons": "6.2.1",
"@fortawesome/react-fontawesome": "0.2.0", "@fortawesome/react-fontawesome": "0.2.0",
"@microsoft/signalr": "6.0.11", "@microsoft/signalr": "6.0.13",
"@sentry/browser": "7.28.0", "@sentry/browser": "7.28.0",
"@sentry/integrations": "7.28.0", "@sentry/integrations": "7.28.0",
"chart.js": "4.1.1", "chart.js": "4.1.1",
@@ -93,6 +94,7 @@
"@babel/plugin-syntax-dynamic-import": "7.8.3", "@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/preset-env": "7.20.2", "@babel/preset-env": "7.20.2",
"@babel/preset-react": "7.18.6", "@babel/preset-react": "7.18.6",
"are-you-es5": "2.1.2",
"autoprefixer": "10.4.13", "autoprefixer": "10.4.13",
"babel-loader": "9.1.0", "babel-loader": "9.1.0",
"babel-plugin-inline-classnames": "2.0.1", "babel-plugin-inline-classnames": "2.0.1",
+3
View File
@@ -0,0 +1,3 @@
is_global = true
dotnet_diagnostic.CA1014.severity = none
+1
View File
@@ -1,6 +1,7 @@
<Project> <Project>
<!-- Common to all Prowlarr Projects --> <!-- Common to all Prowlarr Projects -->
<PropertyGroup> <PropertyGroup>
<AnalysisLevel>6.0-all</AnalysisLevel>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors> <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles> <ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
<PlatformTarget>AnyCPU</PlatformTarget> <PlatformTarget>AnyCPU</PlatformTarget>
@@ -53,6 +53,26 @@ namespace NzbDrone.Common.Test.Http
newUri.FullUri.Should().Be(expected); newUri.FullUri.Should().Be(expected);
} }
[TestCase("", "./relative", "relative")]
[TestCase("/", "./relative", "/relative")]
[TestCase("/base", "./relative", "/relative")]
[TestCase("/base/sub", "./relative", "/base/relative")]
[TestCase("/base/sub/", "./relative", "/base/sub/relative")]
[TestCase("base/sub", "./relative", "base/relative")]
[TestCase("base/sub/", "./relative", "base/sub/relative")]
[TestCase("", "../relative", "relative")]
[TestCase("/", "../relative", "/relative")]
[TestCase("/base", "../relative", "/relative")]
[TestCase("/base/sub", "../relative", "/base/relative")]
[TestCase("/base/sub/", "../relative", "/base/sub/relative")]
[TestCase("base/sub", "../relative", "base/relative")]
[TestCase("base/sub/", "../relative", "base/sub/relative")]
public void should_combine_uri_with_dot_segment(string basePath, string relativePath, string expected)
{
var newUri = new HttpUri(basePath) + new HttpUri(relativePath);
newUri.FullUri.Should().Be(expected);
}
[TestCase("", "", "")] [TestCase("", "", "")]
[TestCase("/", "", "/")] [TestCase("/", "", "/")]
[TestCase("base", "", "base")] [TestCase("base", "", "base")]
@@ -24,12 +24,16 @@ namespace NzbDrone.Common.Test.InstrumentationTests
[TestCase(@"https://beyond-hd.me/api/torrents/2b51db35e1912ffc138825a12b9933d2")] [TestCase(@"https://beyond-hd.me/api/torrents/2b51db35e1912ffc138825a12b9933d2")]
[TestCase(@"Req: [POST] https://www3.yggtorrent.nz/user/login: id=mySecret&pass=mySecret&ci_csrf_token=2b51db35e1912ffc138825a12b9933d2")] [TestCase(@"Req: [POST] https://www3.yggtorrent.nz/user/login: id=mySecret&pass=mySecret&ci_csrf_token=2b51db35e1912ffc138825a12b9933d2")]
[TestCase(@"https://torrentseeds.org/api/torrents/filter?api_token=2b51db35e1912ffc138825a12b9933d2&name=&sortField=created_at&sortDirection=desc&perPage=100&page=1")] [TestCase(@"https://torrentseeds.org/api/torrents/filter?api_token=2b51db35e1912ffc138825a12b9933d2&name=&sortField=created_at&sortDirection=desc&perPage=100&page=1")]
[TestCase(@"https://beyond-hd.me/torrent/download/the-next-365-days-2022-2160p-nf-web-dl-dual-ddp-51-dovi-hdr-hevc-apex.225146.2b51db35e1912ffc138825a12b9933d2")]
[TestCase(@"https://anthelion.me/api.php?api_key=2b51db35e1910123321025a12b9933d2&o=json&t=movie&q=&tmdb=&imdb=&cat=&limit=100&offset=0")]
[TestCase(@"https://avistaz.to/api/v1/jackett/auth: username=mySecret&password=mySecret&pid=mySecret")]
// Indexer and Download Client Responses // Indexer and Download Client Responses
// avistaz response // avistaz response
[TestCase(@"""download"":""https://avistaz.to/rss/download/2b51db35e1910123321025a12b9933d2/tb51db35e1910123321025a12b9933d2.torrent"",")] [TestCase(@"""download"":""https://avistaz.to/rss/download/2b51db35e1910123321025a12b9933d2/tb51db35e1910123321025a12b9933d2.torrent"",")]
[TestCase(@",""info_hash"":""2b51db35e1910123321025a12b9933d2"",")] [TestCase(@",""info_hash"":""2b51db35e1910123321025a12b9933d2"",")]
[TestCase(@"""token"":""2b51db35e1910123321025a12b9933d2""")]
// animebytes response // animebytes response
[TestCase(@"""Link"":""https://animebytes.tv/torrent/994064/download/tb51db35e1910123321025a12b9933d2"",")] [TestCase(@"""Link"":""https://animebytes.tv/torrent/994064/download/tb51db35e1910123321025a12b9933d2"",")]
@@ -131,7 +131,7 @@ namespace NzbDrone.Common.Extensions
public static string WrapInQuotes(this string text) public static string WrapInQuotes(this string text)
{ {
if (!text.Contains(" ")) if (!text.Contains(' '))
{ {
return text; return text;
} }
@@ -255,7 +255,7 @@ namespace NzbDrone.Common.Extensions
public static string ToUrlHost(this string input) public static string ToUrlHost(this string input)
{ {
return input.Contains(":") ? $"[{input}]" : input; return input.Contains(':') ? $"[{input}]" : input;
} }
} }
} }
@@ -113,7 +113,7 @@ namespace NzbDrone.Common.Http.Dispatchers
{ {
if (request.ResponseStream != null && responseMessage.StatusCode == HttpStatusCode.OK) 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 else
{ {
+1
View File
@@ -76,6 +76,7 @@ namespace NzbDrone.Common.Http
get get
{ {
var newUrl = Headers["Location"]; var newUrl = Headers["Location"];
if (newUrl == null) if (newUrl == null)
{ {
newUrl = Headers["Refresh"]; newUrl = Headers["Refresh"];
+31
View File
@@ -166,6 +166,37 @@ namespace NzbDrone.Common.Http
return relativePath; return relativePath;
} }
if (relativePath.StartsWith("./"))
{
relativePath = relativePath.TrimStart('.').TrimStart('/');
var lastIndex = basePath.LastIndexOf("/");
if (lastIndex > 0)
{
basePath = basePath.Substring(0, lastIndex) + "/";
}
}
if (relativePath.StartsWith("../"))
{
relativePath = relativePath.TrimStart('.').TrimStart('/');
var lastIndex = basePath.LastIndexOf("/");
if (lastIndex > 0)
{
basePath = basePath.Substring(0, lastIndex) + "/";
}
var secondLastIndex = basePath.LastIndexOf("/");
if (lastIndex > 0)
{
basePath = basePath.Substring(0, secondLastIndex) + "/";
}
}
var baseSlashIndex = basePath.LastIndexOf('/'); var baseSlashIndex = basePath.LastIndexOf('/');
if (baseSlashIndex >= 0) if (baseSlashIndex >= 0)
@@ -1,4 +1,4 @@
using System; using System;
namespace NzbDrone.Common.Http namespace NzbDrone.Common.Http
{ {
@@ -11,19 +11,23 @@ namespace NzbDrone.Common.Http
{ {
if (response.Headers.ContainsKey("Retry-After")) if (response.Headers.ContainsKey("Retry-After"))
{ {
var retryAfter = response.Headers["Retry-After"].ToString(); var retryAfter = response.Headers["Retry-After"];
int seconds;
DateTime date;
if (int.TryParse(retryAfter, out seconds)) if (int.TryParse(retryAfter, out var seconds))
{ {
RetryAfter = TimeSpan.FromSeconds(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; RetryAfter = date.ToUniversalTime() - DateTime.UtcNow;
} }
} }
} }
public TooManyRequestsException(HttpRequest request, HttpResponse response, TimeSpan retryWait)
: base(request, response)
{
RetryAfter = retryWait;
}
} }
} }
@@ -1,4 +1,3 @@
using System;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@@ -8,10 +7,10 @@ namespace NzbDrone.Common.Instrumentation
{ {
public class CleanseLogMessage public class CleanseLogMessage
{ {
private static readonly Regex[] CleansingRules = new[] private static readonly Regex[] CleansingRules =
{ {
// Url // Url
new Regex(@"(?<=[?&: ;])(apikey|(?:(?:access|api)[-_]?)?token|pass(?:key|wd)?|auth|authkey|user|u?id|api|[a-z_]*apikey|account|pwd)=(?<secret>[^&=""]+?)(?=[ ""&=]|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"(?<=[?&: ;])(apikey|api_key|(?:(?:access|api)[-_]?)?token|pass(?:key|wd)?|auth|authkey|user|u?id|api|[a-z_]*apikey|account|pid|pwd)=(?<secret>[^&=""]+?)(?=[ ""&=]|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=[?& ;])[^=]*?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"(?<=[?& ;])[^=]*?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"rss\.torrentleech\.org/(?!rss)(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"rss\.torrentleech\.org/(?!rss)(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"rss\.torrentleech\.org/rss/download/[0-9]+/(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"rss\.torrentleech\.org/rss/download/[0-9]+/(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
@@ -21,6 +20,7 @@ namespace NzbDrone.Common.Instrumentation
new Regex(@"\b(\w*)?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"\b(\w*)?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=authkey = "")(?<secret>[^&=]+?)(?="")", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"(?<=authkey = "")(?<secret>[^&=]+?)(?="")", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=beyond-hd\.[a-z]+/api/torrents/)(?<secret>[^&=][a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"(?<=beyond-hd\.[a-z]+/api/torrents/)(?<secret>[^&=][a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=beyond-hd\.[a-z]+/torrent/download/[\w\d-]+[.]\d+[.])(?<secret>[a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// UNIT3D // UNIT3D
new Regex(@"(?<=[a-z0-9-]+\.[a-z]+/torrent/download/\d+\.)(?<secret>[^&=][a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"(?<=[a-z0-9-]+\.[a-z]+/torrent/download/\d+\.)(?<secret>[^&=][a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
@@ -58,9 +58,10 @@ namespace NzbDrone.Common.Instrumentation
new Regex(@"(?:avistaz|exoticaz|cinemaz|privatehd)\.[a-z]{2,3}/rss/download/(?<secret>[^&=]+?)/(?<secret>[^&=]+?)\.torrent", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"(?:avistaz|exoticaz|cinemaz|privatehd)\.[a-z]{2,3}/rss/download/(?<secret>[^&=]+?)/(?<secret>[^&=]+?)\.torrent", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?:animebytes)\.[a-z]{2,3}/torrent/[0-9]+/download/(?<secret>[^&=]+?)[""]", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"(?:animebytes)\.[a-z]{2,3}/torrent/[0-9]+/download/(?<secret>[^&=]+?)[""]", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@",""info_hash"":""(?<secret>[^&=]+?)"",", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@",""info_hash"":""(?<secret>[^&=]+?)"",", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"""token"":""(?<secret>[^&=]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@",""pass[- _]?key"":""(?<secret>[^&=]+?)"",", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@",""pass[- _]?key"":""(?<secret>[^&=]+?)"",", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@",""rss[- _]?key"":""(?<secret>[^&=]+?)"",", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@",""rss[- _]?key"":""(?<secret>[^&=]+?)"",", RegexOptions.Compiled | RegexOptions.IgnoreCase),
}; };
private static readonly Regex CleanseRemoteIPRegex = new Regex(@"(?:Auth-\w+(?<!Failure|Unauthorized) ip|from) (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})", RegexOptions.Compiled); private static readonly Regex CleanseRemoteIPRegex = new Regex(@"(?:Auth-\w+(?<!Failure|Unauthorized) ip|from) (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})", RegexOptions.Compiled);
@@ -8,6 +8,7 @@ using System.Threading;
using NLog; using NLog;
using NLog.Common; using NLog.Common;
using NLog.Targets; using NLog.Targets;
using Npgsql;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using Sentry; using Sentry;
@@ -34,6 +35,14 @@ namespace NzbDrone.Common.Instrumentation.Sentry
SQLiteErrorCode.Auth SQLiteErrorCode.Auth
}; };
private static readonly HashSet<string> FilteredPostgresErrorCodes = new HashSet<string>
{
PostgresErrorCodes.OutOfMemory,
PostgresErrorCodes.TooManyConnections,
PostgresErrorCodes.DiskFull,
PostgresErrorCodes.ProgramLimitExceeded
};
// use string and not Type so we don't need a reference to the project // use string and not Type so we don't need a reference to the project
// where these are defined // where these are defined
private static readonly HashSet<string> FilteredExceptionTypeNames = new HashSet<string> private static readonly HashSet<string> FilteredExceptionTypeNames = new HashSet<string>
@@ -239,6 +248,19 @@ namespace NzbDrone.Common.Instrumentation.Sentry
return false; return false;
} }
var pgEx = logEvent.Exception as PostgresException;
if (pgEx != null && FilteredPostgresErrorCodes.Contains(pgEx.SqlState))
{
return false;
}
// We don't care about transient network and timeout errors
var npgEx = logEvent.Exception as NpgsqlException;
if (npgEx != null && npgEx.IsTransient)
{
return false;
}
if (FilteredExceptionTypeNames.Contains(logEvent.Exception.GetType().Name)) if (FilteredExceptionTypeNames.Contains(logEvent.Exception.GetType().Name))
{ {
return false; return false;
+1 -1
View File
@@ -226,7 +226,7 @@ namespace NzbDrone.Common.OAuth
#if WINRT #if WINRT
return CultureInfo.InvariantCulture.CompareInfo.Compare(left, right, CompareOptions.IgnoreCase) == 0; return CultureInfo.InvariantCulture.CompareInfo.Compare(left, right, CompareOptions.IgnoreCase) == 0;
#else #else
return string.Compare(left, right, StringComparison.InvariantCultureIgnoreCase) == 0; return string.Equals(left, right, StringComparison.InvariantCultureIgnoreCase);
#endif #endif
} }
+2 -1
View File
@@ -4,12 +4,13 @@
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants> <DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="DryIoc.dll" Version="5.3.1" /> <PackageReference Include="DryIoc.dll" Version="5.3.3" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.1" /> <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
<PackageReference Include="NLog" Version="5.1.0" /> <PackageReference Include="NLog" Version="5.1.0" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.2.0" /> <PackageReference Include="NLog.Extensions.Logging" Version="5.2.0" />
<PackageReference Include="Npgsql" Version="5.0.11" />
<PackageReference Include="Sentry" Version="3.24.1" /> <PackageReference Include="Sentry" Version="3.24.1" />
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" /> <PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
<PackageReference Include="SharpZipLib" Version="1.3.3" /> <PackageReference Include="SharpZipLib" Version="1.3.3" />
+1 -1
View File
@@ -64,7 +64,7 @@ namespace NzbDrone.Common
var args = $"create {serviceName} " + var args = $"create {serviceName} " +
$"DisplayName= \"{serviceName}\" " + $"DisplayName= \"{serviceName}\" " +
$"binpath= \"{Process.GetCurrentProcess().MainModule.FileName}\" " + $"binpath= \"{Environment.ProcessPath}\" " +
"start= auto " + "start= auto " +
"depend= EventLog/Tcpip/http " + "depend= EventLog/Tcpip/http " +
"obj= \"NT AUTHORITY\\LocalService\""; "obj= \"NT AUTHORITY\\LocalService\"";
@@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
@@ -19,7 +19,7 @@ namespace NzbDrone.Common.TPL
private readonly int _maxDegreeOfParallelism; private readonly int _maxDegreeOfParallelism;
/// <summary>Whether the scheduler is currently processing work items.</summary> /// <summary>Whether the scheduler is currently processing work items.</summary>
private int _delegatesQueuedOrRunning = 0; private int _delegatesQueuedOrRunning;
/// <summary> /// <summary>
/// Initializes an instance of the LimitedConcurrencyLevelTaskScheduler class with the /// Initializes an instance of the LimitedConcurrencyLevelTaskScheduler class with the
@@ -28,15 +28,15 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
[Test] [Test]
public void should_return_warning_when_branch_not_valid() public void should_return_warning_when_branch_not_valid()
{ {
GivenValidBranch("master"); GivenValidBranch("test");
Subject.Check().ShouldBeWarning(); Subject.Check().ShouldBeWarning();
} }
[TestCase("Develop")]
[TestCase("develop")]
[TestCase("nightly")] [TestCase("nightly")]
[TestCase("Nightly")] [TestCase("Nightly")]
[TestCase("develop")]
[TestCase("master")]
public void should_return_no_warning_when_branch_valid(string branch) public void should_return_no_warning_when_branch_valid(string branch)
{ {
GivenValidBranch(branch); GivenValidBranch(branch);
@@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
torrentInfo.InfoUrl.Should().Be("https://avistaz.to/torrent/187240-japan-sinks-people-of-hope-2021-s01e05-720p-nf-web-dl-ddp20-x264-seikel"); torrentInfo.InfoUrl.Should().Be("https://avistaz.to/torrent/187240-japan-sinks-people-of-hope-2021-s01e05-720p-nf-web-dl-ddp20-x264-seikel");
torrentInfo.CommentUrl.Should().BeNullOrEmpty(); torrentInfo.CommentUrl.Should().BeNullOrEmpty();
torrentInfo.Indexer.Should().Be(Subject.Definition.Name); torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-11-15 04:26:21")); torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-11-14 22:26:21"));
torrentInfo.Size.Should().Be(935127615); torrentInfo.Size.Should().Be(935127615);
torrentInfo.InfoHash.Should().Be("a879261d4e6e792402f92401141a21de70d51bf2"); torrentInfo.InfoHash.Should().Be("a879261d4e6e792402f92401141a21de70d51bf2");
torrentInfo.MagnetUrl.Should().Be(null); torrentInfo.MagnetUrl.Should().Be(null);
@@ -71,12 +71,12 @@ namespace NzbDrone.Core.Test.IndexerTests.CardigannTests
result.Should().Be(expected); result.Should().Be(expected);
} }
[TestCase("{{ .Today.Year }}", "2022")] [TestCase("{{ .Today.Year }}")]
public void should_handle_variables_statements(string template, string expected) public void should_handle_variables_statements(string template)
{ {
var result = Subject.ApplyGoTemplateText(template, _variables); var result = Subject.ApplyGoTemplateText(template, _variables);
result.Should().Be(expected); result.Should().Be(DateTime.Now.Year.ToString());
} }
[TestCase("{{if .False }}0{{else}}1{{end}}", "1")] [TestCase("{{if .False }}0{{else}}1{{end}}", "1")]
@@ -8,7 +8,7 @@ using Moq;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.FileList; using NzbDrone.Core.Indexers.Definitions.FileList;
using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
@@ -21,10 +21,15 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
[SetUp] [SetUp]
public void Setup() public void Setup()
{ {
Subject.Definition = new IndexerDefinition() Subject.Definition = new IndexerDefinition
{ {
Name = "FileList", Name = "FileList",
Settings = new FileListSettings() { Username = "someuser", Passkey = "somepass" } Settings = new FileListSettings
{
BaseUrl = "https://filelist.io/",
Username = "someuser",
Passkey = "somepass"
}
}; };
} }
@@ -35,9 +40,9 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
Mocker.GetMock<IIndexerHttpClient>() Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition)) .Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed))); .Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)));
var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 } })).Releases; var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new[] { 2000 } })).Releases;
releases.Should().HaveCount(4); releases.Should().HaveCount(4);
releases.First().Should().BeOfType<TorrentInfo>(); releases.First().Should().BeOfType<TorrentInfo>();
@@ -50,12 +55,14 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
torrentInfo.InfoUrl.Should().Be("https://filelist.io/details.php?id=665873"); torrentInfo.InfoUrl.Should().Be("https://filelist.io/details.php?id=665873");
torrentInfo.CommentUrl.Should().BeNullOrEmpty(); torrentInfo.CommentUrl.Should().BeNullOrEmpty();
torrentInfo.Indexer.Should().Be(Subject.Definition.Name); torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2020-01-25 22:20:19")); torrentInfo.PublishDate.Should().Be(DateTime.Parse("2020-01-25 20:20:19"));
torrentInfo.Size.Should().Be(8300512414); torrentInfo.Size.Should().Be(8300512414);
torrentInfo.InfoHash.Should().Be(null); torrentInfo.InfoHash.Should().Be(null);
torrentInfo.MagnetUrl.Should().Be(null); torrentInfo.MagnetUrl.Should().Be(null);
torrentInfo.Peers.Should().Be(2 + 12); torrentInfo.Peers.Should().Be(2 + 12);
torrentInfo.Seeders.Should().Be(12); torrentInfo.Seeders.Should().Be(12);
releases.Any(t => t.IndexerFlags.Contains(IndexerFlag.Internal)).Should().Be(true);
} }
} }
} }
@@ -3,7 +3,7 @@ using System.Linq;
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.FileList; using NzbDrone.Core.Indexers.Definitions.FileList;
using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
@@ -16,34 +16,35 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
[SetUp] [SetUp]
public void Setup() public void Setup()
{ {
Subject.Settings = new FileListSettings() Subject.Settings = new FileListSettings
{ {
BaseUrl = "https://filelist.io/",
Passkey = "abcd", Passkey = "abcd",
Username = "somename", Username = "somename"
BaseUrl = "https://filelist.io"
}; };
Subject.Capabilities = new IndexerCapabilities Subject.Capabilities = new IndexerCapabilities
{ {
TvSearchParams = new List<TvSearchParam> TvSearchParams = new List<TvSearchParam>
{ {
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep TvSearchParam.Q, TvSearchParam.ImdbId, TvSearchParam.Season, TvSearchParam.Ep
}, },
MovieSearchParams = new List<MovieSearchParam> MovieSearchParams = new List<MovieSearchParam>
{ {
MovieSearchParam.Q, MovieSearchParam.ImdbId MovieSearchParam.Q, MovieSearchParam.ImdbId
}, },
MusicSearchParams = new List<MusicSearchParam> MusicSearchParams = new List<MusicSearchParam>
{ {
MusicSearchParam.Q MusicSearchParam.Q
}, },
BookSearchParams = new List<BookSearchParam> BookSearchParams = new List<BookSearchParam>
{ {
BookSearchParam.Q BookSearchParam.Q
}, },
Flags = new List<IndexerFlag> Flags = new List<IndexerFlag>
{ {
IndexerFlag.FreeLeech IndexerFlag.FreeLeech,
IndexerFlag.Internal,
} }
}; };
@@ -53,7 +54,7 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
_movieSearchCriteria = new MovieSearchCriteria _movieSearchCriteria = new MovieSearchCriteria
{ {
SearchTerm = "Star Wars", SearchTerm = "Star Wars",
Categories = new int[] { 2000 } Categories = new[] { 2000 }
}; };
} }
@@ -65,13 +66,13 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
[Test] [Test]
public void should_use_categories_for_feed() public void should_use_categories_for_feed()
{ {
var results = Subject.GetSearchRequests(new MovieSearchCriteria { Categories = new int[] { NewznabStandardCategory.MoviesSD.Id, NewznabStandardCategory.MoviesDVD.Id } }); var results = Subject.GetSearchRequests(new MovieSearchCriteria { Categories = new[] { NewznabStandardCategory.MoviesSD.Id, NewznabStandardCategory.MoviesDVD.Id } });
results.GetAllTiers().Should().HaveCount(1); results.GetAllTiers().Should().HaveCount(1);
var page = results.GetAllTiers().First().First(); var page = results.GetAllTiers().First().First();
page.Url.Query.Should().Contain("&category=1,2&"); page.Url.Query.Should().Contain("&category=1%2C2");
} }
[Test] [Test]
@@ -100,7 +101,7 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
var page = results.GetAllTiers().First().First(); var page = results.GetAllTiers().First().First();
page.Url.Query.Should().Contain("type=name"); page.Url.Query.Should().Contain("type=name");
page.Url.Query.Should().Contain("query=Star Wars"); page.Url.Query.Should().Contain("query=Star+Wars");
} }
} }
} }
@@ -10,7 +10,7 @@ using NUnit.Framework;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer; using NzbDrone.Common.Serializer;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.HDBits; using NzbDrone.Core.Indexers.Definitions.HDBits;
using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
@@ -5,7 +5,7 @@ using Newtonsoft.Json;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.HDBits; using NzbDrone.Core.Indexers.Definitions.HDBits;
using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
@@ -14,11 +14,13 @@ namespace NzbDrone.Core.Test.IndexerTests.HDBitsTests
public class HDBitsRequestGeneratorFixture : CoreTest<HDBitsRequestGenerator> public class HDBitsRequestGeneratorFixture : CoreTest<HDBitsRequestGenerator>
{ {
private MovieSearchCriteria _movieSearchCriteria; private MovieSearchCriteria _movieSearchCriteria;
private TvSearchCriteria _tvSearchSeasonEpisodeCriteria;
private TvSearchCriteria _tvSearchDailyEpisodeCriteria;
[SetUp] [SetUp]
public void Setup() public void Setup()
{ {
Subject.Settings = new HDBitsSettings() Subject.Settings = new HDBitsSettings
{ {
ApiKey = "abcd", ApiKey = "abcd",
Username = "somename" Username = "somename"
@@ -47,9 +49,25 @@ namespace NzbDrone.Core.Test.IndexerTests.HDBitsTests
_movieSearchCriteria = new MovieSearchCriteria _movieSearchCriteria = new MovieSearchCriteria
{ {
Categories = new int[] { 2000, 2010 }, Categories = new[] { 2000, 2010 },
ImdbId = "0076759" 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] [Test]
@@ -70,5 +88,49 @@ namespace NzbDrone.Core.Test.IndexerTests.HDBitsTests
query.Category.Should().HaveCount(1); query.Category.Should().HaveCount(1);
query.ImdbInfo.Id.Should().Be(imdbQuery); 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();
}
} }
} }
@@ -9,7 +9,7 @@ using NUnit.Framework;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.Definitions; using NzbDrone.Core.Indexers.Definitions;
using NzbDrone.Core.Indexers.Gazelle; using NzbDrone.Core.Indexers.Definitions.Gazelle;
using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
@@ -24,7 +24,7 @@ namespace NzbDrone.Core.Test.IndexerTests.OrpheusTests
Subject.Definition = new IndexerDefinition() Subject.Definition = new IndexerDefinition()
{ {
Name = "Orpheus", Name = "Orpheus",
Settings = new OrpheusSettings() { Apikey = "somekey" } Settings = new OrpheusSettings { Apikey = "somekey" }
}; };
} }
@@ -37,14 +37,14 @@ namespace NzbDrone.Core.Test.IndexerTests.OrpheusTests
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition)) .Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed))); .Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)));
var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new int[] { 2000 } })).Releases; var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new[] { 2000 } })).Releases;
releases.Should().HaveCount(65); releases.Should().HaveCount(65);
releases.First().Should().BeOfType<GazelleInfo>(); releases.First().Should().BeOfType<GazelleInfo>();
var torrentInfo = releases.First() as GazelleInfo; var torrentInfo = releases.First() as GazelleInfo;
torrentInfo.Title.Should().Be("The Beatles - Abbey Road (1969) [MP3 V2 (VBR)] [BD]"); torrentInfo.Title.Should().Be("The Beatles - Abbey Road [1969] [Album] [2.0 Mix 2019] [MP3 V2 (VBR)] [BD]");
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent); torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
torrentInfo.DownloadUrl.Should().Be("https://orpheus.network/ajax.php?action=download&id=1902448"); torrentInfo.DownloadUrl.Should().Be("https://orpheus.network/ajax.php?action=download&id=1902448");
torrentInfo.InfoUrl.Should().Be("https://orpheus.network/torrents.php?id=466&torrentid=1902448"); torrentInfo.InfoUrl.Should().Be("https://orpheus.network/torrents.php?id=466&torrentid=1902448");
@@ -53,7 +53,7 @@ namespace NzbDrone.Core.Test.IndexerTests.RarbgTests
torrentInfo.Title.Should().Be("Sense8.S01E01.WEBRip.x264-FGT"); torrentInfo.Title.Should().Be("Sense8.S01E01.WEBRip.x264-FGT");
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent); 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.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={BuildInfo.AppName}_{BuildInfo.Version}");
torrentInfo.Indexer.Should().Be(Subject.Definition.Name); torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2015-06-05 16:58:11 +0000").ToUniversalTime()); torrentInfo.PublishDate.Should().Be(DateTime.Parse("2015-06-05 16:58:11 +0000").ToUniversalTime());
torrentInfo.Size.Should().Be(564198371); torrentInfo.Size.Should().Be(564198371);
@@ -9,7 +9,7 @@ using NUnit.Framework;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.Definitions; using NzbDrone.Core.Indexers.Definitions;
using NzbDrone.Core.Indexers.Gazelle; using NzbDrone.Core.Indexers.Definitions.Gazelle;
using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
@@ -162,7 +162,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
releaseInfo.InfoHash.Should().Be("(removed)"); releaseInfo.InfoHash.Should().Be("(removed)");
releaseInfo.Seeders.Should().Be(3); releaseInfo.Seeders.Should().Be(3);
releaseInfo.Peers.Should().Be(3); releaseInfo.Peers.Should().Be(3);
releaseInfo.Categories.Count().Should().Be(4); releaseInfo.Categories.Count.Should().Be(4);
} }
[Test] [Test]
@@ -6,7 +6,7 @@
<PackageReference Include="Dapper" Version="2.0.123" /> <PackageReference Include="Dapper" Version="2.0.123" />
<PackageReference Include="NBuilder" Version="6.1.0" /> <PackageReference Include="NBuilder" Version="6.1.0" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" /> <PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
<PackageReference Include="YamlDotNet" Version="12.3.1" /> <PackageReference Include="YamlDotNet" Version="13.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\NzbDrone.Test.Common\Prowlarr.Test.Common.csproj" /> <ProjectReference Include="..\NzbDrone.Test.Common\Prowlarr.Test.Common.csproj" />
@@ -90,7 +90,7 @@ namespace NzbDrone.Core.Test.ThingiProviderTests
var status = Subject.GetBlockedProviders().FirstOrDefault(); var status = Subject.GetBlockedProviders().FirstOrDefault();
status.Should().NotBeNull(); status.Should().NotBeNull();
status.DisabledTill.Should().HaveValue(); status.DisabledTill.Should().HaveValue();
status.DisabledTill.Value.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(5), 500); status.DisabledTill.Value.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(1), 500);
} }
[Test] [Test]
@@ -133,7 +133,7 @@ namespace NzbDrone.Core.Test.ThingiProviderTests
var status = Subject.GetBlockedProviders().FirstOrDefault(); var status = Subject.GetBlockedProviders().FirstOrDefault();
status.Should().NotBeNull(); status.Should().NotBeNull();
status.DisabledTill.Should().HaveValue(); status.DisabledTill.Should().HaveValue();
status.DisabledTill.Value.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(15), 500); status.DisabledTill.Value.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(5), 500);
} }
[Test] [Test]
@@ -160,7 +160,7 @@ namespace NzbDrone.Core.Test.ThingiProviderTests
status.Should().NotBeNull(); status.Should().NotBeNull();
origStatus.EscalationLevel.Should().Be(3); origStatus.EscalationLevel.Should().Be(3);
status.DisabledTill.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(5), 500); status.DisabledTill.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(1), 500);
} }
} }
} }
@@ -147,6 +147,7 @@ namespace NzbDrone.Core.Applications.LazyLibrarian
Categories = string.Join(",", indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray())), Categories = string.Join(",", indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray())),
Enabled = indexer.Enable, Enabled = indexer.Enable,
Type = schema, Type = schema,
Priority = indexer.Priority
}; };
return lazyLibrarianIndexer; return lazyLibrarianIndexer;
@@ -30,6 +30,7 @@ namespace NzbDrone.Core.Applications.LazyLibrarian
public bool Enabled { get; set; } public bool Enabled { get; set; }
public string Altername { get; set; } public string Altername { get; set; }
public LazyLibrarianProviderType Type { get; set; } public LazyLibrarianProviderType Type { get; set; }
public int Priority { get; set; }
public bool Equals(LazyLibrarianIndexer other) public bool Equals(LazyLibrarianIndexer other)
{ {
@@ -43,7 +44,8 @@ namespace NzbDrone.Core.Applications.LazyLibrarian
other.Name == Name && other.Name == Name &&
other.Categories == Categories && other.Categories == Categories &&
other.Enabled == Enabled && other.Enabled == Enabled &&
other.Altername == Altername; other.Altername == Altername &&
other.Priority == Priority;
} }
} }
} }
@@ -21,6 +21,8 @@ namespace NzbDrone.Core.Applications.LazyLibrarian
public class LazyLibrarianV1Proxy : ILazyLibrarianV1Proxy public class LazyLibrarianV1Proxy : ILazyLibrarianV1Proxy
{ {
private const int ProwlarrHighestPriority = 50;
private readonly IHttpClient _httpClient; private readonly IHttpClient _httpClient;
private readonly Logger _logger; private readonly Logger _logger;
@@ -90,7 +92,8 @@ namespace NzbDrone.Core.Applications.LazyLibrarian
{ "host", indexer.Host }, { "host", indexer.Host },
{ "prov_apikey", indexer.Apikey }, { "prov_apikey", indexer.Apikey },
{ "enabled", indexer.Enabled.ToString().ToLower() }, { "enabled", indexer.Enabled.ToString().ToLower() },
{ "categories", indexer.Categories } { "categories", indexer.Categories },
{ "dlpriority", CalculatePriority(indexer.Priority).ToString() }
}; };
var request = BuildRequest(settings, "/api", "addProvider", HttpMethod.Get, parameters); var request = BuildRequest(settings, "/api", "addProvider", HttpMethod.Get, parameters);
@@ -108,7 +111,8 @@ namespace NzbDrone.Core.Applications.LazyLibrarian
{ "prov_apikey", indexer.Apikey }, { "prov_apikey", indexer.Apikey },
{ "enabled", indexer.Enabled.ToString().ToLower() }, { "enabled", indexer.Enabled.ToString().ToLower() },
{ "categories", indexer.Categories }, { "categories", indexer.Categories },
{ "altername", indexer.Altername } { "altername", indexer.Altername },
{ "dlpriority", CalculatePriority(indexer.Priority).ToString() }
}; };
var request = BuildRequest(settings, "/api", "changeProvider", HttpMethod.Get, parameters); var request = BuildRequest(settings, "/api", "changeProvider", HttpMethod.Get, parameters);
@@ -191,5 +195,7 @@ namespace NzbDrone.Core.Applications.LazyLibrarian
return results; return results;
} }
private int CalculatePriority(int indexerPriority) => ProwlarrHighestPriority - indexerPriority + 1;
} }
} }
@@ -196,7 +196,7 @@ namespace NzbDrone.Core.Applications.Lidarr
if (indexer.Protocol == DownloadProtocol.Torrent) if (indexer.Protocol == DownloadProtocol.Torrent)
{ {
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value = indexer.AppProfile.Value.MinimumSeeders; lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.AppMinimumSeeders ?? indexer.AppProfile.Value.MinimumSeeders;
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedRatio; lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedRatio;
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime; lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
@@ -196,7 +196,7 @@ namespace NzbDrone.Core.Applications.Radarr
if (indexer.Protocol == DownloadProtocol.Torrent) if (indexer.Protocol == DownloadProtocol.Torrent)
{ {
radarrIndexer.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value = indexer.AppProfile.Value.MinimumSeeders; radarrIndexer.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.AppMinimumSeeders ?? indexer.AppProfile.Value.MinimumSeeders;
radarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedRatio; radarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedRatio;
radarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime; radarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
} }
@@ -192,7 +192,7 @@ namespace NzbDrone.Core.Applications.Readarr
if (indexer.Protocol == DownloadProtocol.Torrent) if (indexer.Protocol == DownloadProtocol.Torrent)
{ {
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value = indexer.AppProfile.Value.MinimumSeeders; readarrIndexer.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.AppMinimumSeeders ?? indexer.AppProfile.Value.MinimumSeeders;
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedRatio; readarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedRatio;
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime; readarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
@@ -198,7 +198,7 @@ namespace NzbDrone.Core.Applications.Sonarr
if (indexer.Protocol == DownloadProtocol.Torrent) if (indexer.Protocol == DownloadProtocol.Torrent)
{ {
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value = indexer.AppProfile.Value.MinimumSeeders; sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.AppMinimumSeeders ?? indexer.AppProfile.Value.MinimumSeeders;
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedRatio; sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedRatio;
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime; sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seasonPackSeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.PackSeedTime ?? ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime; sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seasonPackSeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.PackSeedTime ?? ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
@@ -192,7 +192,7 @@ namespace NzbDrone.Core.Applications.Whisparr
if (indexer.Protocol == DownloadProtocol.Torrent) if (indexer.Protocol == DownloadProtocol.Torrent)
{ {
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value = indexer.AppProfile.Value.MinimumSeeders; whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.AppMinimumSeeders ?? indexer.AppProfile.Value.MinimumSeeders;
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedRatio; whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedRatio;
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime; whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
} }
@@ -19,14 +19,14 @@ namespace NzbDrone.Core.Authentication
public class UserService : IUserService 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 IUserRepository _repo;
private readonly IAppFolderInfo _appFolderInfo; private readonly IAppFolderInfo _appFolderInfo;
private readonly IDiskProvider _diskProvider; private readonly IDiskProvider _diskProvider;
private static readonly int ITERATIONS = 10000;
private static readonly int SALT_SIZE = 128 / 8;
private static readonly int NUMBER_OF_BYTES = 256 / 8;
public UserService(IUserRepository repo, IAppFolderInfo appFolderInfo, IDiskProvider diskProvider) public UserService(IUserRepository repo, IAppFolderInfo appFolderInfo, IDiskProvider diskProvider)
{ {
_repo = repo; _repo = repo;
@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Configuration
var releaseInfoPath = Path.Combine(bin, "release_info"); var releaseInfoPath = Path.Combine(bin, "release_info");
PackageUpdateMechanism = UpdateMechanism.BuiltIn; PackageUpdateMechanism = UpdateMechanism.BuiltIn;
DefaultBranch = "develop"; DefaultBranch = "master";
if (Path.GetFileName(bin) == "bin" && diskProvider.FileExists(packageInfoPath)) if (Path.GetFileName(bin) == "bin" && diskProvider.FileExists(packageInfoPath))
{ {
@@ -0,0 +1,14 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(025)]
public class speedcd_userpasssettings_to_speedcdsettings : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Update.Table("Indexers").Set(new { ConfigContract = "SpeedCDSettings" }).Where(new { Implementation = "SpeedCD" });
}
}
}
@@ -0,0 +1,14 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(026)]
public class torrentday_cookiesettings_to_torrentdaysettings : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Update.Table("Indexers").Set(new { ConfigContract = "TorrentDaySettings" }).Where(new { Implementation = "TorrentDay" });
}
}
}
@@ -0,0 +1,14 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration;
[Migration(027)]
public class alpharatio_greatposterwall_config_contract : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Update.Table("Indexers").Set(new { ConfigContract = "AlphaRatioSettings" }).Where(new { Implementation = "AlphaRatio" });
Update.Table("Indexers").Set(new { ConfigContract = "GreatPosterWallSettings" }).Where(new { Implementation = "GreatPosterWall" });
}
}
@@ -0,0 +1,14 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration;
[Migration(028)]
public class remove_notwhatcd : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
// Remove, site dead
Delete.FromTable("Indexers").Row(new { Implementation = "NotWhatCD" });
}
}
@@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data; using System.Data;
using System.Text; using System.Text;
@@ -250,7 +250,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
} }
Index = end + 1; Index = end + 1;
identifier.Append(Buffer.Substring(start, end - start)); identifier.Append(Buffer.AsSpan(start, end - start));
if (Buffer[Index] != escape) if (Buffer[Index] != escape)
{ {
@@ -15,9 +15,9 @@ namespace NzbDrone.Core.Datastore
private const DbType EnumerableMultiParameter = (DbType)(-1); private const DbType EnumerableMultiParameter = (DbType)(-1);
private readonly string _paramNamePrefix; private readonly string _paramNamePrefix;
private readonly bool _requireConcreteValue = false; private readonly bool _requireConcreteValue;
private int _paramCount = 0; private int _paramCount;
private bool _gotConcreteValue = false; private bool _gotConcreteValue;
public WhereBuilderPostgres(Expression filter, bool requireConcreteValue, int seq) public WhereBuilderPostgres(Expression filter, bool requireConcreteValue, int seq)
{ {
@@ -15,9 +15,9 @@ namespace NzbDrone.Core.Datastore
private const DbType EnumerableMultiParameter = (DbType)(-1); private const DbType EnumerableMultiParameter = (DbType)(-1);
private readonly string _paramNamePrefix; private readonly string _paramNamePrefix;
private readonly bool _requireConcreteValue = false; private readonly bool _requireConcreteValue;
private int _paramCount = 0; private int _paramCount;
private bool _gotConcreteValue = false; private bool _gotConcreteValue;
public WhereBuilderSqlite(Expression filter, bool requireConcreteValue, int seq) public WhereBuilderSqlite(Expression filter, bool requireConcreteValue, int seq)
{ {
@@ -0,0 +1,27 @@
namespace NzbDrone.Core.Download.Clients.FreeboxDownload
{
public static class EncodingForBase64
{
public static string EncodeBase64(this string text)
{
if (text == null)
{
return null;
}
byte[] textAsBytes = System.Text.Encoding.UTF8.GetBytes(text);
return System.Convert.ToBase64String(textAsBytes);
}
public static string DecodeBase64(this string encodedText)
{
if (encodedText == null)
{
return null;
}
byte[] textAsBytes = System.Convert.FromBase64String(encodedText);
return System.Text.Encoding.UTF8.GetString(textAsBytes);
}
}
}
@@ -0,0 +1,10 @@
namespace NzbDrone.Core.Download.Clients.FreeboxDownload
{
public class FreeboxDownloadException : DownloadClientException
{
public FreeboxDownloadException(string message)
: base(message)
{
}
}
}
@@ -0,0 +1,8 @@
namespace NzbDrone.Core.Download.Clients.FreeboxDownload
{
public enum FreeboxDownloadPriority
{
Last = 0,
First = 1
}
}
@@ -0,0 +1,271 @@
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Download.Clients.FreeboxDownload.Responses;
namespace NzbDrone.Core.Download.Clients.FreeboxDownload
{
public interface IFreeboxDownloadProxy
{
void Authenticate(FreeboxDownloadSettings settings);
string AddTaskFromUrl(string url, string directory, bool addPaused, bool addFirst, FreeboxDownloadSettings settings);
string AddTaskFromFile(string fileName, byte[] fileContent, string directory, bool addPaused, bool addFirst, FreeboxDownloadSettings settings);
void DeleteTask(string id, bool deleteData, FreeboxDownloadSettings settings);
FreeboxDownloadConfiguration GetDownloadConfiguration(FreeboxDownloadSettings settings);
List<FreeboxDownloadTask> GetTasks(FreeboxDownloadSettings settings);
}
public class FreeboxDownloadProxy : IFreeboxDownloadProxy
{
private readonly IHttpClient _httpClient;
private readonly Logger _logger;
private ICached<string> _authSessionTokenCache;
public FreeboxDownloadProxy(ICacheManager cacheManager, IHttpClient httpClient, Logger logger)
{
_httpClient = httpClient;
_logger = logger;
_authSessionTokenCache = cacheManager.GetCache<string>(GetType(), "authSessionToken");
}
public void Authenticate(FreeboxDownloadSettings settings)
{
var request = BuildRequest(settings).Resource("/login").Build();
var response = ProcessRequest<FreeboxLogin>(request, settings);
if (response.Result.LoggedIn == false)
{
throw new DownloadClientAuthenticationException("Not logged");
}
}
public string AddTaskFromUrl(string url, string directory, bool addPaused, bool addFirst, FreeboxDownloadSettings settings)
{
var request = BuildRequest(settings).Resource("/downloads/add").Post();
request.Headers.ContentType = "application/x-www-form-urlencoded";
request.AddFormParameter("download_url", System.Web.HttpUtility.UrlPathEncode(url));
if (!directory.IsNullOrWhiteSpace())
{
request.AddFormParameter("download_dir", directory);
}
var response = ProcessRequest<FreeboxDownloadTask>(request.Build(), settings);
SetTorrentSettings(response.Result.Id, addPaused, addFirst, settings);
return response.Result.Id;
}
public string AddTaskFromFile(string fileName, byte[] fileContent, string directory, bool addPaused, bool addFirst, FreeboxDownloadSettings settings)
{
var request = BuildRequest(settings).Resource("/downloads/add").Post();
request.AddFormUpload("download_file", fileName, fileContent, "multipart/form-data");
if (directory.IsNotNullOrWhiteSpace())
{
request.AddFormParameter("download_dir", directory);
}
var response = ProcessRequest<FreeboxDownloadTask>(request.Build(), settings);
SetTorrentSettings(response.Result.Id, addPaused, addFirst, settings);
return response.Result.Id;
}
public void DeleteTask(string id, bool deleteData, FreeboxDownloadSettings settings)
{
var uri = "/downloads/" + id;
if (deleteData == true)
{
uri += "/erase";
}
var request = BuildRequest(settings).Resource(uri).Build();
request.Method = HttpMethod.Delete;
ProcessRequest<string>(request, settings);
}
public FreeboxDownloadConfiguration GetDownloadConfiguration(FreeboxDownloadSettings settings)
{
var request = BuildRequest(settings).Resource("/downloads/config/").Build();
return ProcessRequest<FreeboxDownloadConfiguration>(request, settings).Result;
}
public List<FreeboxDownloadTask> GetTasks(FreeboxDownloadSettings settings)
{
var request = BuildRequest(settings).Resource("/downloads/").Build();
return ProcessRequest<List<FreeboxDownloadTask>>(request, settings).Result;
}
private static string BuildCachedHeaderKey(FreeboxDownloadSettings settings)
{
return $"{settings.Host}:{settings.AppId}:{settings.AppToken}";
}
private void SetTorrentSettings(string id, bool addPaused, bool addFirst, FreeboxDownloadSettings settings)
{
var request = BuildRequest(settings).Resource("/downloads/" + id).Build();
request.Method = HttpMethod.Put;
var body = new Dictionary<string, object> { };
if (addPaused)
{
body.Add("status", FreeboxDownloadTaskStatus.Stopped.ToString().ToLower());
}
if (addFirst)
{
body.Add("queue_pos", "1");
}
if (body.Count == 0)
{
return;
}
request.SetContent(body.ToJson());
ProcessRequest<FreeboxDownloadTask>(request, settings);
}
private string GetSessionToken(HttpRequestBuilder requestBuilder, FreeboxDownloadSettings settings, bool force = false)
{
var sessionToken = _authSessionTokenCache.Find(BuildCachedHeaderKey(settings));
if (sessionToken == null || force)
{
_authSessionTokenCache.Remove(BuildCachedHeaderKey(settings));
_logger.Debug($"Client needs a new Session Token to reach the API with App ID '{settings.AppId}'");
// Obtaining a Session Token (from official documentation):
// To protect the app_token secret, it will never be used directly to authenticate the
// application, instead the API will provide a challenge the app will combine to its
// app_token to open a session and get a session_token.
// The validity of the session_token is limited in time and the app will have to renew
// this session_token once in a while.
// Retrieving the 'challenge' value (it changes frequently and have a limited time validity)
// needed to build password
var challengeRequest = requestBuilder.Resource("/login").Build();
challengeRequest.Method = HttpMethod.Get;
var challenge = ProcessRequest<FreeboxLogin>(challengeRequest, settings).Result.Challenge;
// The password is computed using the 'challenge' value and the 'app_token' ('App Token' setting)
var enc = System.Text.Encoding.ASCII;
var hmac = new HMACSHA1(enc.GetBytes(settings.AppToken));
hmac.Initialize();
var buffer = enc.GetBytes(challenge);
var password = System.BitConverter.ToString(hmac.ComputeHash(buffer)).Replace("-", "").ToLower();
// Both 'app_id' ('App ID' setting) and computed password are set to get a Session Token
var sessionRequest = requestBuilder.Resource("/login/session").Post().Build();
var body = new Dictionary<string, object>
{
{ "app_id", settings.AppId },
{ "password", password }
};
sessionRequest.SetContent(body.ToJson());
sessionToken = ProcessRequest<FreeboxLogin>(sessionRequest, settings).Result.SessionToken;
_authSessionTokenCache.Set(BuildCachedHeaderKey(settings), sessionToken);
_logger.Debug($"New Session Token stored in cache for App ID '{settings.AppId}', ready to reach API");
}
return sessionToken;
}
private HttpRequestBuilder BuildRequest(FreeboxDownloadSettings settings, bool authentication = true)
{
var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.ApiUrl)
{
LogResponseContent = true
};
requestBuilder.Headers.ContentType = "application/json";
if (authentication == true)
{
requestBuilder.SetHeader("X-Fbx-App-Auth", GetSessionToken(requestBuilder, settings));
}
return requestBuilder;
}
private FreeboxResponse<T> ProcessRequest<T>(HttpRequest request, FreeboxDownloadSettings settings)
{
request.LogResponseContent = true;
request.SuppressHttpError = true;
HttpResponse response;
try
{
response = _httpClient.Execute(request);
}
catch (HttpRequestException ex)
{
throw new DownloadClientUnavailableException($"Unable to reach Freebox API. Verify 'Host', 'Port' or 'Use SSL' settings. (Error: {ex.Message})", ex);
}
catch (WebException ex)
{
throw new DownloadClientUnavailableException("Unable to connect to Freebox API, please check your settings", ex);
}
if (response.StatusCode == HttpStatusCode.Forbidden || response.StatusCode == HttpStatusCode.Unauthorized)
{
_authSessionTokenCache.Remove(BuildCachedHeaderKey(settings));
var responseContent = Json.Deserialize<FreeboxResponse<FreeboxLogin>>(response.Content);
var msg = $"Authentication to Freebox API failed. Reason: {responseContent.GetErrorDescription()}";
_logger.Error(msg);
throw new DownloadClientAuthenticationException(msg);
}
else if (response.StatusCode == HttpStatusCode.NotFound)
{
throw new FreeboxDownloadException("Unable to reach Freebox API. Verify 'API URL' setting for base URL and version.");
}
else if (response.StatusCode == HttpStatusCode.OK)
{
var responseContent = Json.Deserialize<FreeboxResponse<T>>(response.Content);
if (responseContent.Success)
{
return responseContent;
}
else
{
var msg = $"Freebox API returned error: {responseContent.GetErrorDescription()}";
_logger.Error(msg);
throw new DownloadClientException(msg);
}
}
else
{
throw new DownloadClientException("Unable to connect to Freebox, please check your settings.");
}
}
}
}
@@ -0,0 +1,84 @@
using System.Text.RegularExpressions;
using FluentValidation;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
using NzbDrone.Core.Validation.Paths;
namespace NzbDrone.Core.Download.Clients.FreeboxDownload
{
public class FreeboxDownloadSettingsValidator : AbstractValidator<FreeboxDownloadSettings>
{
public FreeboxDownloadSettingsValidator()
{
RuleFor(c => c.Host).ValidHost();
RuleFor(c => c.Port).InclusiveBetween(1, 65535);
RuleFor(c => c.ApiUrl).NotEmpty()
.WithMessage("'API URL' must not be empty.");
RuleFor(c => c.ApiUrl).ValidUrlBase();
RuleFor(c => c.AppId).NotEmpty()
.WithMessage("'App ID' must not be empty.");
RuleFor(c => c.AppToken).NotEmpty()
.WithMessage("'App Token' must not be empty.");
RuleFor(c => c.Category).Matches(@"^\.?[-a-z]*$", RegexOptions.IgnoreCase)
.WithMessage("Allowed characters a-z and -");
RuleFor(c => c.DestinationDirectory).IsValidPath()
.When(c => c.DestinationDirectory.IsNotNullOrWhiteSpace());
RuleFor(c => c.DestinationDirectory).Empty()
.When(c => c.Category.IsNotNullOrWhiteSpace())
.WithMessage("Cannot use 'Category' and 'Destination Directory' at the same time.");
RuleFor(c => c.Category).Empty()
.When(c => c.DestinationDirectory.IsNotNullOrWhiteSpace())
.WithMessage("Cannot use 'Category' and 'Destination Directory' at the same time.");
}
}
public class FreeboxDownloadSettings : IProviderConfig
{
private static readonly FreeboxDownloadSettingsValidator Validator = new FreeboxDownloadSettingsValidator();
public FreeboxDownloadSettings()
{
Host = "mafreebox.freebox.fr";
Port = 443;
UseSsl = true;
ApiUrl = "/api/v1/";
}
[FieldDefinition(0, Label = "Host", Type = FieldType.Textbox, HelpText = "Hostname or host IP address of the Freebox, defaults to 'mafreebox.freebox.fr' (will only work if on same network)")]
public string Host { get; set; }
[FieldDefinition(1, Label = "Port", Type = FieldType.Textbox, HelpText = "Port used to access Freebox interface, defaults to '443'")]
public int Port { get; set; }
[FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secured connection when connecting to Freebox API")]
public bool UseSsl { get; set; }
[FieldDefinition(3, Label = "API URL", Type = FieldType.Textbox, Advanced = true, HelpText = "Define Freebox API base URL with API version, eg http://[host]:[port]/[api_base_url]/[api_version]/, defaults to '/api/v1/'")]
public string ApiUrl { get; set; }
[FieldDefinition(4, Label = "App ID", Type = FieldType.Textbox, HelpText = "App ID given when creating access to Freebox API (ie 'app_id')")]
public string AppId { get; set; }
[FieldDefinition(5, Label = "App Token", Type = FieldType.Password, Privacy = PrivacyLevel.Password, HelpText = "App token retrieved when creating access to Freebox API (ie 'app_token')")]
public string AppToken { get; set; }
[FieldDefinition(6, Label = "Destination Directory", Type = FieldType.Textbox, Advanced = true, HelpText = "Optional location to put downloads in, leave blank to use the default Freebox download location")]
public string DestinationDirectory { get; set; }
[FieldDefinition(7, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Prowlarr avoids conflicts with unrelated non-Prowlarr downloads (will create a [category] subdirectory in the output directory)")]
public string Category { get; set; }
[FieldDefinition(8, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(FreeboxDownloadPriority), HelpText = "Priority to use when grabbing")]
public int Priority { get; set; }
[FieldDefinition(10, Label = "Add Paused", Type = FieldType.Checkbox)]
public bool AddPaused { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}
@@ -0,0 +1,21 @@
using Newtonsoft.Json;
namespace NzbDrone.Core.Download.Clients.FreeboxDownload.Responses
{
public class FreeboxDownloadConfiguration
{
[JsonProperty(PropertyName = "download_dir")]
public string DownloadDirectory { get; set; }
public string DecodedDownloadDirectory
{
get
{
return DownloadDirectory.DecodeBase64();
}
set
{
DownloadDirectory = value.EncodeBase64();
}
}
}
}
@@ -0,0 +1,137 @@
using System.Collections.Generic;
using Newtonsoft.Json;
using NzbDrone.Common.Serializer;
namespace NzbDrone.Core.Download.Clients.FreeboxDownload.Responses
{
public enum FreeboxDownloadTaskType
{
Bt,
Nzb,
Http,
Ftp
}
public enum FreeboxDownloadTaskStatus
{
Unknown,
Stopped,
Queued,
Starting,
Downloading,
Stopping,
Error,
Done,
Checking,
Repairing,
Extracting,
Seeding,
Retry
}
public enum FreeboxDownloadTaskIoPriority
{
Low,
Normal,
High
}
public class FreeboxDownloadTask
{
private static readonly Dictionary<string, string> Descriptions;
[JsonProperty(PropertyName = "id")]
public string Id { get; set; }
[JsonProperty(PropertyName = "name")]
public string Name { get; set; }
[JsonProperty(PropertyName = "download_dir")]
public string DownloadDirectory { get; set; }
public string DecodedDownloadDirectory
{
get
{
return DownloadDirectory.DecodeBase64();
}
set
{
DownloadDirectory = value.EncodeBase64();
}
}
[JsonProperty(PropertyName = "info_hash")]
public string InfoHash { get; set; }
[JsonProperty(PropertyName = "queue_pos")]
public int QueuePosition { get; set; }
[JsonConverter(typeof(UnderscoreStringEnumConverter), FreeboxDownloadTaskStatus.Unknown)]
public FreeboxDownloadTaskStatus Status { get; set; }
[JsonProperty(PropertyName = "eta")]
public long Eta { get; set; }
[JsonProperty(PropertyName = "error")]
public string Error { get; set; }
[JsonProperty(PropertyName = "type")]
public string Type { get; set; }
[JsonProperty(PropertyName = "io_priority")]
public string IoPriority { get; set; }
[JsonProperty(PropertyName = "stop_ratio")]
public long StopRatio { get; set; }
[JsonProperty(PropertyName = "piece_length")]
public long PieceLength { get; set; }
[JsonProperty(PropertyName = "created_ts")]
public long CreatedTimestamp { get; set; }
[JsonProperty(PropertyName = "size")]
public long Size { get; set; }
[JsonProperty(PropertyName = "rx_pct")]
public long ReceivedPrct { get; set; }
[JsonProperty(PropertyName = "rx_bytes")]
public long ReceivedBytes { get; set; }
[JsonProperty(PropertyName = "rx_rate")]
public long ReceivedRate { get; set; }
[JsonProperty(PropertyName = "tx_pct")]
public long TransmittedPrct { get; set; }
[JsonProperty(PropertyName = "tx_bytes")]
public long TransmittedBytes { get; set; }
[JsonProperty(PropertyName = "tx_rate")]
public long TransmittedRate { get; set; }
static FreeboxDownloadTask()
{
Descriptions = new Dictionary<string, string>
{
{ "internal", "Internal error." },
{ "disk_full", "The disk is full." },
{ "unknown", "Unknown error." },
{ "parse_error", "Parse error." },
{ "unknown_host", "Unknown host." },
{ "timeout", "Timeout." },
{ "bad_authentication", "Invalid credentials." },
{ "connection_refused", "Remote host refused connection." },
{ "bt_tracker_error", "Unable to announce on tracker." },
{ "bt_missing_files", "Missing torrent files." },
{ "bt_file_error", "Error accessing torrent files." },
{ "missing_ctx_file", "Error accessing task context file." },
{ "nzb_no_group", "Cannot find the requested group on server." },
{ "nzb_not_found", "Article not fount on the server." },
{ "nzb_invalid_crc", "Invalid article CRC." },
{ "nzb_invalid_size", "Invalid article size." },
{ "nzb_invalid_filename", "Invalid filename." },
{ "nzb_open_failed", "Error opening." },
{ "nzb_write_failed", "Error writing." },
{ "nzb_missing_size", "Missing article size." },
{ "nzb_decode_error", "Article decoding error." },
{ "nzb_missing_segments", "Missing article segments." },
{ "nzb_error", "Other nzb error." },
{ "nzb_authentication_required", "Nzb server need authentication." }
};
}
public string GetErrorDescription()
{
if (Descriptions.ContainsKey(Error))
{
return Descriptions[Error];
}
return $"{Error} - Unknown error";
}
}
}
@@ -0,0 +1,18 @@
using Newtonsoft.Json;
namespace NzbDrone.Core.Download.Clients.FreeboxDownload.Responses
{
public class FreeboxLogin
{
[JsonProperty(PropertyName = "logged_in")]
public bool LoggedIn { get; set; }
[JsonProperty(PropertyName = "challenge")]
public string Challenge { get; set; }
[JsonProperty(PropertyName = "password_salt")]
public string PasswordSalt { get; set; }
[JsonProperty(PropertyName = "password_set")]
public bool PasswordSet { get; set; }
[JsonProperty(PropertyName = "session_token")]
public string SessionToken { get; set; }
}
}
@@ -0,0 +1,69 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace NzbDrone.Core.Download.Clients.FreeboxDownload.Responses
{
public class FreeboxResponse<T>
{
private static readonly Dictionary<string, string> Descriptions;
[JsonProperty(PropertyName = "success")]
public bool Success { get; set; }
[JsonProperty(PropertyName = "msg")]
public string Message { get; set; }
[JsonProperty(PropertyName = "error_code")]
public string ErrorCode { get; set; }
[JsonProperty(PropertyName = "result")]
public T Result { get; set; }
static FreeboxResponse()
{
Descriptions = new Dictionary<string, string>
{
// Common errors
{ "invalid_request", "Your request is invalid." },
{ "invalid_api_version", "Invalid API base url or unknown API version." },
{ "internal_error", "Internal error." },
// Login API errors
{ "auth_required", "Invalid session token, or no session token sent." },
{ "invalid_token", "The app token you are trying to use is invalid or has been revoked." },
{ "pending_token", "The app token you are trying to use has not been validated by user yet." },
{ "insufficient_rights", "Your app permissions does not allow accessing this API." },
{ "denied_from_external_ip", "You are trying to get an app_token from a remote IP." },
{ "ratelimited", "Too many auth error have been made from your IP." },
{ "new_apps_denied", "New application token request has been disabled." },
{ "apps_denied", "API access from apps has been disabled." },
// Download API errors
{ "task_not_found", "No task was found with the given id." },
{ "invalid_operation", "Attempt to perform an invalid operation." },
{ "invalid_file", "Error with the download file (invalid format ?)." },
{ "invalid_url", "URL is invalid." },
{ "not_implemented", "Method not implemented." },
{ "out_of_memory", "No more memory available to perform the requested action." },
{ "invalid_task_type", "The task type is invalid." },
{ "hibernating", "The downloader is hibernating." },
{ "need_bt_stopped_done", "This action is only valid for Bittorrent task in stopped or done state." },
{ "bt_tracker_not_found", "Attempt to access an invalid tracker object." },
{ "too_many_tasks", "Too many tasks." },
{ "invalid_address", "Invalid peer address." },
{ "port_conflict", "Port conflict when setting config." },
{ "invalid_priority", "Invalid priority." },
{ "ctx_file_error", "Failed to initialize task context file (need to check disk)." },
{ "exists", "Same task already exists." },
{ "port_outside_range", "Incoming port is not available for this customer." }
};
}
public string GetErrorDescription()
{
if (Descriptions.ContainsKey(ErrorCode))
{
return Descriptions[ErrorCode];
}
return $"{ErrorCode} - Unknown error";
}
}
}
@@ -0,0 +1,137 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download.Clients.FreeboxDownload.Responses;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Download.Clients.FreeboxDownload
{
public class TorrentFreeboxDownload : TorrentClientBase<FreeboxDownloadSettings>
{
private readonly IFreeboxDownloadProxy _proxy;
public TorrentFreeboxDownload(IFreeboxDownloadProxy proxy,
ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
IConfigService configService,
IDiskProvider diskProvider,
Logger logger)
: base(torrentFileInfoReader, httpClient, configService, diskProvider, logger)
{
_proxy = proxy;
}
public override string Name => "Freebox Download";
public override bool SupportsCategories => true;
protected IEnumerable<FreeboxDownloadTask> GetTorrents()
{
return _proxy.GetTasks(Settings).Where(v => v.Type.ToLower() == FreeboxDownloadTaskType.Bt.ToString().ToLower());
}
protected override string AddFromMagnetLink(ReleaseInfo release, string hash, string magnetLink)
{
return _proxy.AddTaskFromUrl(magnetLink,
GetDownloadDirectory(release).EncodeBase64(),
ToBePaused(),
ToBeQueuedFirst(),
Settings);
}
protected override string AddFromTorrentFile(ReleaseInfo release, string hash, string filename, byte[] fileContent)
{
return _proxy.AddTaskFromFile(filename,
fileContent,
GetDownloadDirectory(release).EncodeBase64(),
ToBePaused(),
ToBeQueuedFirst(),
Settings);
}
protected override string AddFromTorrentLink(ReleaseInfo release, string hash, string torrentLink)
{
return _proxy.AddTaskFromUrl(torrentLink,
GetDownloadDirectory(release).EncodeBase64(),
ToBePaused(),
ToBeQueuedFirst(),
Settings);
}
protected override void Test(List<ValidationFailure> failures)
{
try
{
_proxy.Authenticate(Settings);
}
catch (DownloadClientUnavailableException ex)
{
failures.Add(new ValidationFailure("Host", ex.Message));
failures.Add(new ValidationFailure("Port", ex.Message));
}
catch (DownloadClientAuthenticationException ex)
{
failures.Add(new ValidationFailure("AppId", ex.Message));
failures.Add(new ValidationFailure("AppToken", ex.Message));
}
catch (FreeboxDownloadException ex)
{
failures.Add(new ValidationFailure("ApiUrl", ex.Message));
}
}
protected override void ValidateCategories(List<ValidationFailure> failures)
{
base.ValidateCategories(failures);
foreach (var label in Categories)
{
if (!Regex.IsMatch(label.ClientCategory, "^\\.?[-a-z]*$"))
{
failures.AddIfNotNull(new ValidationFailure(string.Empty, "Mapped Categories allowed characters a-z and -"));
}
}
}
private string GetDownloadDirectory(ReleaseInfo release)
{
if (Settings.DestinationDirectory.IsNotNullOrWhiteSpace())
{
return Settings.DestinationDirectory.TrimEnd('/');
}
var destDir = _proxy.GetDownloadConfiguration(Settings).DecodedDownloadDirectory.TrimEnd('/');
if (Settings.Category.IsNotNullOrWhiteSpace())
{
var category = GetCategoryForRelease(release) ?? Settings.Category;
destDir = $"{destDir}/{category}";
}
return destDir;
}
private bool ToBeQueuedFirst()
{
if (Settings.Priority == (int)FreeboxDownloadPriority.First)
{
return true;
}
return false;
}
private bool ToBePaused()
{
return Settings.AddPaused;
}
}
}
+4 -13
View File
@@ -1,7 +1,6 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using NLog; using NLog;
using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Common.Instrumentation.Extensions; using NzbDrone.Common.Instrumentation.Extensions;
@@ -61,14 +60,6 @@ namespace NzbDrone.Core.Download
// Get the seed configuration for this release. // Get the seed configuration for this release.
// remoteMovie.SeedConfiguration = _seedConfigProvider.GetSeedConfiguration(remoteMovie); // 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 indexer = _indexerFactory.GetInstance(_indexerFactory.Get(release.IndexerId));
string downloadClientId; string downloadClientId;
@@ -92,8 +83,7 @@ namespace NzbDrone.Core.Download
} }
catch (ReleaseDownloadException ex) catch (ReleaseDownloadException ex)
{ {
var http429 = ex.InnerException as TooManyRequestsException; if (ex.InnerException is TooManyRequestsException http429)
if (http429 != null)
{ {
_indexerStatusService.RecordFailure(release.IndexerId, http429.RetryAfter); _indexerStatusService.RecordFailure(release.IndexerId, http429.RetryAfter);
} }
@@ -141,8 +131,7 @@ namespace NzbDrone.Core.Download
} }
catch (ReleaseDownloadException ex) catch (ReleaseDownloadException ex)
{ {
var http429 = ex.InnerException as TooManyRequestsException; if (ex.InnerException is TooManyRequestsException http429)
if (http429 != null)
{ {
_indexerStatusService.RecordFailure(indexerId, http429.RetryAfter); _indexerStatusService.RecordFailure(indexerId, http429.RetryAfter);
} }
@@ -155,7 +144,9 @@ namespace NzbDrone.Core.Download
throw; throw;
} }
_logger.Trace("Downloaded {0} bytes from {1}", downloadedBytes.Length, link);
_eventAggregator.PublishEvent(new IndexerDownloadEvent(indexerId, success, source, host, title, url.AbsoluteUri)); _eventAggregator.PublishEvent(new IndexerDownloadEvent(indexerId, success, source, host, title, url.AbsoluteUri));
return downloadedBytes; return downloadedBytes;
} }
@@ -31,6 +31,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
public enum ReleaseBranches public enum ReleaseBranches
{ {
Master,
Develop, Develop,
Nightly Nightly
} }
@@ -34,8 +34,8 @@ namespace NzbDrone.Core.HealthCheck
private readonly ICached<HealthCheck> _healthCheckResults; private readonly ICached<HealthCheck> _healthCheckResults;
private bool _hasRunHealthChecksAfterGracePeriod = false; private bool _hasRunHealthChecksAfterGracePeriod;
private bool _isRunningHealthChecksAfterGracePeriod = false; private bool _isRunningHealthChecksAfterGracePeriod;
public HealthCheckService(IEnumerable<IProvideHealthCheck> healthChecks, public HealthCheckService(IEnumerable<IProvideHealthCheck> healthChecks,
IServerSideNotificationService serverSideNotificationService, IServerSideNotificationService serverSideNotificationService,
@@ -55,7 +55,7 @@ namespace NzbDrone.Core.HealthCheck
_startupHealthChecks = _healthChecks.Where(v => v.CheckOnStartup).ToArray(); _startupHealthChecks = _healthChecks.Where(v => v.CheckOnStartup).ToArray();
_scheduledHealthChecks = _healthChecks.Where(v => v.CheckOnSchedule).ToArray(); _scheduledHealthChecks = _healthChecks.Where(v => v.CheckOnSchedule).ToArray();
_eventDrivenHealthChecks = GetEventDrivenHealthChecks(); _eventDrivenHealthChecks = GetEventDrivenHealthChecks();
_startupGracePeriodEndTime = runtimeInfo.StartTime + TimeSpan.FromMinutes(15); _startupGracePeriodEndTime = runtimeInfo.StartTime.AddMinutes(15);
} }
public List<HealthCheck> Results() public List<HealthCheck> Results()
@@ -19,6 +19,7 @@ namespace NzbDrone.Core.History
List<History> Since(DateTime date, HistoryEventType? eventType); List<History> Since(DateTime date, HistoryEventType? eventType);
void Cleanup(int days); void Cleanup(int days);
int CountSince(int indexerId, DateTime date, List<HistoryEventType> eventTypes); 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 public class HistoryRepository : BasicRepository<History>, IHistoryRepository
@@ -115,5 +116,24 @@ namespace NzbDrone.Core.History
return conn.ExecuteScalar<int>(sql.RawSql, sql.Parameters); 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);
}
} }
} }
+7 -1
View File
@@ -27,6 +27,7 @@ namespace NzbDrone.Core.History
List<History> Between(DateTime start, DateTime end); List<History> Between(DateTime start, DateTime end);
List<History> Since(DateTime date, HistoryEventType? eventType); List<History> Since(DateTime date, HistoryEventType? eventType);
int CountSince(int indexerId, DateTime date, List<HistoryEventType> eventTypes); int CountSince(int indexerId, DateTime date, List<HistoryEventType> eventTypes);
History FindFirstForIndexerSince(int indexerId, DateTime date, List<HistoryEventType> eventTypes, int limit);
} }
public class HistoryService : IHistoryService, public class HistoryService : IHistoryService,
@@ -173,7 +174,7 @@ namespace NzbDrone.Core.History
history.Data.Add("Categories", string.Join(",", message.Query.Categories) ?? string.Empty); history.Data.Add("Categories", string.Join(",", message.Query.Categories) ?? string.Empty);
history.Data.Add("Source", message.Query.Source ?? string.Empty); history.Data.Add("Source", message.Query.Source ?? string.Empty);
history.Data.Add("Host", message.Query.Host ?? 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); history.Data.Add("Url", message.QueryResult.Response?.Request.Url.FullUri ?? string.Empty);
_historyRepository.Insert(history); _historyRepository.Insert(history);
@@ -232,5 +233,10 @@ namespace NzbDrone.Core.History
{ {
return _historyRepository.CountSince(indexerId, date, eventTypes); 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);
}
} }
} }
@@ -1,15 +1,16 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Text.RegularExpressions;
using NLog; using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
namespace NzbDrone.Core.Http.CloudFlare namespace NzbDrone.Core.Http.CloudFlare
{ {
public class CloudFlareDetectionService 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; private readonly Logger _logger;
public CloudFlareDetectionService(Logger logger) public CloudFlareDetectionService(Logger logger)
@@ -28,12 +29,20 @@ namespace NzbDrone.Core.Http.CloudFlare
if (response.StatusCode.Equals(HttpStatusCode.ServiceUnavailable) || if (response.StatusCode.Equals(HttpStatusCode.ServiceUnavailable) ||
response.StatusCode.Equals(HttpStatusCode.Forbidden)) response.StatusCode.Equals(HttpStatusCode.Forbidden))
{ {
return true; // Defected CloudFlare and DDoS-GUARD var responseHtml = response.Content;
if (responseHtml.Contains("<title>Just a moment...</title>") ||
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>", StringComparison.OrdinalIgnoreCase))
{
return true;
}
} }
// detect Custom CloudFlare for EbookParadijs, Film-Paleis, MuziekFabriek and Puur-Hollands // detect Custom CloudFlare for EbookParadijs, Film-Paleis, MuziekFabriek and Puur-Hollands
if (response.Headers.Vary == "Accept-Encoding,User-Agent" && if (response.Headers.Vary == "Accept-Encoding,User-Agent" &&
response.Headers.ContentEncoding == "" && response.Headers.ContentEncoding.IsNullOrWhiteSpace() &&
response.Content.ToLower().Contains("ddos")) response.Content.ToLower().Contains("ddos"))
{ {
return true; return true;
@@ -170,6 +170,7 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
newRequest.Headers.ContentType = "application/json"; newRequest.Headers.ContentType = "application/json";
newRequest.Method = HttpMethod.Post; newRequest.Method = HttpMethod.Post;
newRequest.LogResponseContent = true; newRequest.LogResponseContent = true;
newRequest.RequestTimeout = TimeSpan.FromSeconds(Settings.RequestTimeout + 5);
newRequest.SetContent(req.ToJson()); newRequest.SetContent(req.ToJson());
_logger.Debug("Cloudflare Detected, Applying FlareSolverr Proxy {0} to request {1}", Name, request.Url); _logger.Debug("Cloudflare Detected, Applying FlareSolverr Proxy {0} to request {1}", Name, request.Url);
@@ -10,17 +10,6 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
public int? Year { get; set; } public int? Year { get; set; }
public string Genre { get; set; } public string Genre { get; set; }
public override bool RssSearch public override bool RssSearch => SearchTerm.IsNullOrWhiteSpace() && Author.IsNullOrWhiteSpace() && Title.IsNullOrWhiteSpace();
{
get
{
if (SearchTerm.IsNullOrWhiteSpace() && Author.IsNullOrWhiteSpace() && Title.IsNullOrWhiteSpace())
{
return true;
}
return false;
}
}
} }
} }
@@ -13,18 +13,7 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
public int? Year { get; set; } public int? Year { get; set; }
public string Genre { get; set; } public string Genre { get; set; }
public override bool RssSearch public override bool RssSearch => SearchTerm.IsNullOrWhiteSpace() && ImdbId.IsNullOrWhiteSpace() && !TmdbId.HasValue && !TraktId.HasValue;
{
get
{
if (SearchTerm.IsNullOrWhiteSpace() && ImdbId.IsNullOrWhiteSpace() && !TmdbId.HasValue && !TraktId.HasValue)
{
return true;
}
return false;
}
}
public string FullImdbId => ParseUtil.GetFullImdbId(ImdbId); public string FullImdbId => ParseUtil.GetFullImdbId(ImdbId);
@@ -11,17 +11,6 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
public string Track { get; set; } public string Track { get; set; }
public int? Year { get; set; } public int? Year { get; set; }
public override bool RssSearch public override bool RssSearch => SearchTerm.IsNullOrWhiteSpace() && Album.IsNullOrWhiteSpace() && Artist.IsNullOrWhiteSpace() && Label.IsNullOrWhiteSpace();
{
get
{
if (SearchTerm.IsNullOrWhiteSpace() && Album.IsNullOrWhiteSpace() && Artist.IsNullOrWhiteSpace() && Label.IsNullOrWhiteSpace())
{
return true;
}
return false;
}
}
} }
} }
@@ -7,9 +7,8 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
{ {
public abstract class SearchCriteriaBase public abstract class SearchCriteriaBase
{ {
private static readonly Regex SpecialCharacter = new Regex(@"[`'.]", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex StandardizeDashesRegex = new (@"\p{Pd}+", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex NonWord = new Regex(@"[\W]", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex StandardizeSingleQuotesRegex = new (@"[\u0060\u00B4\u2018\u2019]", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex BeginningThe = new Regex(@"^the\s", RegexOptions.IgnoreCase | RegexOptions.Compiled);
public virtual bool InteractiveSearch { get; set; } public virtual bool InteractiveSearch { get; set; }
public List<int> IndexerIds { get; set; } public List<int> IndexerIds { get; set; }
@@ -21,58 +20,24 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
public string Source { get; set; } public string Source { get; set; }
public string Host { 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 RssSearch => SearchTerm.IsNullOrWhiteSpace();
public string SanitizedSearchTerm => GetSanitizedTerm(SearchTerm);
private static string GetSanitizedTerm(string term)
{ {
get term ??= "";
{
return $"Term: [{SearchTerm}]";
}
}
public override string ToString() term = StandardizeDashesRegex.Replace(term, "-");
{ term = StandardizeSingleQuotesRegex.Replace(term, "'");
return $"{SearchQuery}, Offset: {Offset ?? 0}, Limit: {Limit ?? 0}, Categories: [{string.Join(", ", Categories)}]";
}
public virtual bool RssSearch var safeTitle = term.Where(c => char.IsLetterOrDigit(c) || char.IsWhiteSpace(c) || c is '-' or '.' or '_' or '(' or ')' or '@' or '/' or '\'' or '[' or ']' or '+' or '%');
{
get
{
if (SearchTerm.IsNullOrWhiteSpace())
{
return true;
}
return false; return string.Concat(safeTitle);
}
}
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);
}
} }
} }
} }
@@ -21,23 +21,12 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
public int? Year { get; set; } public int? Year { get; set; }
public string Genre { 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 EpisodeSearchString => GetEpisodeSearchString();
public string FullImdbId => ParseUtil.GetFullImdbId(ImdbId); public string FullImdbId => ParseUtil.GetFullImdbId(ImdbId);
public override bool RssSearch public override bool RssSearch => SearchTerm.IsNullOrWhiteSpace() && ImdbId.IsNullOrWhiteSpace() && !TvdbId.HasValue && !RId.HasValue && !TraktId.HasValue && !TvMazeId.HasValue;
{
get
{
if (SearchTerm.IsNullOrWhiteSpace() && ImdbId.IsNullOrWhiteSpace() && !TvdbId.HasValue && !RId.HasValue && !TraktId.HasValue && !TvMazeId.HasValue)
{
return true;
}
return false;
}
}
public override string SearchQuery public override string SearchQuery
{ {
@@ -93,12 +93,13 @@ namespace NzbDrone.Core.IndexerSearch
r.Languages == null ? null : from c in r.Languages select GetNabElement("language", c, protocol), r.Languages == null ? null : from c in r.Languages select GetNabElement("language", c, protocol),
r.Subs == null ? null : from c in r.Subs select GetNabElement("subs", c, protocol), r.Subs == null ? null : from c in r.Subs select GetNabElement("subs", c, protocol),
r.Genres == null ? null : GetNabElement("genre", string.Join(", ", r.Genres), protocol), r.Genres == null ? null : GetNabElement("genre", string.Join(", ", r.Genres), protocol),
GetNabElement("rageid", r.TvRageId, protocol), r.TvRageId == 0 ? null : GetNabElement("rageid", r.TvRageId, protocol),
GetNabElement("tvdbid", r.TvdbId, protocol), r.TvdbId == 0 ? null : GetNabElement("tvdbid", r.TvdbId, protocol),
GetNabElement("imdb", r.ImdbId.ToString("D7"), protocol), r.ImdbId == 0 ? null : GetNabElement("imdb", r.ImdbId.ToString("D7"), protocol),
GetNabElement("tmdbid", r.TmdbId, protocol), r.TmdbId == 0 ? null : GetNabElement("tmdbid", r.TmdbId, protocol),
GetNabElement("traktid", r.TraktId, protocol), r.TraktId == 0 ? null : GetNabElement("traktid", r.TraktId, protocol),
GetNabElement("doubanid", r.DoubanId, protocol), r.TvMazeId == 0 ? null : GetNabElement("tvmazeid", r.TvMazeId, protocol),
r.DoubanId == 0 ? null : GetNabElement("doubanid", r.DoubanId, protocol),
GetNabElement("seeders", t.Seeders, protocol), GetNabElement("seeders", t.Seeders, protocol),
GetNabElement("files", r.Files, protocol), GetNabElement("files", r.Files, protocol),
GetNabElement("grabs", r.Grabs, protocol), GetNabElement("grabs", r.Grabs, protocol),
@@ -59,7 +59,7 @@ namespace NzbDrone.Core.IndexerStats
var elapsedTimeEvents = sortedEvents.Where(h => int.TryParse(h.Data.GetValueOrDefault("elapsedTime"), out temp)) var elapsedTimeEvents = sortedEvents.Where(h => int.TryParse(h.Data.GetValueOrDefault("elapsedTime"), out temp))
.Select(h => 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) foreach (var historyEvent in sortedEvents)
{ {
@@ -29,13 +29,14 @@ namespace NzbDrone.Core.IndexerVersions
/* Update Service will fall back if version # does not exist for an indexer per Ta */ /* Update Service will fall back if version # does not exist for an indexer per Ta */
private const string DEFINITION_BRANCH = "master"; private const string DEFINITION_BRANCH = "master";
private const int DEFINITION_VERSION = 7; private const int DEFINITION_VERSION = 8;
//Used when moving yml to C# // Used when moving yml to C#
private readonly List<string> _defintionBlocklist = new List<string>() private readonly List<string> _definitionBlocklist = new ()
{ {
"aither", "aither",
"animeworld", "animeworld",
"audiobookbay",
"beyond-hd-oneurl", "beyond-hd-oneurl",
"beyond-hd", "beyond-hd",
"blutopia", "blutopia",
@@ -89,7 +90,7 @@ namespace NzbDrone.Core.IndexerVersions
{ {
var request = new HttpRequest($"https://indexers.prowlarr.com/{DEFINITION_BRANCH}/{DEFINITION_VERSION}"); var request = new HttpRequest($"https://indexers.prowlarr.com/{DEFINITION_BRANCH}/{DEFINITION_VERSION}");
var response = _httpClient.Get<List<CardigannMetaDefinition>>(request); 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 catch
{ {
@@ -125,7 +126,7 @@ namespace NzbDrone.Core.IndexerVersions
public List<string> GetBlocklist() public List<string> GetBlocklist()
{ {
return _defintionBlocklist; return _definitionBlocklist;
} }
private List<CardigannMetaDefinition> ReadDefinitionsFromDisk(List<CardigannMetaDefinition> defs, string path, SearchOption options = SearchOption.TopDirectoryOnly) 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) if (definition.Settings == null)
{ {
definition.Settings = new List<SettingsField> definition.Settings = new List<SettingsField>
{ {
new SettingsField { Name = "username", Label = "Username", Type = "text" }, new () { Name = "username", Label = "Username", Type = "text" },
new SettingsField { Name = "password", Label = "Password", Type = "password" } new () { Name = "password", Label = "Password", Type = "password" }
}; };
} }
if (definition.Encoding == null) if (definition.Encoding == null)
@@ -1,85 +1,120 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized;
using NLog; using NLog;
using NzbDrone.Common.Http; using NzbDrone.Core.Annotations;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers.Definitions.Gazelle;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.Indexers.Definitions namespace NzbDrone.Core.Indexers.Definitions;
public class AlphaRatio : GazelleBase<AlphaRatioSettings>
{ {
public class AlphaRatio : Gazelle.Gazelle public override string Name => "AlphaRatio";
public override string[] IndexerUrls => new[] { "https://alpharatio.cc/" };
public override string Description => "AlphaRatio(AR) is a Private Torrent Tracker for 0DAY / GENERAL";
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public AlphaRatio(IIndexerHttpClient httpClient,
IEventAggregator eventAggregator,
IIndexerStatusService indexerStatusService,
IConfigService configService,
Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{ {
public override string Name => "AlphaRatio";
public override string[] IndexerUrls => new string[] { "https://alpharatio.cc/" };
public override string Description => "AlphaRatio(AR) is a Private Torrent Tracker for 0DAY / GENERAL";
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public AlphaRatio(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}
public override IIndexerRequestGenerator GetRequestGenerator()
{
return new AlphaRatioRequestGenerator()
{
Settings = Settings,
HttpClient = _httpClient,
Logger = _logger,
Capabilities = Capabilities
};
}
protected override IndexerCapabilities SetCapabilities()
{
var caps = new IndexerCapabilities
{
TvSearchParams = new List<TvSearchParam>
{
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
},
MovieSearchParams = new List<MovieSearchParam>
{
MovieSearchParam.Q
}
};
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.TVSD, "TvSD");
caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.TVHD, "TvHD");
caps.Categories.AddCategoryMapping(3, NewznabStandardCategory.TVUHD, "TvUHD");
caps.Categories.AddCategoryMapping(4, NewznabStandardCategory.TVSD, "TvDVDRip");
caps.Categories.AddCategoryMapping(5, NewznabStandardCategory.TVSD, "TvPackSD");
caps.Categories.AddCategoryMapping(6, NewznabStandardCategory.TVHD, "TvPackHD");
caps.Categories.AddCategoryMapping(7, NewznabStandardCategory.TVUHD, "TvPackUHD");
caps.Categories.AddCategoryMapping(8, NewznabStandardCategory.MoviesSD, "MovieSD");
caps.Categories.AddCategoryMapping(9, NewznabStandardCategory.MoviesHD, "MovieHD");
caps.Categories.AddCategoryMapping(10, NewznabStandardCategory.MoviesUHD, "MovieUHD");
caps.Categories.AddCategoryMapping(11, NewznabStandardCategory.MoviesSD, "MoviePackSD");
caps.Categories.AddCategoryMapping(12, NewznabStandardCategory.MoviesHD, "MoviePackHD");
caps.Categories.AddCategoryMapping(13, NewznabStandardCategory.MoviesUHD, "MoviePackUHD");
caps.Categories.AddCategoryMapping(14, NewznabStandardCategory.XXX, "MovieXXX");
caps.Categories.AddCategoryMapping(15, NewznabStandardCategory.MoviesBluRay, "Bluray");
caps.Categories.AddCategoryMapping(16, NewznabStandardCategory.TVAnime, "AnimeSD");
caps.Categories.AddCategoryMapping(17, NewznabStandardCategory.TVAnime, "AnimeHD");
caps.Categories.AddCategoryMapping(18, NewznabStandardCategory.PCGames, "GamesPC");
caps.Categories.AddCategoryMapping(19, NewznabStandardCategory.ConsoleXBox, "GamesxBox");
caps.Categories.AddCategoryMapping(20, NewznabStandardCategory.ConsolePS4, "GamesPS");
caps.Categories.AddCategoryMapping(21, NewznabStandardCategory.ConsoleWii, "GamesNin");
caps.Categories.AddCategoryMapping(22, NewznabStandardCategory.PC0day, "AppsWindows");
caps.Categories.AddCategoryMapping(23, NewznabStandardCategory.PCMac, "AppsMAC");
caps.Categories.AddCategoryMapping(24, NewznabStandardCategory.PC0day, "AppsLinux");
caps.Categories.AddCategoryMapping(25, NewznabStandardCategory.PCMobileOther, "AppsMobile");
caps.Categories.AddCategoryMapping(26, NewznabStandardCategory.XXX, "0dayXXX");
caps.Categories.AddCategoryMapping(27, NewznabStandardCategory.Books, "eBook");
caps.Categories.AddCategoryMapping(28, NewznabStandardCategory.AudioAudiobook, "AudioBook");
caps.Categories.AddCategoryMapping(29, NewznabStandardCategory.AudioOther, "Music");
caps.Categories.AddCategoryMapping(30, NewznabStandardCategory.Other, "Misc");
return caps;
}
} }
public class AlphaRatioRequestGenerator : Gazelle.GazelleRequestGenerator public override IIndexerRequestGenerator GetRequestGenerator()
{ {
protected override bool ImdbInTags => true; return new AlphaRatioRequestGenerator(Settings, Capabilities, _httpClient, _logger);
}
protected override IndexerCapabilities SetCapabilities()
{
var caps = new IndexerCapabilities
{
TvSearchParams = new List<TvSearchParam>
{
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
},
MovieSearchParams = new List<MovieSearchParam>
{
MovieSearchParam.Q
}
};
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.TVSD, "TvSD");
caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.TVHD, "TvHD");
caps.Categories.AddCategoryMapping(3, NewznabStandardCategory.TVUHD, "TvUHD");
caps.Categories.AddCategoryMapping(4, NewznabStandardCategory.TVSD, "TvDVDRip");
caps.Categories.AddCategoryMapping(5, NewznabStandardCategory.TVSD, "TvPackSD");
caps.Categories.AddCategoryMapping(6, NewznabStandardCategory.TVHD, "TvPackHD");
caps.Categories.AddCategoryMapping(7, NewznabStandardCategory.TVUHD, "TvPackUHD");
caps.Categories.AddCategoryMapping(8, NewznabStandardCategory.MoviesSD, "MovieSD");
caps.Categories.AddCategoryMapping(9, NewznabStandardCategory.MoviesHD, "MovieHD");
caps.Categories.AddCategoryMapping(10, NewznabStandardCategory.MoviesUHD, "MovieUHD");
caps.Categories.AddCategoryMapping(11, NewznabStandardCategory.MoviesSD, "MoviePackSD");
caps.Categories.AddCategoryMapping(12, NewznabStandardCategory.MoviesHD, "MoviePackHD");
caps.Categories.AddCategoryMapping(13, NewznabStandardCategory.MoviesUHD, "MoviePackUHD");
caps.Categories.AddCategoryMapping(14, NewznabStandardCategory.XXX, "MovieXXX");
caps.Categories.AddCategoryMapping(15, NewznabStandardCategory.MoviesBluRay, "Bluray");
caps.Categories.AddCategoryMapping(16, NewznabStandardCategory.TVAnime, "AnimeSD");
caps.Categories.AddCategoryMapping(17, NewznabStandardCategory.TVAnime, "AnimeHD");
caps.Categories.AddCategoryMapping(18, NewznabStandardCategory.PCGames, "GamesPC");
caps.Categories.AddCategoryMapping(19, NewznabStandardCategory.ConsoleXBox, "GamesxBox");
caps.Categories.AddCategoryMapping(20, NewznabStandardCategory.ConsolePS4, "GamesPS");
caps.Categories.AddCategoryMapping(21, NewznabStandardCategory.ConsoleWii, "GamesNin");
caps.Categories.AddCategoryMapping(22, NewznabStandardCategory.PC0day, "AppsWindows");
caps.Categories.AddCategoryMapping(23, NewznabStandardCategory.PCMac, "AppsMAC");
caps.Categories.AddCategoryMapping(24, NewznabStandardCategory.PC0day, "AppsLinux");
caps.Categories.AddCategoryMapping(25, NewznabStandardCategory.PCMobileOther, "AppsMobile");
caps.Categories.AddCategoryMapping(26, NewznabStandardCategory.XXX, "0dayXXX");
caps.Categories.AddCategoryMapping(27, NewznabStandardCategory.Books, "eBook");
caps.Categories.AddCategoryMapping(28, NewznabStandardCategory.AudioAudiobook, "AudioBook");
caps.Categories.AddCategoryMapping(29, NewznabStandardCategory.AudioOther, "Music");
caps.Categories.AddCategoryMapping(30, NewznabStandardCategory.Other, "Misc");
return caps;
} }
} }
public class AlphaRatioRequestGenerator : GazelleRequestGenerator
{
protected override bool ImdbInTags => true;
private readonly AlphaRatioSettings _settings;
public AlphaRatioRequestGenerator(AlphaRatioSettings settings,
IndexerCapabilities capabilities,
IIndexerHttpClient httpClient,
Logger logger)
: base(settings, capabilities, httpClient, logger)
{
_settings = settings;
}
protected override NameValueCollection GetBasicSearchParameters(string term, int[] categories)
{
var parameters = base.GetBasicSearchParameters(term, categories);
if (_settings.FreeleechOnly)
{
parameters.Set("freetorrent", "1");
}
if (_settings.ExcludeScene)
{
parameters.Set("scene", "0");
}
return parameters;
}
}
public class AlphaRatioSettings : GazelleSettings
{
[FieldDefinition(6, Label = "Freeleech Only", Type = FieldType.Checkbox, HelpText = "Search freeleech torrents only")]
public bool FreeleechOnly { get; set; }
[FieldDefinition(7, Label = "Exclude Scene", Type = FieldType.Checkbox, HelpText = "Exclude Scene torrents from results")]
public bool ExcludeScene { get; set; }
}
@@ -0,0 +1,365 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Text;
using AngleSharp.Html.Parser;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.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 Anidex : TorrentIndexerBase<AnidexSettings>
{
public override string Name => "Anidex";
public override string[] IndexerUrls => new[] { "https://anidex.info/" };
public override string Description => "Anidex is a Public torrent tracker and indexer, primarily for English fansub groups of anime";
public override string Language => "en-US";
public override Encoding Encoding => Encoding.UTF8;
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
public override IndexerPrivacy Privacy => IndexerPrivacy.Public;
public override IndexerCapabilities Capabilities => SetCapabilities();
public Anidex(IIndexerHttpClient httpClient,
IEventAggregator eventAggregator,
IIndexerStatusService indexerStatusService,
IConfigService configService,
Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}
public override IIndexerRequestGenerator GetRequestGenerator()
{
return new AnidexRequestGenerator(Settings, Capabilities);
}
public override IParseIndexerResponse GetParser()
{
return new AnidexParser(Settings, Capabilities.Categories);
}
private IndexerCapabilities SetCapabilities()
{
var caps = new IndexerCapabilities
{
TvSearchParams = new List<TvSearchParam>
{
TvSearchParam.Q
},
MusicSearchParams = new List<MusicSearchParam>
{
MusicSearchParam.Q,
},
BookSearchParams = new List<BookSearchParam>
{
BookSearchParam.Q,
}
};
caps.Categories.AddCategoryMapping("1", NewznabStandardCategory.TVAnime, "Anime - Sub");
caps.Categories.AddCategoryMapping("2", NewznabStandardCategory.TVAnime, "Anime - Raw");
caps.Categories.AddCategoryMapping("3", NewznabStandardCategory.TVAnime, "Anime - Dub");
caps.Categories.AddCategoryMapping("4", NewznabStandardCategory.TVAnime, "LA - Sub");
caps.Categories.AddCategoryMapping("5", NewznabStandardCategory.TVAnime, "LA - Raw");
caps.Categories.AddCategoryMapping("6", NewznabStandardCategory.BooksEBook, "Light Novel");
caps.Categories.AddCategoryMapping("7", NewznabStandardCategory.BooksComics, "Manga - TLed");
caps.Categories.AddCategoryMapping("8", NewznabStandardCategory.BooksComics, "Manga - Raw");
caps.Categories.AddCategoryMapping("9", NewznabStandardCategory.AudioMP3, "♫ - Lossy");
caps.Categories.AddCategoryMapping("10", NewznabStandardCategory.AudioLossless, "♫ - Lossless");
caps.Categories.AddCategoryMapping("11", NewznabStandardCategory.AudioVideo, "♫ - Video");
caps.Categories.AddCategoryMapping("12", NewznabStandardCategory.PCGames, "Games");
caps.Categories.AddCategoryMapping("13", NewznabStandardCategory.PC0day, "Applications");
caps.Categories.AddCategoryMapping("14", NewznabStandardCategory.XXXImageSet, "Pictures");
caps.Categories.AddCategoryMapping("15", NewznabStandardCategory.XXX, "Adult Video");
caps.Categories.AddCategoryMapping("16", NewznabStandardCategory.Other, "Other");
return caps;
}
}
public class AnidexRequestGenerator : IIndexerRequestGenerator
{
private readonly AnidexSettings _settings;
private readonly IndexerCapabilities _capabilities;
public AnidexRequestGenerator(AnidexSettings settings, IndexerCapabilities capabilities)
{
_settings = settings;
_capabilities = capabilities;
}
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
{
return new IndexerPageableRequestChain();
}
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.SanitizedSearchTerm}", searchCriteria.Categories));
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)
{
var parameters = new NameValueCollection
{
{ "page", "search" },
{ "s", "upload_timestamp" },
{ "o", "desc" },
{ "group_id", "0" }, // All groups
{ "q", term ?? string.Empty }
};
if (_settings.AuthorisedOnly)
{
parameters.Add("a", "1");
}
var searchUrl = $"{_settings.BaseUrl}?{parameters.GetQueryString()}";
var queryCats = _capabilities.Categories.MapTorznabCapsToTrackers(categories);
if (queryCats.Any())
{
searchUrl += "&id=" + string.Join(",", queryCats);
}
if (_settings.LanguagesOnly.Any())
{
searchUrl += "&lang_id=" + string.Join(",", _settings.LanguagesOnly);
}
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 AnidexParser : IParseIndexerResponse
{
private readonly AnidexSettings _settings;
private readonly IndexerCapabilitiesCategories _categories;
public AnidexParser(AnidexSettings settings, IndexerCapabilitiesCategories categories)
{
_settings = settings;
_categories = categories;
}
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
{
if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
{
throw new IndexerException(indexerResponse, $"Anidex search returned unexpected result. Expected 200 OK but got {indexerResponse.HttpResponse.StatusCode}.");
}
var releaseInfos = new List<ReleaseInfo>();
var parser = new HtmlParser();
var dom = parser.ParseDocument(indexerResponse.Content);
var rows = dom.QuerySelectorAll("div#content table > tbody > tr");
foreach (var row in rows)
{
var downloadUrl = _settings.BaseUrl + row.QuerySelector("a[href^=\"/dl/\"]")?.GetAttribute("href");
var infoUrl = _settings.BaseUrl + row.QuerySelector("td:nth-child(3) a")?.GetAttribute("href");
var title = row.QuerySelector("td:nth-child(3) span")?.GetAttribute("title")?.Trim();
var language = row.QuerySelector("td:nth-child(1) img")?.GetAttribute("title")?.Trim();
if (language.IsNotNullOrWhiteSpace())
{
title += $" [{language}]";
}
var categoryLink = row.QuerySelector("td:nth-child(1) a").GetAttribute("href");
var cat = ParseUtil.GetArgumentFromQueryString(categoryLink, "id");
var categories = _categories.MapTrackerCatToNewznab(cat);
var seeders = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(9)")?.TextContent);
var peers = seeders + ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(10)")?.TextContent.Trim());
var added = row.QuerySelector("td:nth-child(8)").GetAttribute("title").Trim();
var release = new TorrentInfo
{
Guid = infoUrl,
InfoUrl = infoUrl,
DownloadUrl = downloadUrl,
MagnetUrl = row.QuerySelector("a[href^=\"magnet:?\"]")?.GetAttribute("href"),
Title = title,
Categories = categories,
Seeders = seeders,
Peers = peers,
Size = ParseUtil.GetBytes(row.QuerySelector("td:nth-child(7)")?.TextContent.Trim()),
Grabs = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(11)")?.TextContent),
PublishDate = DateTime.ParseExact(added, "yyyy-MM-dd HH:mm:ss UTC", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal),
DownloadVolumeFactor = 0,
UploadVolumeFactor = 1
};
releaseInfos.Add(release);
}
return releaseInfos.ToArray();
}
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}
public class AnidexSettings : NoAuthTorrentBaseSettings
{
public AnidexSettings()
{
AuthorisedOnly = false;
LanguagesOnly = Array.Empty<int>();
}
[FieldDefinition(2, Label = "Authorised Only", Type = FieldType.Checkbox, HelpText = "Search authorised torrents only")]
public bool AuthorisedOnly { get; set; }
[FieldDefinition(3, Label = "Languages Only", Type = FieldType.Select, SelectOptions = typeof(AnidexLanguage), HelpText = "Search selected languages only. None ticked = ALL.")]
public IEnumerable<int> LanguagesOnly { get; set; }
}
public enum AnidexLanguage
{
[FieldOption(Hint = "English")]
GB = 1,
[FieldOption(Hint = "Japanese")]
JP = 2,
[FieldOption(Hint = "Polish")]
PL = 3,
[FieldOption(Hint = "Serbo-Croatian")]
RS = 4,
[FieldOption(Hint = "Dutch")]
NL = 5,
[FieldOption(Hint = "Italian")]
IT = 6,
[FieldOption(Hint = "Russian")]
RU = 7,
[FieldOption(Hint = "German")]
DE = 8,
[FieldOption(Hint = "Hungarian")]
HU = 9,
[FieldOption(Hint = "French")]
FR = 10,
[FieldOption(Hint = "Finnish")]
FI = 11,
[FieldOption(Hint = "Vietnamese")]
VN = 12,
[FieldOption(Hint = "Greek")]
GR = 13,
[FieldOption(Hint = "Bulgarian")]
BG = 14,
[FieldOption(Hint = "Spanish (Spain)")]
ES = 15,
[FieldOption(Hint = "Portuguese (Brazil)")]
BR = 16,
[FieldOption(Hint = "Portuguese (Portugal)")]
PT = 17,
[FieldOption(Hint = "Swedish")]
SE = 18,
[FieldOption(Hint = "Arabic")]
SA = 19,
[FieldOption(Hint = "Danish")]
DK = 20,
[FieldOption(Hint = "Chinese (Simplified)")]
CN = 21,
[FieldOption(Hint = "Bengali")]
BD = 22,
[FieldOption(Hint = "Romanian")]
RO = 23,
[FieldOption(Hint = "Czech")]
CZ = 24,
[FieldOption(Hint = "Mongolian")]
MN = 25,
[FieldOption(Hint = "Turkish")]
TR = 26,
[FieldOption(Hint = "Indonesian")]
ID = 27,
[FieldOption(Hint = "Korean")]
KR = 28,
[FieldOption(Hint = "Spanish (LATAM)")]
MX = 29,
[FieldOption(Hint = "Persian")]
IR = 30,
[FieldOption(Hint = "Malaysian")]
MY = 31,
}
}
@@ -9,10 +9,8 @@ using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using AngleSharp.Html.Parser; using AngleSharp.Html.Parser;
using FluentValidation;
using NLog; using NLog;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers.Exceptions; using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.Indexers.Settings; using NzbDrone.Core.Indexers.Settings;
@@ -20,14 +18,13 @@ using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser; using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Definitions namespace NzbDrone.Core.Indexers.Definitions
{ {
public class Anidub : TorrentIndexerBase<UserPassTorrentBaseSettings> public class Anidub : TorrentIndexerBase<UserPassTorrentBaseSettings>
{ {
public override string Name => "Anidub"; 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 Description => "Anidub is russian anime voiceover group and eponymous anime tracker.";
public override string Language => "ru-RU"; public override string Language => "ru-RU";
public override Encoding Encoding => Encoding.UTF8; public override Encoding Encoding => Encoding.UTF8;
@@ -42,31 +39,29 @@ namespace NzbDrone.Core.Indexers.Definitions
public override IIndexerRequestGenerator GetRequestGenerator() public override IIndexerRequestGenerator GetRequestGenerator()
{ {
return new AnidubRequestGenerator() { Settings = Settings, Capabilities = Capabilities }; return new AnidubRequestGenerator(Settings);
} }
public override IParseIndexerResponse GetParser() 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() protected override async Task DoLogin()
{ {
UpdateCookies(null, null); UpdateCookies(null, null);
var mainPage = await ExecuteAuth(new HttpRequest(Settings.BaseUrl));
var requestBuilder = new HttpRequestBuilder(Settings.BaseUrl + "index.php") var requestBuilder = new HttpRequestBuilder(Settings.BaseUrl + "index.php")
{ {
LogResponseContent = true, 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 var authLoginRequest = requestBuilder
.SetCookies(mainPage.GetCookies())
.AddFormParameter("login_name", Settings.Username) .AddFormParameter("login_name", Settings.Username)
.AddFormParameter("login_password", Settings.Password) .AddFormParameter("login_password", Settings.Password)
.AddFormParameter("login", "submit") .AddFormParameter("login", "submit")
@@ -77,27 +72,22 @@ namespace NzbDrone.Core.Indexers.Definitions
if (response.Content != null && !CheckIfLoginNeeded(response)) 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"); _logger.Debug("Anidub authentication succeeded");
} }
else else
{ {
const string ErrorSelector = "#content .berror .berror_c";
var parser = new HtmlParser(); var parser = new HtmlParser();
var document = await parser.ParseDocumentAsync(response.Content); var document = await parser.ParseDocumentAsync(response.Content);
var errorMessage = document.QuerySelector(ErrorSelector).TextContent.Trim(); var errorMessage = document.QuerySelector("#content .berror .berror_c")?.TextContent.Trim();
throw new IndexerAuthException("Anidub authentication failed. Error: " + errorMessage);
throw new IndexerAuthException(errorMessage ?? "Unknown error message, please report.");
} }
} }
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse) protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
{ {
if (httpResponse.Content.Contains("index.php?action=logout")) return !httpResponse.Content.Contains("index.php?action=logout");
{
return false;
}
return true;
} }
private IndexerCapabilities SetCapabilities() private IndexerCapabilities SetCapabilities()
@@ -138,31 +128,32 @@ namespace NzbDrone.Core.Indexers.Definitions
caps.Categories.AddCategoryMapping(15, NewznabStandardCategory.BooksComics, "Манга"); caps.Categories.AddCategoryMapping(15, NewznabStandardCategory.BooksComics, "Манга");
caps.Categories.AddCategoryMapping(16, NewznabStandardCategory.Audio, "OST"); caps.Categories.AddCategoryMapping(16, NewznabStandardCategory.Audio, "OST");
caps.Categories.AddCategoryMapping(17, NewznabStandardCategory.Audio, "Подкасты"); caps.Categories.AddCategoryMapping(17, NewznabStandardCategory.Audio, "Подкасты");
return caps; return caps;
} }
} }
public class AnidubRequestGenerator : IIndexerRequestGenerator public class AnidubRequestGenerator : IIndexerRequestGenerator
{ {
public UserPassTorrentBaseSettings Settings { get; set; } private readonly UserPassTorrentBaseSettings _settings;
public IndexerCapabilities Capabilities { get; set; }
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); var isSearch = !string.IsNullOrWhiteSpace(term);
if (isSearch) if (isSearch)
{ {
requestUrl = string.Format("{0}/index.php?do=search", Settings.BaseUrl.TrimEnd('/')); requestUrl = $"{_settings.BaseUrl.TrimEnd('/')}/index.php?do=search";
} }
else else
{ {
requestUrl = Settings.BaseUrl; requestUrl = _settings.BaseUrl;
} }
var request = new IndexerRequest(requestUrl, HttpAccept.Html); var request = new IndexerRequest(requestUrl, HttpAccept.Html);
@@ -207,7 +198,7 @@ namespace NzbDrone.Core.Indexers.Definitions
{ {
var pageableRequests = new IndexerPageableRequestChain(); var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories)); pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}"));
return pageableRequests; return pageableRequests;
} }
@@ -216,7 +207,7 @@ namespace NzbDrone.Core.Indexers.Definitions
{ {
var pageableRequests = new IndexerPageableRequestChain(); var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedTvSearchString), searchCriteria.Categories)); pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedTvSearchString}"));
return pageableRequests; return pageableRequests;
} }
@@ -225,7 +216,7 @@ namespace NzbDrone.Core.Indexers.Definitions
{ {
var pageableRequests = new IndexerPageableRequestChain(); var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories)); pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}"));
return pageableRequests; return pageableRequests;
} }
@@ -234,7 +225,7 @@ namespace NzbDrone.Core.Indexers.Definitions
{ {
var pageableRequests = new IndexerPageableRequestChain(); var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories)); pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}"));
return pageableRequests; return pageableRequests;
} }
@@ -243,7 +234,7 @@ namespace NzbDrone.Core.Indexers.Definitions
{ {
var pageableRequests = new IndexerPageableRequestChain(); var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories)); pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}"));
return pageableRequests; return pageableRequests;
} }
@@ -256,33 +247,37 @@ namespace NzbDrone.Core.Indexers.Definitions
{ {
private readonly UserPassTorrentBaseSettings _settings; private readonly UserPassTorrentBaseSettings _settings;
private readonly IndexerCapabilitiesCategories _categories; private readonly IndexerCapabilitiesCategories _categories;
public IIndexerHttpClient HttpClient { get; set; } private readonly TimeSpan _rateLimit;
public Logger Logger { get; set; } private readonly IIndexerHttpClient _httpClient;
private readonly Logger _logger;
private static Dictionary<string, string> CategoriesMap => new Dictionary<string, string> private static Dictionary<string, string> CategoriesMap => new ()
{ {
{ "/anime_tv/full", "14" }, { "/anime_tv/full", "14" },
{ "/anime_tv/anime_ongoing", "10" }, { "/anime_tv/anime_ongoing", "10" },
{ "/anime_tv/shonen", "11" }, { "/anime_tv/shonen", "11" },
{ "/anime_tv", "2" }, { "/anime_tv", "2" },
{ "/xxx", "13" }, { "/xxx", "13" },
{ "/manga", "15" }, { "/manga", "15" },
{ "/ost", "16" }, { "/ost", "16" },
{ "/podcast", "17" }, { "/podcast", "17" },
{ "/anime_movie", "3" }, { "/anime_movie", "3" },
{ "/anime_ova/anime_ona", "5" }, { "/anime_ova/anime_ona", "5" },
{ "/anime_ova", "4" }, { "/anime_ova", "4" },
{ "/dorama/japan_dorama", "6" }, { "/dorama/japan_dorama", "6" },
{ "/dorama/korea_dorama", "7" }, { "/dorama/korea_dorama", "7" },
{ "/dorama/china_dorama", "8" }, { "/dorama/china_dorama", "8" },
{ "/dorama", "9" }, { "/dorama", "9" },
{ "/anons_ongoing", "12" } { "/anons_ongoing", "12" }
}; };
public AnidubParser(UserPassTorrentBaseSettings settings, IndexerCapabilitiesCategories categories) public AnidubParser(UserPassTorrentBaseSettings settings, IndexerCapabilitiesCategories categories, TimeSpan rateLimit, IIndexerHttpClient httpClient, Logger logger)
{ {
_settings = settings; _settings = settings;
_categories = categories; _categories = categories;
_rateLimit = rateLimit;
_httpClient = httpClient;
_logger = logger;
} }
private static string GetTitle(AngleSharp.Html.Dom.IHtmlDocument content, AngleSharp.Dom.IElement tabNode) 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) 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); int.TryParse(leechersStr, out var leechers);
return leechers; return leechers;
} }
@@ -345,18 +340,18 @@ namespace NzbDrone.Core.Indexers.Definitions
private static int GetReleaseGrabs(AngleSharp.Dom.IElement tabNode) 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); int.TryParse(grabsStr, out var grabs);
return grabs; return grabs;
} }
private static string GetDateFromDocument(AngleSharp.Html.Dom.IHtmlDocument content) 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") if (domDate?.NodeName != "#text")
{ {
@@ -397,16 +392,16 @@ namespace NzbDrone.Core.Indexers.Definitions
return utcDate.AddHours(-russianStandardTimeDiff); 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; return DateTime.UtcNow;
} }
private static long GetReleaseSize(AngleSharp.Dom.IElement tabNode) 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); return ParseUtil.GetBytes(sizeStr);
} }
@@ -446,11 +441,11 @@ namespace NzbDrone.Core.Indexers.Definitions
var release = new TorrentInfo var release = new TorrentInfo
{ {
Title = GetTitle(dom, t), Title = GetTitle(dom, t),
InfoUrl = indexerResponse.Request.Url.ToString(), InfoUrl = indexerResponse.Request.Url.FullUri,
DownloadVolumeFactor = 0, DownloadVolumeFactor = 0,
UploadVolumeFactor = 1, UploadVolumeFactor = 1,
Guid = indexerResponse.Request.Url.ToString() + t.Id, Guid = indexerResponse.Request.Url.FullUri + t.Id,
Seeders = GetReleaseSeeders(t), Seeders = GetReleaseSeeders(t),
Peers = GetReleaseSeeders(t) + GetReleaseLeechers(t), Peers = GetReleaseSeeders(t) + GetReleaseLeechers(t),
Grabs = GetReleaseGrabs(t), Grabs = GetReleaseGrabs(t),
@@ -472,36 +467,30 @@ namespace NzbDrone.Core.Indexers.Definitions
var parser = new HtmlParser(); var parser = new HtmlParser();
var dom = parser.ParseDocument(indexerResponse.Content); var dom = parser.ParseDocument(indexerResponse.Content);
var domQuery = string.Empty;
if (indexerResponse.Request.Url.Query.Contains("do=search")) var links = dom.QuerySelectorAll(".searchitem > h3 > a[href], #dle-content > .story > .story_h > .lcol > h2 > a[href]");
{
domQuery = ".searchitem > h3 > a";
}
else
{
domQuery = "#dle-content > .story > .story_h > .lcol > h2 > a";
}
var links = dom.QuerySelectorAll(domQuery);
foreach (var link in links) foreach (var link in links)
{ {
var url = link.GetAttribute("href"); var url = link.GetAttribute("href");
var releaseRequest = new IndexerRequest(url, HttpAccept.Html); var releaseRequest = new HttpRequestBuilder(url)
var releaseResponse = new IndexerResponse(releaseRequest, HttpClient.Execute(releaseRequest.HttpRequest)); .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 // Throw common http errors here before we try to parse
if (releaseResponse.HttpResponse.HasHttpError) if (releaseResponse.HttpResponse.HasHttpError)
{ {
if (releaseResponse.HttpResponse.StatusCode == HttpStatusCode.TooManyRequests) if (releaseResponse.HttpResponse.StatusCode == HttpStatusCode.TooManyRequests)
{ {
throw new TooManyRequestsException(releaseRequest.HttpRequest, releaseResponse.HttpResponse); throw new TooManyRequestsException(releaseResponse.HttpRequest, releaseResponse.HttpResponse);
}
else
{
throw new IndexerException(releaseResponse, "Http error code: " + releaseResponse.HttpResponse.StatusCode);
} }
throw new IndexerException(releaseResponse, $"HTTP Error - {releaseResponse.HttpResponse.StatusCode}. {url}");
} }
torrentInfos.AddRange(ParseRelease(releaseResponse)); torrentInfos.AddRange(ParseRelease(releaseResponse));
@@ -425,7 +425,7 @@ namespace NzbDrone.Core.Indexers.Definitions
} }
var releaseGroup = releaseTags.LastOrDefault(); var releaseGroup = releaseTags.LastOrDefault();
if (releaseGroup != null && releaseGroup.Contains("(") && releaseGroup.Contains(")")) if (releaseGroup != null && releaseGroup.Contains('(') && releaseGroup.Contains(')'))
{ {
//// Skip raws if set //// Skip raws if set
//if (releaseGroup.ToLowerInvariant().StartsWith("raw") && !AllowRaws) //if (releaseGroup.ToLowerInvariant().StartsWith("raw") && !AllowRaws)
@@ -521,7 +521,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; } public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
} }
public class AnimeBytesSettingsValidator : AbstractValidator<AnimeBytesSettings> public class AnimeBytesSettingsValidator : NoAuthSettingsValidator<AnimeBytesSettings>
{ {
public AnimeBytesSettingsValidator() public AnimeBytesSettingsValidator()
{ {
@@ -535,7 +535,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public class AnimeBytesSettings : NoAuthTorrentBaseSettings public class AnimeBytesSettings : NoAuthTorrentBaseSettings
{ {
private static readonly AnimeBytesSettingsValidator Validator = new AnimeBytesSettingsValidator(); private static readonly AnimeBytesSettingsValidator Validator = new ();
public AnimeBytesSettings() public AnimeBytesSettings()
{ {
@@ -7,10 +7,8 @@ using System.Net.Http;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using AngleSharp.Html.Parser; using AngleSharp.Html.Parser;
using FluentValidation;
using NLog; using NLog;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers.Exceptions; using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.Indexers.Settings; using NzbDrone.Core.Indexers.Settings;
@@ -18,7 +16,6 @@ using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser; using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Definitions namespace NzbDrone.Core.Indexers.Definitions
{ {
@@ -26,7 +23,7 @@ namespace NzbDrone.Core.Indexers.Definitions
{ {
public override string Name => "AnimeTorrents"; 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"; public override string Description => "Definitive source for anime and manga";
private string LoginUrl => Settings.BaseUrl + "login.php"; private string LoginUrl => Settings.BaseUrl + "login.php";
public override DownloadProtocol Protocol => DownloadProtocol.Torrent; public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
@@ -40,7 +37,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public override IIndexerRequestGenerator GetRequestGenerator() public override IIndexerRequestGenerator GetRequestGenerator()
{ {
return new AnimeTorrentsRequestGenerator() { Settings = Settings, Capabilities = Capabilities }; return new AnimeTorrentsRequestGenerator { Settings = Settings, Capabilities = Capabilities };
} }
public override IParseIndexerResponse GetParser() public override IParseIndexerResponse GetParser()
@@ -52,30 +49,29 @@ namespace NzbDrone.Core.Indexers.Definitions
{ {
UpdateCookies(null, null); UpdateCookies(null, null);
var loginPage = await ExecuteAuth(new HttpRequest(LoginUrl));
var requestBuilder = new HttpRequestBuilder(LoginUrl) var requestBuilder = new HttpRequestBuilder(LoginUrl)
{ {
LogResponseContent = true, 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 var authLoginRequest = requestBuilder
.SetCookies(loginPage.GetCookies())
.AddFormParameter("username", Settings.Username) .AddFormParameter("username", Settings.Username)
.AddFormParameter("password", Settings.Password) .AddFormParameter("password", Settings.Password)
.AddFormParameter("form", "login") .AddFormParameter("form", "login")
.AddFormParameter("rememberme[]", "1") .AddFormParameter("rememberme[]", "1")
.SetHeader("Content-Type", "multipart/form-data") .SetHeader("Content-Type", "application/x-www-form-urlencoded")
.Build(); .Build();
var response = await ExecuteAuth(authLoginRequest); var response = await ExecuteAuth(authLoginRequest);
if (response.Content != null && response.Content.Contains("logout.php")) 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"); _logger.Debug("AnimeTorrents authentication succeeded");
} }
@@ -87,12 +83,7 @@ namespace NzbDrone.Core.Indexers.Definitions
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse) protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
{ {
if (httpResponse.Content.Contains("Access Denied!") || httpResponse.Content.Contains("login.php")) return httpResponse.Content.Contains("Access Denied!") || httpResponse.Content.Contains("login.php");
{
return true;
}
return false;
} }
private IndexerCapabilities SetCapabilities() private IndexerCapabilities SetCapabilities()
@@ -100,13 +91,13 @@ namespace NzbDrone.Core.Indexers.Definitions
var caps = new IndexerCapabilities var caps = new IndexerCapabilities
{ {
TvSearchParams = new List<TvSearchParam> TvSearchParams = new List<TvSearchParam>
{ {
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
}, },
MovieSearchParams = new List<MovieSearchParam> MovieSearchParams = new List<MovieSearchParam>
{ {
MovieSearchParam.Q MovieSearchParam.Q
} }
}; };
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.MoviesSD, "Anime Movie"); caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.MoviesSD, "Anime Movie");
@@ -138,10 +129,6 @@ namespace NzbDrone.Core.Indexers.Definitions
public UserPassTorrentBaseSettings Settings { get; set; } public UserPassTorrentBaseSettings Settings { get; set; }
public IndexerCapabilities Capabilities { get; set; } public IndexerCapabilities Capabilities { get; set; }
public AnimeTorrentsRequestGenerator()
{
}
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories) private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories)
{ {
var searchString = term; var searchString = term;
@@ -5,10 +5,8 @@ using System.Net;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using AngleSharp.Html.Parser; using AngleSharp.Html.Parser;
using FluentValidation;
using NLog; using NLog;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers.Exceptions; using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.Indexers.Settings; using NzbDrone.Core.Indexers.Settings;
@@ -16,14 +14,13 @@ using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser; using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Definitions namespace NzbDrone.Core.Indexers.Definitions
{ {
public class Animedia : TorrentIndexerBase<NoAuthTorrentBaseSettings> public class Animedia : TorrentIndexerBase<NoAuthTorrentBaseSettings>
{ {
public override string Name => "Animedia"; 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 Description => "Animedia is russian anime voiceover group and eponymous anime tracker.";
public override string Language => "ru-RU"; public override string Language => "ru-RU";
public override Encoding Encoding => Encoding.UTF8; public override Encoding Encoding => Encoding.UTF8;
@@ -38,12 +35,12 @@ namespace NzbDrone.Core.Indexers.Definitions
public override IIndexerRequestGenerator GetRequestGenerator() public override IIndexerRequestGenerator GetRequestGenerator()
{ {
return new AnimediaRequestGenerator() { Settings = Settings, Capabilities = Capabilities }; return new AnimediaRequestGenerator(Settings);
} }
public override IParseIndexerResponse GetParser() 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() private IndexerCapabilities SetCapabilities()
@@ -51,38 +48,40 @@ namespace NzbDrone.Core.Indexers.Definitions
var caps = new IndexerCapabilities var caps = new IndexerCapabilities
{ {
TvSearchParams = new List<TvSearchParam> TvSearchParams = new List<TvSearchParam>
{ {
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
}, },
MovieSearchParams = new List<MovieSearchParam> MovieSearchParams = new List<MovieSearchParam>
{ {
MovieSearchParam.Q MovieSearchParam.Q
} }
}; };
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.TVAnime, "TV Anime"); caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.TVAnime, "TV Anime");
caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.TVAnime, "OVA/ONA/Special"); caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.TVAnime, "OVA/ONA/Special");
caps.Categories.AddCategoryMapping(3, NewznabStandardCategory.TV, "Dorama"); caps.Categories.AddCategoryMapping(3, NewznabStandardCategory.TV, "Dorama");
caps.Categories.AddCategoryMapping(4, NewznabStandardCategory.Movies, "Movies"); caps.Categories.AddCategoryMapping(4, NewznabStandardCategory.Movies, "Movies");
return caps; return caps;
} }
} }
public class AnimediaRequestGenerator : IIndexerRequestGenerator public class AnimediaRequestGenerator : IIndexerRequestGenerator
{ {
public NoAuthTorrentBaseSettings Settings { get; set; } private readonly NoAuthTorrentBaseSettings _settings;
public IndexerCapabilities Capabilities { get; set; }
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)) if (string.IsNullOrWhiteSpace(term))
{ {
requestUrl = Settings.BaseUrl; requestUrl = _settings.BaseUrl;
} }
else else
{ {
@@ -94,18 +93,17 @@ namespace NzbDrone.Core.Indexers.Definitions
{ "orderby_sort", "entry_date|desc" } { "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 new IndexerRequest(requestUrl, HttpAccept.Html);
yield return request;
} }
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
{ {
var pageableRequests = new IndexerPageableRequestChain(); var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories)); pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}"));
return pageableRequests; return pageableRequests;
} }
@@ -114,7 +112,7 @@ namespace NzbDrone.Core.Indexers.Definitions
{ {
var pageableRequests = new IndexerPageableRequestChain(); var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedTvSearchString), searchCriteria.Categories)); pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedTvSearchString}"));
return pageableRequests; return pageableRequests;
} }
@@ -123,7 +121,7 @@ namespace NzbDrone.Core.Indexers.Definitions
{ {
var pageableRequests = new IndexerPageableRequestChain(); var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories)); pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}"));
return pageableRequests; return pageableRequests;
} }
@@ -148,6 +146,9 @@ namespace NzbDrone.Core.Indexers.Definitions
{ {
private readonly NoAuthTorrentBaseSettings _settings; private readonly NoAuthTorrentBaseSettings _settings;
private readonly IndexerCapabilitiesCategories _categories; 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 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 ResolutionInfoQueryRegex = new Regex(@"качество (\d+)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex SizeInfoQueryRegex = new Regex(@"размер:(.*)\n", 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 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 CategorieOVARegex = new Regex(@"ОВА|OVA|ОНА|ONA|Special", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex CategorieDoramaRegex = new Regex(@"Дорама", 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; _settings = settings;
_categories = categories; _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 nameRu = dom.QuerySelector("div.media__post__header > h1")?.TextContent.Trim() ?? string.Empty;
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 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 name_orig = dom.QuerySelector("div.media__panel > div:nth-of-type(1) > div.col-l:nth-of-type(2) > div > span").TextContent.Trim(); 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; var title = nameRu + " / " + nameEn;
if (name_en != name_orig) if (nameEn != nameOrig)
{ {
title += " / " + name_orig; title += " / " + nameOrig;
} }
var tabName = t.TextContent; var tabName = t.TextContent;
@@ -183,7 +184,7 @@ namespace NzbDrone.Core.Indexers.Definitions
tabName = ""; 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 // Parse episodes info from heading if episods info present
var match = EpisodesInfoQueryRegex.Match(heading); var match = EpisodesInfoQueryRegex.Match(heading);
@@ -192,40 +193,40 @@ namespace NzbDrone.Core.Indexers.Definitions
{ {
if (string.IsNullOrEmpty(match.Groups[2].Value)) if (string.IsNullOrEmpty(match.Groups[2].Value))
{ {
heading += " E" + match.Groups[1].Value; heading += $" E{match.Groups[1].Value}";
} }
else 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; 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()); 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()); 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) private ICollection<IndexerCategory> MapCategories(AngleSharp.Html.Dom.IHtmlDocument dom, AngleSharp.Dom.IElement t, AngleSharp.Dom.IElement tr)
{ {
var rName = t.TextContent; var rName = t.TextContent;
var rDesc = tr.QuerySelector("h3.tracker_info_bold").TextContent; var rDesc = tr.QuerySelector("h3.tracker_info_bold")?.TextContent.Trim() ?? string.Empty;
var type = dom.QuerySelector("div.releases-date:contains('Тип:')").TextContent; 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 // Check OVA first cause OVA looks like anime with OVA in release name or description
if (CategorieOVARegex.IsMatch(rName) || CategorieOVARegex.IsMatch(rDesc)) 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")) foreach (var t in dom.QuerySelectorAll("ul.media__tabs__nav > li > a"))
{ {
var tr_id = t.Attributes["href"].Value; var trId = t.GetAttribute("href");
var tr = dom.QuerySelector("div" + tr_id); var tr = dom.QuerySelector("div" + trId);
var seeders = int.Parse(tr.QuerySelector("div.circle_green_text_top").TextContent); 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 var release = new TorrentInfo
{ {
Title = composeTitle(dom, t, tr), Title = ComposeTitle(dom, t, tr),
InfoUrl = url, InfoUrl = url,
DownloadVolumeFactor = 0, DownloadVolumeFactor = 0,
UploadVolumeFactor = 1, UploadVolumeFactor = 1,
Guid = url + tr_id, Guid = url + trId,
Seeders = seeders, Seeders = seeders,
Peers = seeders + int.Parse(tr.QuerySelector("div.circle_red_text_top").TextContent), Peers = seeders + int.Parse(tr.QuerySelector("div.circle_red_text_top").TextContent),
Grabs = int.Parse(tr.QuerySelector("div.circle_grey_text_top").TextContent), Grabs = int.Parse(tr.QuerySelector("div.circle_grey_text_top").TextContent),
Categories = MapCategories(dom, t, tr), Categories = MapCategories(dom, t, tr),
PublishDate = getReleaseDate(tr), PublishDate = GetReleaseDate(tr),
DownloadUrl = tr.QuerySelector("div.download_tracker > a.btn__green").Attributes["href"].Value, DownloadUrl = tr.QuerySelector("div.download_tracker > a.btn__green").GetAttribute("href"),
MagnetUrl = tr.QuerySelector("div.download_tracker > a.btn__d-gray").Attributes["href"].Value, MagnetUrl = tr.QuerySelector("div.download_tracker > a.btn__d-gray").GetAttribute("href"),
Size = getReleaseSize(tr), Size = GetReleaseSize(tr),
Resolution = getResolution(tr) Resolution = GetResolution(tr)
}; };
torrentInfos.Add(release); torrentInfos.Add(release);
} }
@@ -291,6 +292,7 @@ namespace NzbDrone.Core.Indexers.Definitions
var parser = new HtmlParser(); var parser = new HtmlParser();
var dom = parser.ParseDocument(indexerResponse.Content); var dom = parser.ParseDocument(indexerResponse.Content);
var links = dom.QuerySelectorAll("a.ads-list__item__title"); var links = dom.QuerySelectorAll("a.ads-list__item__title");
foreach (var link in links) foreach (var link in links)
{ {
@@ -302,20 +304,24 @@ namespace NzbDrone.Core.Indexers.Definitions
url = "https:" + url; url = "https:" + url;
} }
var releaseRequest = new IndexerRequest(url, HttpAccept.Html); var releaseRequest = new HttpRequestBuilder(url)
var releaseResponse = new IndexerResponse(releaseRequest, HttpClient.Execute(releaseRequest.HttpRequest)); .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 // Throw common http errors here before we try to parse
if (releaseResponse.HttpResponse.HasHttpError) if (releaseResponse.HttpResponse.HasHttpError)
{ {
if (releaseResponse.HttpResponse.StatusCode == HttpStatusCode.TooManyRequests) if (releaseResponse.HttpResponse.StatusCode == HttpStatusCode.TooManyRequests)
{ {
throw new TooManyRequestsException(releaseRequest.HttpRequest, releaseResponse.HttpResponse); throw new TooManyRequestsException(releaseResponse.HttpRequest, releaseResponse.HttpResponse);
}
else
{
throw new IndexerException(releaseResponse, "Http error code: " + releaseResponse.HttpResponse.StatusCode);
} }
throw new IndexerException(releaseResponse, $"HTTP Error - {releaseResponse.HttpResponse.StatusCode}. {url}");
} }
torrentInfos.AddRange(ParseRelease(releaseResponse)); torrentInfos.AddRange(ParseRelease(releaseResponse));
@@ -52,55 +52,42 @@ namespace NzbDrone.Core.Indexers.Definitions
var requestBuilder = new HttpRequestBuilder(LoginUrl) var requestBuilder = new HttpRequestBuilder(LoginUrl)
{ {
LogResponseContent = true, LogResponseContent = true,
AllowAutoRedirect = true AllowAutoRedirect = true,
Method = HttpMethod.Post
}; };
requestBuilder.Method = HttpMethod.Post;
requestBuilder.PostProcess += r => r.RequestTimeout = TimeSpan.FromSeconds(15);
var cookies = Cookies; var cookies = Cookies;
Cookies = null; Cookies = null;
var authLoginRequest = requestBuilder var authLoginRequest = requestBuilder
.AddFormParameter("username", Settings.Username) .AddFormParameter("username", Settings.Username)
.AddFormParameter("password", Settings.Password) .AddFormParameter("password", Settings.Password)
.AddFormParameter("keeplogged", "1") .AddFormParameter("keeplogged", "1")
.AddFormParameter("login", "Log+In!") .AddFormParameter("login", "Log+In!")
.SetHeader("Content-Type", "multipart/form-data") .SetHeader("Content-Type", "application/x-www-form-urlencoded")
.SetHeader("Referer", LoginUrl)
.Build(); .Build();
var headers = new NameValueCollection
{
{ "Referer", LoginUrl }
};
authLoginRequest.Headers.Add(headers);
var response = await ExecuteAuth(authLoginRequest); var response = await ExecuteAuth(authLoginRequest);
if (CheckIfLoginNeeded(response)) if (CheckIfLoginNeeded(response))
{ {
var parser = new HtmlParser(); var parser = new HtmlParser();
var dom = parser.ParseDocument(response.Content); 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(); cookies = response.GetCookies();
UpdateCookies(cookies, DateTime.Now + TimeSpan.FromDays(30)); UpdateCookies(cookies, DateTime.Now.AddDays(30));
_logger.Debug("Anthelion authentication succeeded."); _logger.Debug("Anthelion authentication succeeded.");
} }
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse) protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
{ {
if (!httpResponse.Content.Contains("logout.php")) return !httpResponse.Content.Contains("logout.php");
{
return true;
}
return false;
} }
private IndexerCapabilities SetCapabilities() private IndexerCapabilities SetCapabilities()
@@ -108,13 +95,13 @@ namespace NzbDrone.Core.Indexers.Definitions
var caps = new IndexerCapabilities var caps = new IndexerCapabilities
{ {
TvSearchParams = new List<TvSearchParam> TvSearchParams = new List<TvSearchParam>
{ {
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
}, },
MovieSearchParams = new List<MovieSearchParam> MovieSearchParams = new List<MovieSearchParam>
{ {
MovieSearchParam.Q MovieSearchParam.Q
} }
}; };
caps.Categories.AddCategoryMapping("1", NewznabStandardCategory.Movies, "Film/Feature"); caps.Categories.AddCategoryMapping("1", NewznabStandardCategory.Movies, "Film/Feature");
@@ -131,10 +118,6 @@ namespace NzbDrone.Core.Indexers.Definitions
public UserPassTorrentBaseSettings Settings { get; set; } public UserPassTorrentBaseSettings Settings { get; set; }
public IndexerCapabilities Capabilities { get; set; } public IndexerCapabilities Capabilities { get; set; }
public AnthelionRequestGenerator()
{
}
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories, string imdbId = null) private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories, string imdbId = null)
{ {
var searchUrl = string.Format("{0}/torrents.php", Settings.BaseUrl.TrimEnd('/')); var searchUrl = string.Format("{0}/torrents.php", Settings.BaseUrl.TrimEnd('/'));
@@ -0,0 +1,81 @@
using System.Collections.Generic;
using AngleSharp.Html.Parser;
using NLog;
using NzbDrone.Common.Http;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers.Definitions.Gazelle;
using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.Indexers.Definitions;
public class AroLol : GazelleBase<AroLolSettings>
{
public override string Name => "aro.lol";
public override string[] IndexerUrls => new[] { "https://aro.lol/" };
public override string Description => "aro.lol is a SERBIAN/ENGLISH Private Torrent Tracker for ANIME";
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public AroLol(IIndexerHttpClient httpClient,
IEventAggregator eventAggregator,
IIndexerStatusService indexerStatusService,
IConfigService configService,
Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}
protected override HttpRequestBuilder AuthLoginRequestBuilder()
{
return base.AuthLoginRequestBuilder()
.AddFormParameter("twofa", Settings.TwoFactorAuthCode?.Trim() ?? "");
}
protected override bool CheckForLoginError(HttpResponse response)
{
if (response.Content.Contains("loginform"))
{
var parser = new HtmlParser();
var dom = parser.ParseDocument(response.Content);
var errorMessage = dom.QuerySelector("#loginform > .warning")?.TextContent.Trim();
throw new IndexerAuthException(errorMessage ?? "Unknown error message, please report.");
}
return true;
}
protected override IndexerCapabilities SetCapabilities()
{
var caps = new IndexerCapabilities
{
TvSearchParams = new List<TvSearchParam>
{
TvSearchParam.Q
},
MovieSearchParams = new List<MovieSearchParam>
{
MovieSearchParam.Q
},
BookSearchParams = new List<BookSearchParam>
{
BookSearchParam.Q
}
};
caps.Categories.AddCategoryMapping("1", NewznabStandardCategory.Movies, "Movies");
caps.Categories.AddCategoryMapping("2", NewznabStandardCategory.TVAnime, "Anime");
caps.Categories.AddCategoryMapping("3", NewznabStandardCategory.Books, "Manga");
caps.Categories.AddCategoryMapping("4", NewznabStandardCategory.Console, "Games");
caps.Categories.AddCategoryMapping("5", NewznabStandardCategory.Other, "Other");
return caps;
}
}
public class AroLolSettings : GazelleSettings
{
[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 aro.lol Web Site. Otherwise just leave it empty.")]
public string TwoFactorAuthCode { get; set; }
}

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