Compare commits

...

84 Commits

Author SHA1 Message Date
dependabot[bot]
ca0de18413 Bump moment from 2.29.1 to 2.29.2
Bumps [moment](https://github.com/moment/moment) from 2.29.1 to 2.29.2.
- [Release notes](https://github.com/moment/moment/releases)
- [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/moment/moment/compare/2.29.1...2.29.2)

---
updated-dependencies:
- dependency-name: moment
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-10 11:34:28 -05:00
Vladimir Tasic
fb8b65a91b #834 #256 fix for unable to load the Indexes page
#834 #256 fix for unable to load the Indexes page
2022-04-09 21:57:45 -05:00
ta264
b4e0608b3b Fix .editorconfig to disallow this
[common]
2022-04-08 20:20:24 +01:00
ta264
60d9f02830 New: MyAnonamouse freeleech support 2022-04-08 18:28:00 +01:00
bakerboy448
4f83116413 Fixed: (BHD) TMDb Parsing Exception 2022-04-03 12:04:39 -05:00
missingfile
d440bc079f Fixed: MoreThanTV indexer from browse page layout changes (#922) 2022-04-02 16:50:35 -05:00
Qstick
a5c7c6cbcb We don't have two Radarrs 2022-04-02 16:30:29 -05:00
Qstick
710c3d6deb Fix indent from 37c393a659 2022-04-02 15:34:36 -05:00
bakerboy448
1959efbd09 Fixed: (HDBits) Treat 403 as Query Limit 2022-04-02 14:58:58 -05:00
bakerboy448
c4468b9cbb Fixed: (PTP) Treat 403 as Query Limit 2022-04-02 14:58:58 -05:00
bakerboy448
8762d94dda New: (BTN) Rate Limit to 1 Query per 5 Seconds 2022-04-02 14:50:06 -05:00
bakerboy448
e07ea80977 Fixed: (BTN) Handle Query Limit Error 2022-04-02 14:50:06 -05:00
bakerboy448
37c393a659 New: (Lidarr/Radarr/Readarr/Sonarr) Improved Errors 2022-04-02 14:26:03 -05:00
ta264
b4b779df5c Fixed: Loading old commands from database
Fixes #926

(cherry picked from commit 0f87cb72e559a19bddc6c9d4387ec7d9428291f8)
2022-04-02 14:22:00 -05:00
Qstick
1dbf35deb5 Fixed: Cleanup Temp files after backup creation
Fixes #925

(cherry picked from commit 7f0b708cb9f16a0116199625a2a5b4e67049be6a)
2022-04-02 14:22:00 -05:00
Qstick
6ab226c43a Add Support 2022-04-02 14:20:02 -05:00
Weblate
3a896fc43e Translated using Weblate (Finnish)
Currently translated at 97.7% (436 of 446 strings)

Co-authored-by: Anonymous <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fi/
Translation: Servarr/Prowlarr
2022-03-31 20:53:42 -05:00
bakerboy448
a81d632878 Fixed: Indexer Infobox Error (#920)
* Fixed: Indexer Infobox Error

* fixup! Fixed: Indexer Infobox Error

* fixup! Fixed: Indexer Infobox Error
2022-03-30 09:22:17 -05:00
bakerboy448
cb8c0d4aa7 New: Indexer Description in Add Indexer Modal 2022-03-29 23:24:48 -05:00
bakerboy448
788a5a3e24 Fixed: Missing Translates 2022-03-29 23:24:48 -05:00
bakerboy448
4f056bf228 New: Add Search Capabilities to Indexer API & InfoBox 2022-03-29 23:24:48 -05:00
Robin Dadswell
6ce9c779c1 Fixed: Update from version in logs
Closes #873
Closes #877

(cherry picked from commit bc3e3714b97dec19091e45324d9f2b550f1dbbd5)
(cherry picked from commit b34f4fde1bbe611bb5c886890e3be7cec2b96e6b)
2022-03-29 23:23:04 -05:00
Servarr
32fc2ec365 Automated API Docs update 2022-03-29 23:22:23 -05:00
Weblate
9ddd38a334 Translated using Weblate (Chinese (Simplified) (zh_CN))
Currently translated at 98.5% (422 of 428 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (428 of 428 strings)

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: libsu <libsu@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2022-03-29 23:06:34 -05:00
Weblate
b45e5a5e38 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (428 of 428 strings)

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translation: Servarr/Prowlarr
2022-03-29 20:21:49 -05:00
Qstick
135efe6d94 Fixed: Validation when testing indexers, connections and download clients
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2022-03-28 19:55:35 -05:00
Qstick
69de6d18eb Fixed: Prevent delete of last profile 2022-03-25 18:53:29 -05:00
Qstick
627da14a32 New: Load more (page) results on Search UI 2022-03-24 22:19:00 -05:00
Qstick
83bf3620c0 Update webpack packages 2022-03-24 21:02:06 -05:00
Qstick
fa41bf3c58 Frontend Package Updates 2022-03-24 20:30:56 -05:00
Qstick
31e7a101ef Backend Package Updates 2022-03-24 20:30:56 -05:00
Qstick
22f9e9e6b7 Bump dotnet to 6.0.3 2022-03-24 20:30:56 -05:00
Weblate
8834431ba6 Translated using Weblate (Spanish)
Currently translated at 77.5% (332 of 428 strings)

Co-authored-by: RicardoVelaC <ricardovelac@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translation: Servarr/Prowlarr
2022-03-24 18:47:24 -05:00
bakerboy448
01cc9b3d07 Fixed: (Gazelle) Replace Periods for Space in Search Term
based on Jackett b8b816f953ad9d0508f95a9c31f9ff4385ebdd73
2022-03-23 21:18:39 -05:00
bakerboy448
a0cbe1de5d Fixed: (HDSpace) Replace Periods for Space in Search Term
based on jackett 22efff93e7f1df858b341b3c2756d8204cae6f4a
2022-03-23 21:18:39 -05:00
bakerboy448
75792c0760 Fixed: (Anthelion) Replace Periods for Space in Search Term
based on jackett 22efff93e7f1df858b341b3c2756d8204cae6f4a
2022-03-23 21:18:39 -05:00
bakerboy448
83ca724120 Fixed: (Redacted) Map Categories Comedy & E-Learning Videos to 'Other'
indexer does not actually have movies and tv
2022-03-23 21:16:22 -05:00
bakerboy448
aee6ee1a00 Fixed: No longer require first run as admin on windows (#885)
* Fixed: No longer require first run as admin on windows

(cherry picked from commit 8d9302da7761752189b2d2820915d7d13197ce47)
(cherry picked from commit daa9ee30a2c5fc2511e34167ce642c1bfe2a8b41)

Co-Authored By: ta264 <ta264@users.noreply.github.com>

* Build installer from build.sh

(cherry picked from commit 6db135877a8394244a95142347b99ea651e0cceb)
(cherry picked from commit 86102349c54f7e20f2d1e5ab104505a4ea0d68bc)

* Fixed: (macOS) Prowlarr Bundle Identifier using .video

* fixup! Fixed: No longer require first run as admin on windows

* fixup! Build installer from build.sh

Co-authored-by: ta264 <ta264@users.noreply.github.com>
2022-03-23 20:58:42 -05:00
Weblate
e66c69a292 Translated using Weblate (Chinese (Simplified) (zh_CN))
Currently translated at 98.3% (421 of 428 strings)

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

Currently translated at 98.1% (420 of 428 strings)

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

Currently translated at 98.1% (420 of 428 strings)

Translated using Weblate (French)

Currently translated at 98.5% (422 of 428 strings)

Co-authored-by: EthanChoy <ethanchoy@163.com>
Co-authored-by: KevoM <lilmarsu@gmail.com>
Co-authored-by: Vincent <intelligentvincent@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: wzw1wzw <opinionspirit222@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2022-03-23 19:31:41 -05:00
JigSawFr
930370729b indexer(xthor): moved to YAML definition v5 2022-03-21 19:45:57 -05:00
bakerboy448
2f5b55013a Fixed: '/indexers' URL Base breaking UI navigation
(cherry picked from commit 140547e42a32cbaf80e7a794c5be9bfb574e97a2)
2022-03-21 18:01:39 -05:00
Weblate
cb74fade18 Translated using Weblate (French)
Currently translated at 98.5% (422 of 428 strings)

Co-authored-by: KevoM <lilmarsu@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translation: Servarr/Prowlarr
2022-03-21 18:00:16 -05:00
nitsua
e0da422b0e Fix app settings delete modal not closing and reloading app profiles 2022-03-21 15:31:30 +00:00
Weblate
77caaa2a55 Translated using Weblate (French)
Currently translated at 97.1% (416 of 428 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 1.6% (7 of 428 strings)

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

Currently translated at 95.5% (409 of 428 strings)

Co-authored-by: Ayi <4ayixd@gmail.com>
Co-authored-by: Kakise <sam.taa@icloud.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: 杰森 <john@johnrosen1.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_Hans/
Translation: Servarr/Prowlarr
2022-03-18 14:39:40 -05:00
Qstick
55d03967ec Bump Swashbuckle to 6.3.0 2022-03-14 22:33:08 -05:00
Weblate
684c30893a Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (428 of 428 strings)

Translated using Weblate (Portuguese)

Currently translated at 82.0% (351 of 428 strings)

Translated using Weblate (French)

Currently translated at 96.7% (414 of 428 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: José Eduardo Veiga <vitruxpt@vitruxbot.com>
Co-authored-by: KevoM <lilmarsu@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translation: Servarr/Prowlarr
2022-03-11 01:46:34 -06:00
bakerboy448
3b51a3a618 fixup! New: (DanishBytes) Move to YML 2022-03-06 21:23:35 -06:00
bakerboy448
09bd8137fc New: (DanishBytes) Move to YML 2022-03-06 21:23:35 -06:00
Weblate
495daf7967 Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Translated using Weblate (Chinese (Traditional) (zh_TW))

Currently translated at 0.4% (2 of 428 strings)

Translated using Weblate (Arabic)

Currently translated at 76.1% (326 of 428 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (428 of 428 strings)

Translated using Weblate (Vietnamese)

Currently translated at 75.9% (325 of 428 strings)

Translated using Weblate (Turkish)

Currently translated at 75.7% (324 of 428 strings)

Translated using Weblate (Thai)

Currently translated at 75.9% (325 of 428 strings)

Translated using Weblate (Swedish)

Currently translated at 93.4% (400 of 428 strings)

Translated using Weblate (Russian)

Currently translated at 77.1% (330 of 428 strings)

Translated using Weblate (Romanian)

Currently translated at 75.9% (325 of 428 strings)

Translated using Weblate (Portuguese)

Currently translated at 78.0% (334 of 428 strings)

Translated using Weblate (Polish)

Currently translated at 75.9% (325 of 428 strings)

Translated using Weblate (Dutch)

Currently translated at 93.6% (401 of 428 strings)

Translated using Weblate (Dutch)

Currently translated at 93.6% (401 of 428 strings)

Translated using Weblate (Korean)

Currently translated at 56.5% (242 of 428 strings)

Translated using Weblate (Japanese)

Currently translated at 75.9% (325 of 428 strings)

Translated using Weblate (Italian)

Currently translated at 80.3% (344 of 428 strings)

Translated using Weblate (Icelandic)

Currently translated at 75.9% (325 of 428 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (428 of 428 strings)

Translated using Weblate (Hindi)

Currently translated at 75.9% (325 of 428 strings)

Translated using Weblate (Hebrew)

Currently translated at 75.9% (325 of 428 strings)

Translated using Weblate (French)

Currently translated at 93.6% (401 of 428 strings)

Translated using Weblate (Finnish)

Currently translated at 100.0% (428 of 428 strings)

Translated using Weblate (Spanish)

Currently translated at 76.8% (329 of 428 strings)

Translated using Weblate (Greek)

Currently translated at 75.9% (325 of 428 strings)

Translated using Weblate (German)

Currently translated at 100.0% (428 of 428 strings)

Translated using Weblate (Danish)

Currently translated at 75.9% (325 of 428 strings)

Translated using Weblate (Czech)

Currently translated at 76.4% (327 of 428 strings)

Translated using Weblate (Bulgarian)

Currently translated at 70.7% (303 of 428 strings)

Translated using Weblate (Portuguese)

Currently translated at 78.0% (334 of 428 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.3% (421 of 428 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.3% (421 of 428 strings)

Translated using Weblate (German)

Currently translated at 91.3% (391 of 428 strings)

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

Currently translated at 93.6% (401 of 428 strings)

Translated using Weblate (Finnish)

Currently translated at 100.0% (428 of 428 strings)

Translated using Weblate (Finnish)

Currently translated at 100.0% (428 of 428 strings)

Added translation using Weblate (Ukrainian)

Added translation using Weblate (Persian)

Added translation using Weblate (Bengali)

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Nuno Filipe de Vilhena Santos <nunovilhenasantos@msn.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Rico_Walker <ricardo.walker1203@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Zalhera <tobias.bechen@gmail.com>
Co-authored-by: nopetw <lubduphaur@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/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/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/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/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_TW/
Translation: Servarr/Prowlarr
2022-03-06 21:23:19 -06:00
gaizaharduz
bb3821c254 New: (RuTracker.org) add .bet mirror (#876) 2022-03-06 14:21:15 -06:00
bakerboy448
855f8d35f2 Fixed:(pornolab) language formatting 2022-02-28 18:47:50 -06:00
Qstick
15dab381af New: Housekeeper for ApplicationStatus 2022-02-27 23:14:02 -06:00
bakerboy448
9c5a88e2e7 Fixed: Cleanse Tracker api_token from logs 2022-02-27 22:34:37 -06:00
Qstick
80d295cce5 New: (HDTorrents) Add hd-torrents.org as Url option
Fixes #853
2022-02-27 22:27:13 -06:00
Qstick
76afb70b01 New: (Cardigann) Allow JSON filters
Fixes #844
2022-02-27 22:19:19 -06:00
Qstick
c29fba3a2b Fixed: Convert List<HistoryEventTypes> to Int before passing to DB
Fixes #861
2022-02-27 19:10:49 -06:00
Qstick
68e41f0860 Fixed: WhereBuilder for Postgres 2022-02-27 18:09:40 -06:00
Weblate
4c68645175 Translated using Weblate (Finnish)
Currently translated at 100.0% (428 of 428 strings)

Translated using Weblate (Finnish)

Currently translated at 100.0% (428 of 428 strings)

Added translation using Weblate (Ukrainian)

Added translation using Weblate (Persian)

Added translation using Weblate (Bengali)

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fi/
Translation: Servarr/Prowlarr
2022-02-23 21:45:57 -06:00
ta264
aaef8fb29c Fixed: Make authentication cookie name unique to Prowlarr 2022-02-20 21:38:07 +00:00
Pfuenzle
b2e300b6da Update Categories 2022-02-20 11:09:02 -06:00
bakerboy448
77c840b03a Fixed: Enable response compression over https
(cherry picked from commit cb0ae3603309daae4d6b026de15f26cb4b59096d)
2022-02-16 14:04:38 -06:00
bakerboy448
98c3408909 Fixed: (RuTracker) Update Cats
(cherry picked from jackett commit 7a7144bd9d6b4fc4c1ace4886d3bc62ec37b35e6)

Fixes #836
2022-02-15 19:15:13 -06:00
Zak Saunders
40f6c2e59d Fixed: Clarify App Sync Settings (#847) 2022-02-15 16:35:12 -06:00
James White
64c9bb4231 Set version header to X-Application-Version (missing hyphen)
Not sure if this is intentional or really harms anything but to be consistent with other Arr apps like Sonarr and Radarr, this changes the version header to X-Application-Version.
2022-02-06 21:05:27 -06:00
Qstick
94ef3ea88f Go to http if def exists on def server 2022-02-06 16:11:42 -05:00
bakerboy448
dab4500b16 Fixed: (BHD) Handle API Auth Errors 2022-02-05 19:54:26 -06:00
bakerboy448
d951943c67 Fixed: (Immortalseed) Keywordless Search 2022-02-05 17:46:04 -06:00
Qstick
bba6f9349b Fixed: (Cardigann) TraktId was mapping to TvRageId 2022-02-05 14:16:05 -06:00
Qstick
7e0f88ad7a New: (Cardigann) - Cardigann v4 Support for Genre, Year, and TraktID 2022-02-05 13:30:12 -06:00
bakerboy448
88c6cbf943 New: (Cardigann) - Cardigann v4 Support for categorydesc
based on Jackett 02e43bd6a9838562a14c6bd6ea10cfed5a293d59
based on Jackett 60d2c425e1038a28f54bda658a9f543f3a197a66

Fixes #782
2022-02-05 13:30:12 -06:00
bakerboy448
69d31f96de New: (Cardigann) - Cardigann v4 Add Support for MapTrackerCatDescToNewznab 2022-02-05 13:30:12 -06:00
bakerboy448
a5718ad937 New: (Cardigann) - Cardigann v4 Improved Search Logging 2022-02-05 13:30:12 -06:00
bakerboy448
505d9c151d Fixed: Corrected Query Limit and Grab Limit HelpText 2022-02-05 12:44:13 -06:00
Qstick
a637677ec4 New: (Avistaz) Better error reporting for unauthorized tests
Fixes #822
2022-02-04 22:51:55 -06:00
bakerboy448
a0d5421dc8 Fixed: (Cardigann) Requests Failing for Definitions without LegacyLinks 2022-02-04 13:17:07 -06:00
dependabot[bot]
95f62be50c Bump SharpZipLib from 1.3.1 to 1.3.3 in /src/NzbDrone.Common
Bumps [SharpZipLib](https://github.com/icsharpcode/SharpZipLib) from 1.3.1 to 1.3.3.
- [Release notes](https://github.com/icsharpcode/SharpZipLib/releases)
- [Changelog](https://github.com/icsharpcode/SharpZipLib/blob/master/docs/Changes.txt)
- [Commits](https://github.com/icsharpcode/SharpZipLib/compare/v1.3.1...v1.3.3)

---
updated-dependencies:
- dependency-name: SharpZipLib
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-04 10:23:26 -06:00
Qstick
d7b5100e35 Fixed: (Cardigann) Smarter redirect domain compare
Fixes #808
2022-02-03 23:32:25 -06:00
Qstick
64c1e1fa54 Fixed: (Cardigann) Treat "Refresh" header as redirect
Fixes #812
2022-02-03 23:16:20 -06:00
Qstick
74663ea077 Fixed: (Cardigann) Replace legacy links with default link when making requests 2022-02-03 22:29:46 -06:00
Qstick
bc1e397ce3 Sync Indexers on app start, go to http if not sync'd yet 2022-01-31 12:10:52 -06:00
Qstick
17608cf915 Misc definition handling improvements 2022-01-30 20:57:06 -06:00
PearsonFlyer
a3de574de5 Fixed: Updated ruTorrent stopped state helptext 2022-01-26 09:53:07 -06:00
Robin Dadswell
22161e6d57 Fixed: Added missing translate for Database 2022-01-25 17:24:29 -06:00
Zippy79
c46ed33544 Fixed: Download limit check was using the query limit instead of the grab limit. 2022-01-25 08:56:54 -06:00
165 changed files with 5083 additions and 4264 deletions

View File

@@ -19,10 +19,10 @@ indent_size = 4
dotnet_sort_system_directives_first = true
# Avoid "this." and "Me." if not necessary
dotnet_style_qualification_for_field = false:refactoring
dotnet_style_qualification_for_property = false:refactoring
dotnet_style_qualification_for_method = false:refactoring
dotnet_style_qualification_for_event = false:refactoring
dotnet_style_qualification_for_field = false:warning
dotnet_style_qualification_for_property = false:warning
dotnet_style_qualification_for_method = false:warning
dotnet_style_qualification_for_event = false:warning
# Indentation preferences
csharp_indent_block_contents = true
@@ -32,10 +32,6 @@ csharp_indent_case_contents_when_block = true
csharp_indent_switch_labels = true
csharp_indent_labels = flush_left
dotnet_style_qualification_for_field = false:suggestion
dotnet_style_qualification_for_property = false:suggestion
dotnet_style_qualification_for_method = false:suggestion
dotnet_style_qualification_for_event = false:suggestion
dotnet_naming_style.instance_field_style.capitalization = camel_case
dotnet_naming_style.instance_field_style.required_prefix = _

View File

@@ -13,7 +13,9 @@ variables:
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.100'
dotnetVersion: '6.0.201'
innoVersion: '6.2.0'
nodeVersion: '16.x'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
trigger:
@@ -155,7 +157,7 @@ stages:
- task: NodeTool@0
displayName: Set Node.js version
inputs:
versionSpec: '12.x'
versionSpec: $(nodeVersion)
- checkout: self
submodules: true
fetchDepth: 1
@@ -200,16 +202,11 @@ stages:
artifactName: WindowsFrontend
targetPath: _output
displayName: Fetch Frontend
- bash: ./build.sh --packages
displayName: Create Packages
- bash: |
distribution/windows/setup/inno/ISCC.exe distribution/windows/setup/prowlarr.iss //DFramework=net6.0 //DRuntime=win-x86
cp distribution/windows/setup/output/Prowlarr.*windows.net6.0.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Prowlarr.${BUILDNAME}.windows-core-x86-installer.exe
displayName: Create x86 .NET Core Windows installer
- bash: |
distribution/windows/setup/inno/ISCC.exe distribution/windows/setup/prowlarr.iss //DFramework=net6.0 //DRuntime=win-x64
cp distribution/windows/setup/output/Prowlarr.*windows.net6.0.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Prowlarr.${BUILDNAME}.windows-core-x64-installer.exe
displayName: Create x64 .NET Core Windows installer
./build.sh --packages --installer
cp distribution/windows/setup/output/Prowlarr.*win-x64.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Prowlarr.${BUILDNAME}.windows-core-x64-installer.exe
cp distribution/windows/setup/output/Prowlarr.*win-x86.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Prowlarr.${BUILDNAME}.windows-core-x86-installer.exe
displayName: Create Installers
- publish: $(Build.ArtifactStagingDirectory)
artifact: 'WindowsInstaller'
displayName: Publish Installer
@@ -706,17 +703,17 @@ stages:
osName: 'Linux'
imageName: 'ubuntu-18.04'
pattern: 'Prowlarr.*.linux-core-x64.tar.gz'
failBuild: false
failBuild: true
Mac:
osName: 'Mac'
imageName: 'macos-10.15'
pattern: 'Prowlarr.*.osx-core-x64.tar.gz'
failBuild: false
failBuild: true
Windows:
osName: 'Windows'
imageName: 'windows-2019'
pattern: 'Prowlarr.*.windows-core-x64.zip'
failBuild: false
failBuild: true
pool:
vmImage: $(imageName)
@@ -807,7 +804,7 @@ stages:
- task: NodeTool@0
displayName: Set Node.js version
inputs:
versionSpec: '12.x'
versionSpec: $(nodeVersion)
- checkout: self
submodules: true
fetchDepth: 1
@@ -944,7 +941,7 @@ stages:
- job:
displayName: Discord Notification
pool:
vmImage: 'windows-2019'
vmImage: 'ubuntu-18.04'
steps:
- task: DownloadPipelineArtifact@2
continueOnError: true

View File

@@ -234,6 +234,32 @@ Package()
esac
}
BuildInstaller()
{
local framework="$1"
local runtime="$2"
./_inno/ISCC.exe distribution/windows/setup/prowlarr.iss "//DFramework=$framework" "//DRuntime=$runtime"
}
InstallInno()
{
ProgressStart "Installing portable Inno Setup"
rm -rf _inno
curl -s --output innosetup.exe "https://files.jrsoftware.org/is/6/innosetup-${INNOVERSION:-6.2.0}.exe"
mkdir _inno
./innosetup.exe //portable=1 //silent //currentuser //dir=.\\_inno
rm innosetup.exe
ProgressEnd "Installed portable Inno Setup"
}
RemoveInno()
{
rm -rf _inno
}
PackageTests()
{
local framework="$1"
@@ -265,6 +291,7 @@ if [ $# -eq 0 ]; then
BACKEND=YES
FRONTEND=YES
PACKAGES=YES
INSTALLER=NO
LINT=YES
ENABLE_BSD=NO
fi
@@ -300,6 +327,10 @@ case $key in
PACKAGES=YES
shift # past argument
;;
--installer)
INSTALLER=YES
shift # past argument
;;
--lint)
LINT=YES
shift # past argument
@@ -383,3 +414,11 @@ then
Package "$FRAMEWORK" "$RID"
fi
fi
if [ "$INSTALLER" = "YES" ];
then
InstallInno
BuildInstaller "net6.0" "win-x64"
BuildInstaller "net6.0" "win-x86"
RemoveInno
fi

View File

@@ -15,7 +15,7 @@
<key>CFBundleIconFile</key>
<string>prowlarr.icns</string>
<key>CFBundleIdentifier</key>
<string>com.osx.prowlarr.video</string>
<string>com.osx.prowlarr.com</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>

View File

@@ -1,3 +0,0 @@
#SET BUILD_NUMBER=1
#SET branch=develop
inno\ISCC.exe prowlarr.iss

View File

@@ -1,336 +0,0 @@
; *** Inno Setup version 5.5.3+ English messages ***
;
; To download user-contributed translations of this file, go to:
; http://www.jrsoftware.org/files/istrans/
;
; Note: When translating this text, do not add periods (.) to the end of
; messages that didn't have them already, because on those messages Inno
; Setup adds the periods automatically (appending a period would result in
; two periods being displayed).
[LangOptions]
; The following three entries are very important. Be sure to read and
; understand the '[LangOptions] section' topic in the help file.
LanguageName=English
LanguageID=$0409
LanguageCodePage=0
; If the language you are translating to requires special font faces or
; sizes, uncomment any of the following entries and change them accordingly.
;DialogFontName=
;DialogFontSize=8
;WelcomeFontName=Verdana
;WelcomeFontSize=12
;TitleFontName=Arial
;TitleFontSize=29
;CopyrightFontName=Arial
;CopyrightFontSize=8
[Messages]
; *** Application titles
SetupAppTitle=Setup
SetupWindowTitle=Setup - %1
UninstallAppTitle=Uninstall
UninstallAppFullTitle=%1 Uninstall
; *** Misc. common
InformationTitle=Information
ConfirmTitle=Confirm
ErrorTitle=Error
; *** SetupLdr messages
SetupLdrStartupMessage=This will install %1. Do you wish to continue?
LdrCannotCreateTemp=Unable to create a temporary file. Setup aborted
LdrCannotExecTemp=Unable to execute file in the temporary directory. Setup aborted
; *** Startup error messages
LastErrorMessage=%1.%n%nError %2: %3
SetupFileMissing=The file %1 is missing from the installation directory. Please correct the problem or obtain a new copy of the program.
SetupFileCorrupt=The setup files are corrupted. Please obtain a new copy of the program.
SetupFileCorruptOrWrongVer=The setup files are corrupted, or are incompatible with this version of Setup. Please correct the problem or obtain a new copy of the program.
InvalidParameter=An invalid parameter was passed on the command line:%n%n%1
SetupAlreadyRunning=Setup is already running.
WindowsVersionNotSupported=This program does not support the version of Windows your computer is running.
WindowsServicePackRequired=This program requires %1 Service Pack %2 or later.
NotOnThisPlatform=This program will not run on %1.
OnlyOnThisPlatform=This program must be run on %1.
OnlyOnTheseArchitectures=This program can only be installed on versions of Windows designed for the following processor architectures:%n%n%1
MissingWOW64APIs=The version of Windows you are running does not include functionality required by Setup to perform a 64-bit installation. To correct this problem, please install Service Pack %1.
WinVersionTooLowError=This program requires %1 version %2 or later.
WinVersionTooHighError=This program cannot be installed on %1 version %2 or later.
AdminPrivilegesRequired=You must be logged in as an administrator when installing this program.
PowerUserPrivilegesRequired=You must be logged in as an administrator or as a member of the Power Users group when installing this program.
SetupAppRunningError=Setup has detected that %1 is currently running.%n%nPlease close all instances of it now, then click OK to continue, or Cancel to exit.
UninstallAppRunningError=Uninstall has detected that %1 is currently running.%n%nPlease close all instances of it now, then click OK to continue, or Cancel to exit.
; *** Misc. errors
ErrorCreatingDir=Setup was unable to create the directory "%1"
ErrorTooManyFilesInDir=Unable to create a file in the directory "%1" because it contains too many files
; *** Setup common messages
ExitSetupTitle=Exit Setup
ExitSetupMessage=Setup is not complete. If you exit now, the program will not be installed.%n%nYou may run Setup again at another time to complete the installation.%n%nExit Setup?
AboutSetupMenuItem=&About Setup...
AboutSetupTitle=About Setup
AboutSetupMessage=%1 version %2%n%3%n%n%1 home page:%n%4
AboutSetupNote=
TranslatorNote=
; *** Buttons
ButtonBack=< &Back
ButtonNext=&Next >
ButtonInstall=&Install
ButtonOK=OK
ButtonCancel=Cancel
ButtonYes=&Yes
ButtonYesToAll=Yes to &All
ButtonNo=&No
ButtonNoToAll=N&o to All
ButtonFinish=&Finish
ButtonBrowse=&Browse...
ButtonWizardBrowse=B&rowse...
ButtonNewFolder=&Make New Folder
; *** "Select Language" dialog messages
SelectLanguageTitle=Select Setup Language
SelectLanguageLabel=Select the language to use during the installation:
; *** Common wizard text
ClickNext=Click Next to continue, or Cancel to exit Setup.
BeveledLabel=
BrowseDialogTitle=Browse For Folder
BrowseDialogLabel=Select a folder in the list below, then click OK.
NewFolderName=New Folder
; *** "Welcome" wizard page
WelcomeLabel1=Welcome to the [name] Setup Wizard
WelcomeLabel2=This will install [name/ver] on your computer.%n%nIt is recommended that you close all other applications before continuing.
; *** "Password" wizard page
WizardPassword=Password
PasswordLabel1=This installation is password protected.
PasswordLabel3=Please provide the password, then click Next to continue. Passwords are case-sensitive.
PasswordEditLabel=&Password:
IncorrectPassword=The password you entered is not correct. Please try again.
; *** "License Agreement" wizard page
WizardLicense=License Agreement
LicenseLabel=Please read the following important information before continuing.
LicenseLabel3=Please read the following License Agreement. You must accept the terms of this agreement before continuing with the installation.
LicenseAccepted=I &accept the agreement
LicenseNotAccepted=I &do not accept the agreement
; *** "Information" wizard pages
WizardInfoBefore=Information
InfoBeforeLabel=Please read the following important information before continuing.
InfoBeforeClickLabel=When you are ready to continue with Setup, click Next.
WizardInfoAfter=Information
InfoAfterLabel=Please read the following important information before continuing.
InfoAfterClickLabel=When you are ready to continue with Setup, click Next.
; *** "User Information" wizard page
WizardUserInfo=User Information
UserInfoDesc=Please enter your information.
UserInfoName=&User Name:
UserInfoOrg=&Organization:
UserInfoSerial=&Serial Number:
UserInfoNameRequired=You must enter a name.
; *** "Select Destination Location" wizard page
WizardSelectDir=Select Destination Location
SelectDirDesc=Where should [name] be installed?
SelectDirLabel3=Setup will install [name] into the following folder.
SelectDirBrowseLabel=To continue, click Next. If you would like to select a different folder, click Browse.
DiskSpaceMBLabel=At least [mb] MB of free disk space is required.
CannotInstallToNetworkDrive=Setup cannot install to a network drive.
CannotInstallToUNCPath=Setup cannot install to a UNC path.
InvalidPath=You must enter a full path with drive letter; for example:%n%nC:\APP%n%nor a UNC path in the form:%n%n\\server\share
InvalidDrive=The drive or UNC share you selected does not exist or is not accessible. Please select another.
DiskSpaceWarningTitle=Not Enough Disk Space
DiskSpaceWarning=Setup requires at least %1 KB of free space to install, but the selected drive only has %2 KB available.%n%nDo you want to continue anyway?
DirNameTooLong=The folder name or path is too long.
InvalidDirName=The folder name is not valid.
BadDirName32=Folder names cannot include any of the following characters:%n%n%1
DirExistsTitle=Folder Exists
DirExists=The folder:%n%n%1%n%nalready exists. Would you like to install to that folder anyway?
DirDoesntExistTitle=Folder Does Not Exist
DirDoesntExist=The folder:%n%n%1%n%ndoes not exist. Would you like the folder to be created?
; *** "Select Components" wizard page
WizardSelectComponents=Select Components
SelectComponentsDesc=Which components should be installed?
SelectComponentsLabel2=Select the components you want to install; clear the components you do not want to install. Click Next when you are ready to continue.
FullInstallation=Full installation
; if possible don't translate 'Compact' as 'Minimal' (I mean 'Minimal' in your language)
CompactInstallation=Compact installation
CustomInstallation=Custom installation
NoUninstallWarningTitle=Components Exist
NoUninstallWarning=Setup has detected that the following components are already installed on your computer:%n%n%1%n%nDeselecting these components will not uninstall them.%n%nWould you like to continue anyway?
ComponentSize1=%1 KB
ComponentSize2=%1 MB
ComponentsDiskSpaceMBLabel=Current selection requires at least [mb] MB of disk space.
; *** "Select Additional Tasks" wizard page
WizardSelectTasks=Select Additional Tasks
SelectTasksDesc=Which additional tasks should be performed?
SelectTasksLabel2=Select the additional tasks you would like Setup to perform while installing [name], then click Next.
; *** "Select Start Menu Folder" wizard page
WizardSelectProgramGroup=Select Start Menu Folder
SelectStartMenuFolderDesc=Where should Setup place the program's shortcuts?
SelectStartMenuFolderLabel3=Setup will create the program's shortcuts in the following Start Menu folder.
SelectStartMenuFolderBrowseLabel=To continue, click Next. If you would like to select a different folder, click Browse.
MustEnterGroupName=You must enter a folder name.
GroupNameTooLong=The folder name or path is too long.
InvalidGroupName=The folder name is not valid.
BadGroupName=The folder name cannot include any of the following characters:%n%n%1
NoProgramGroupCheck2=&Don't create a Start Menu folder
; *** "Ready to Install" wizard page
WizardReady=Ready to Install
ReadyLabel1=Setup is now ready to begin installing [name] on your computer.
ReadyLabel2a=Click Install to continue with the installation, or click Back if you want to review or change any settings.
ReadyLabel2b=Click Install to continue with the installation.
ReadyMemoUserInfo=User information:
ReadyMemoDir=Destination location:
ReadyMemoType=Setup type:
ReadyMemoComponents=Selected components:
ReadyMemoGroup=Start Menu folder:
ReadyMemoTasks=Additional tasks:
; *** "Preparing to Install" wizard page
WizardPreparing=Preparing to Install
PreparingDesc=Setup is preparing to install [name] on your computer.
PreviousInstallNotCompleted=The installation/removal of a previous program was not completed. You will need to restart your computer to complete that installation.%n%nAfter restarting your computer, run Setup again to complete the installation of [name].
CannotContinue=Setup cannot continue. Please click Cancel to exit.
ApplicationsFound=The following applications are using files that need to be updated by Setup. It is recommended that you allow Setup to automatically close these applications.
ApplicationsFound2=The following applications are using files that need to be updated by Setup. It is recommended that you allow Setup to automatically close these applications. After the installation has completed, Setup will attempt to restart the applications.
CloseApplications=&Automatically close the applications
DontCloseApplications=&Do not close the applications
ErrorCloseApplications=Setup was unable to automatically close all applications. It is recommended that you close all applications using files that need to be updated by Setup before continuing.
; *** "Installing" wizard page
WizardInstalling=Installing
InstallingLabel=Please wait while Setup installs [name] on your computer.
; *** "Setup Completed" wizard page
FinishedHeadingLabel=Completing the [name] Setup Wizard
FinishedLabelNoIcons=Setup has finished installing [name] on your computer.
FinishedLabel=Setup has finished installing [name] on your computer. The application may be launched by selecting the installed icons.
ClickFinish=Click Finish to exit Setup.
FinishedRestartLabel=To complete the installation of [name], Setup must restart your computer. Would you like to restart now?
FinishedRestartMessage=To complete the installation of [name], Setup must restart your computer.%n%nWould you like to restart now?
ShowReadmeCheck=Yes, I would like to view the README file
YesRadio=&Yes, restart the computer now
NoRadio=&No, I will restart the computer later
; used for example as 'Run MyProg.exe'
RunEntryExec=Run %1
; used for example as 'View Readme.txt'
RunEntryShellExec=View %1
; *** "Setup Needs the Next Disk" stuff
ChangeDiskTitle=Setup Needs the Next Disk
SelectDiskLabel2=Please insert Disk %1 and click OK.%n%nIf the files on this disk can be found in a folder other than the one displayed below, enter the correct path or click Browse.
PathLabel=&Path:
FileNotInDir2=The file "%1" could not be located in "%2". Please insert the correct disk or select another folder.
SelectDirectoryLabel=Please specify the location of the next disk.
; *** Installation phase messages
SetupAborted=Setup was not completed.%n%nPlease correct the problem and run Setup again.
EntryAbortRetryIgnore=Click Retry to try again, Ignore to proceed anyway, or Abort to cancel installation.
; *** Installation status messages
StatusClosingApplications=Closing applications...
StatusCreateDirs=Creating directories...
StatusExtractFiles=Extracting files...
StatusCreateIcons=Creating shortcuts...
StatusCreateIniEntries=Creating INI entries...
StatusCreateRegistryEntries=Creating registry entries...
StatusRegisterFiles=Registering files...
StatusSavingUninstall=Saving uninstall information...
StatusRunProgram=Finishing installation...
StatusRestartingApplications=Restarting applications...
StatusRollback=Rolling back changes...
; *** Misc. errors
ErrorInternal2=Internal error: %1
ErrorFunctionFailedNoCode=%1 failed
ErrorFunctionFailed=%1 failed; code %2
ErrorFunctionFailedWithMessage=%1 failed; code %2.%n%3
ErrorExecutingProgram=Unable to execute file:%n%1
; *** Registry errors
ErrorRegOpenKey=Error opening registry key:%n%1\%2
ErrorRegCreateKey=Error creating registry key:%n%1\%2
ErrorRegWriteKey=Error writing to registry key:%n%1\%2
; *** INI errors
ErrorIniEntry=Error creating INI entry in file "%1".
; *** File copying errors
FileAbortRetryIgnore=Click Retry to try again, Ignore to skip this file (not recommended), or Abort to cancel installation.
FileAbortRetryIgnore2=Click Retry to try again, Ignore to proceed anyway (not recommended), or Abort to cancel installation.
SourceIsCorrupted=The source file is corrupted
SourceDoesntExist=The source file "%1" does not exist
ExistingFileReadOnly=The existing file is marked as read-only.%n%nClick Retry to remove the read-only attribute and try again, Ignore to skip this file, or Abort to cancel installation.
ErrorReadingExistingDest=An error occurred while trying to read the existing file:
FileExists=The file already exists.%n%nWould you like Setup to overwrite it?
ExistingFileNewer=The existing file is newer than the one Setup is trying to install. It is recommended that you keep the existing file.%n%nDo you want to keep the existing file?
ErrorChangingAttr=An error occurred while trying to change the attributes of the existing file:
ErrorCreatingTemp=An error occurred while trying to create a file in the destination directory:
ErrorReadingSource=An error occurred while trying to read the source file:
ErrorCopying=An error occurred while trying to copy a file:
ErrorReplacingExistingFile=An error occurred while trying to replace the existing file:
ErrorRestartReplace=RestartReplace failed:
ErrorRenamingTemp=An error occurred while trying to rename a file in the destination directory:
ErrorRegisterServer=Unable to register the DLL/OCX: %1
ErrorRegSvr32Failed=RegSvr32 failed with exit code %1
ErrorRegisterTypeLib=Unable to register the type library: %1
; *** Post-installation errors
ErrorOpeningReadme=An error occurred while trying to open the README file.
ErrorRestartingComputer=Setup was unable to restart the computer. Please do this manually.
; *** Uninstaller messages
UninstallNotFound=File "%1" does not exist. Cannot uninstall.
UninstallOpenError=File "%1" could not be opened. Cannot uninstall
UninstallUnsupportedVer=The uninstall log file "%1" is in a format not recognized by this version of the uninstaller. Cannot uninstall
UninstallUnknownEntry=An unknown entry (%1) was encountered in the uninstall log
ConfirmUninstall=Are you sure you want to completely remove %1 and all of its components?
UninstallOnlyOnWin64=This installation can only be uninstalled on 64-bit Windows.
OnlyAdminCanUninstall=This installation can only be uninstalled by a user with administrative privileges.
UninstallStatusLabel=Please wait while %1 is removed from your computer.
UninstalledAll=%1 was successfully removed from your computer.
UninstalledMost=%1 uninstall complete.%n%nSome elements could not be removed. These can be removed manually.
UninstalledAndNeedsRestart=To complete the uninstallation of %1, your computer must be restarted.%n%nWould you like to restart now?
UninstallDataCorrupted="%1" file is corrupted. Cannot uninstall
; *** Uninstallation phase messages
ConfirmDeleteSharedFileTitle=Remove Shared File?
ConfirmDeleteSharedFile2=The system indicates that the following shared file is no longer in use by any programs. Would you like for Uninstall to remove this shared file?%n%nIf any programs are still using this file and it is removed, those programs may not function properly. If you are unsure, choose No. Leaving the file on your system will not cause any harm.
SharedFileNameLabel=File name:
SharedFileLocationLabel=Location:
WizardUninstalling=Uninstall Status
StatusUninstalling=Uninstalling %1...
; *** Shutdown block reasons
ShutdownBlockReasonInstallingApp=Installing %1.
ShutdownBlockReasonUninstallingApp=Uninstalling %1.
; The custom messages below aren't used by Setup itself, but if you make
; use of them in your scripts, you'll want to translate them.
[CustomMessages]
NameAndVersion=%1 version %2
AdditionalIcons=Additional icons:
CreateDesktopIcon=Create a &desktop icon
CreateQuickLaunchIcon=Create a &Quick Launch icon
ProgramOnTheWeb=%1 on the Web
UninstallProgram=Uninstall %1
LaunchProgram=Launch %1
AssocFileExtension=&Associate %1 with the %2 file extension
AssocingFileExtension=Associating %1 with the %2 file extension...
AutoStartProgramGroupDescription=Startup:
AutoStartProgram=Automatically start %1
AddonHostProgramNotFound=%1 could not be located in the folder you selected.%n%nDo you want to continue anyway?

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -3,13 +3,12 @@
#define AppName "Prowlarr"
#define AppPublisher "Team Prowlarr"
#define AppURL "https://prowlarr.video/"
#define ForumsURL "https://forums.prowlarr.video/"
#define AppURL "https://prowlarr.com/"
#define ForumsURL "https://prowlarr.com/discord/"
#define AppExeName "Prowlarr.exe"
#define BaseVersion GetEnv('MAJORVERSION')
#define BuildNumber GetEnv('MINORVERSION')
#define BuildVersion GetEnv('PROWLARRVERSION')
#define BranchName GetEnv('BUILD_SOURCEBRANCHNAME')
[Setup]
; NOTE: The value of AppId uniquely identifies this application.
@@ -22,15 +21,15 @@ AppPublisher={#AppPublisher}
AppPublisherURL={#AppURL}
AppSupportURL={#ForumsURL}
AppUpdatesURL={#AppURL}
DefaultDirName={commonappdata}\Prowlarr\bin
DefaultDirName={commonappdata}\Prowlarr
DisableDirPage=yes
DefaultGroupName={#AppName}
DisableProgramGroupPage=yes
OutputBaseFilename=Prowlarr.{#BranchName}.{#BuildVersion}.windows.{#Framework}
OutputBaseFilename=Prowlarr.{#BuildVersion}.{#Runtime}
SolidCompression=yes
AppCopyright=Creative Commons 3.0 License
AllowUNCPath=False
UninstallDisplayIcon={app}\Prowlarr.exe
UninstallDisplayIcon={app}\bin\Prowlarr.exe
DisableReadyPage=True
CompressionThreads=2
Compression=lzma2/normal
@@ -38,6 +37,7 @@ AppContact={#ForumsURL}
VersionInfoVersion={#BaseVersion}.{#BuildNumber}
SetupLogging=yes
OutputDir=output
WizardStyle=modern
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
@@ -48,28 +48,31 @@ Name: "windowsService"; Description: "Install Windows Service (Starts when the c
Name: "startupShortcut"; Description: "Create shortcut in Startup folder (Starts when you log into Windows)"; GroupDescription: "Start automatically"; Flags: exclusive unchecked
Name: "none"; Description: "Do not start automatically"; GroupDescription: "Start automatically"; Flags: exclusive unchecked
[Dirs]
Name: "{app}"; Permissions: users-modify
[Files]
Source: "..\..\..\_artifacts\{#Runtime}\{#Framework}\Prowlarr\Prowlarr.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\..\..\_artifacts\{#Runtime}\{#Framework}\Prowlarr\*"; Excludes: "Prowlarr.Update"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "..\..\..\_artifacts\{#Runtime}\{#Framework}\Prowlarr\Prowlarr.exe"; DestDir: "{app}\bin"; Flags: ignoreversion
Source: "..\..\..\_artifacts\{#Runtime}\{#Framework}\Prowlarr\*"; Excludes: "Prowlarr.Update"; DestDir: "{app}\bin"; Flags: ignoreversion recursesubdirs createallsubdirs
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
[Icons]
Name: "{group}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Parameters: "/icon"
Name: "{commondesktop}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Parameters: "/icon"; Tasks: desktopIcon
Name: "{userstartup}\{#AppName}"; Filename: "{app}\Prowlarr.exe"; WorkingDir: "{app}"; Tasks: startupShortcut
Name: "{group}\{#AppName}"; Filename: "{app}\bin\{#AppExeName}"; Parameters: "/icon"
Name: "{commondesktop}\{#AppName}"; Filename: "{app}\bin\{#AppExeName}"; Parameters: "/icon"; Tasks: desktopIcon
Name: "{userstartup}\{#AppName}"; Filename: "{app}\bin\Prowlarr.exe"; WorkingDir: "{app}\bin"; Tasks: startupShortcut
[InstallDelete]
Name: "{app}"; Type: filesandordirs
Name: "{app}\bin"; Type: filesandordirs
[Run]
Filename: "{app}\Prowlarr.Console.exe"; StatusMsg: "Removing previous Windows Service"; Parameters: "/u /exitimmediately"; Flags: runhidden waituntilterminated;
Filename: "{app}\Prowlarr.Console.exe"; Description: "Enable Access from Other Devices"; StatusMsg: "Enabling Remote access"; Parameters: "/registerurl /exitimmediately"; Flags: postinstall runascurrentuser runhidden waituntilterminated; Tasks: startupShortcut none;
Filename: "{app}\Prowlarr.Console.exe"; StatusMsg: "Installing Windows Service"; Parameters: "/i /exitimmediately"; Flags: runhidden waituntilterminated; Tasks: windowsService
Filename: "{app}\Prowlarr.exe"; Description: "Open Prowlarr Web UI"; Flags: postinstall skipifsilent nowait; Tasks: windowsService;
Filename: "{app}\Prowlarr.exe"; Description: "Start Prowlarr"; Flags: postinstall skipifsilent nowait; Tasks: startupShortcut none;
Filename: "{app}\bin\Prowlarr.Console.exe"; StatusMsg: "Removing previous Windows Service"; Parameters: "/u /exitimmediately"; Flags: runhidden waituntilterminated;
Filename: "{app}\bin\Prowlarr.Console.exe"; Description: "Enable Access from Other Devices"; StatusMsg: "Enabling Remote access"; Parameters: "/registerurl /exitimmediately"; Flags: postinstall runascurrentuser runhidden waituntilterminated; Tasks: startupShortcut none;
Filename: "{app}\bin\Prowlarr.Console.exe"; StatusMsg: "Installing Windows Service"; Parameters: "/i /exitimmediately"; Flags: runhidden waituntilterminated; Tasks: windowsService
Filename: "{app}\bin\Prowlarr.exe"; Description: "Open Prowlarr Web UI"; Flags: postinstall skipifsilent nowait; Tasks: windowsService;
Filename: "{app}\bin\Prowlarr.exe"; Description: "Start Prowlarr"; Flags: postinstall skipifsilent nowait; Tasks: startupShortcut none;
[UninstallRun]
Filename: "{app}\prowlarr.console.exe"; Parameters: "/u"; Flags: waituntilterminated skipifdoesntexist
Filename: "{app}\bin\prowlarr.console.exe"; Parameters: "/u"; Flags: waituntilterminated skipifdoesntexist
[Code]
function PrepareToInstall(var NeedsRestart: Boolean): String;

View File

@@ -27,11 +27,11 @@ dotnet clean $slnFile -c Release
dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids
dotnet new tool-manifest
dotnet tool install --version 6.2.3 Swashbuckle.AspNetCore.Cli
dotnet tool install --version 6.3.0 Swashbuckle.AspNetCore.Cli
dotnet tool run swagger tofile --output ./src/Prowlarr.Api.V1/openapi.json "$outputFolder/net6.0/$RUNTIME/prowlarr.console.dll" v1 &
sleep 10
sleep 30
kill %1

View File

@@ -47,10 +47,6 @@ class Link extends Component {
el = 'a';
linkProps.href = to;
linkProps.target = target || '_self';
} else if (to.startsWith(`${window.Prowlarr.urlBase}/`)) {
el = RouterLink;
linkProps.to = to;
linkProps.target = target;
} else {
el = RouterLink;
linkProps.to = `${window.Prowlarr.urlBase}/${to.replace(/^\//, '')}`;

View File

@@ -37,7 +37,8 @@ function ModalError(props) {
{translate('Close')}
</Button>
</ModalFooter>
</ModalContent>);
</ModalContent>
);
}
ModalError.propTypes = {

View File

@@ -1,6 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Scrollbars } from 'react-custom-scrollbars';
import { Scrollbars } from 'react-custom-scrollbars-2';
import { scrollDirections } from 'Helpers/Props';
import styles from './OverlayScroller.css';

View File

@@ -37,6 +37,12 @@ const columns = [
isSortable: true,
isVisible: true
},
{
name: 'description',
label: translate('Description'),
isSortable: false,
isVisible: true
},
{
name: 'privacy',
label: translate('Privacy'),
@@ -136,12 +142,12 @@ class AddIndexerModalContent extends Component {
return true;
});
const errorMessage = getErrorMessage(error, 'Unable to load indexers');
const errorMessage = getErrorMessage(error, translate('UnableToLoadIndexers'));
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Add Indexer
{translate('AddIndexer')}
</ModalHeader>
<ModalBody

View File

@@ -32,6 +32,7 @@ class SelectIndexerRow extends Component {
privacy,
name,
language,
description,
isExistingIndexer
} = this.props;
@@ -61,6 +62,10 @@ class SelectIndexerRow extends Component {
{language}
</TableRowCell>
<TableRowCell>
{description}
</TableRowCell>
<TableRowCell>
{translate(firstCharToUpper(privacy))}
</TableRowCell>
@@ -74,6 +79,7 @@ SelectIndexerRow.propTypes = {
protocol: PropTypes.string.isRequired,
privacy: PropTypes.string.isRequired,
language: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
implementation: PropTypes.string.isRequired,
onIndexerSelect: PropTypes.func.isRequired,
isExistingIndexer: PropTypes.bool.isRequired

View File

@@ -190,7 +190,7 @@ class IndexerIndexRow extends Component {
key={name}
className={styles[column.name]}
>
{appProfile.name}
{appProfile?.name || ''}
</VirtualTableRowCell>
);
}
@@ -244,12 +244,15 @@ class IndexerIndexRow extends Component {
onPress={this.onIndexerInfoPress}
/>
<IconButton
className={styles.externalLink}
name={icons.EXTERNAL_LINK}
title={translate('Website')}
to={indexerUrls[0].replace('api.', '')}
/>
{
indexerUrls ?
<IconButton
className={styles.externalLink}
name={icons.EXTERNAL_LINK}
title={translate('Website')}
to={indexerUrls[0].replace('api.', '')}
/> : null
}
<IconButton
name={icons.EDIT}
@@ -289,7 +292,7 @@ class IndexerIndexRow extends Component {
IndexerIndexRow.propTypes = {
id: PropTypes.number.isRequired,
indexerUrls: PropTypes.arrayOf(PropTypes.string).isRequired,
indexerUrls: PropTypes.arrayOf(PropTypes.string),
protocol: PropTypes.string.isRequired,
privacy: PropTypes.string.isRequired,
priority: PropTypes.number.isRequired,
@@ -298,7 +301,7 @@ IndexerIndexRow.propTypes = {
redirect: PropTypes.bool.isRequired,
appProfile: PropTypes.object.isRequired,
status: PropTypes.object,
capabilities: PropTypes.object.isRequired,
capabilities: PropTypes.object,
added: PropTypes.string.isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,

View File

@@ -4,6 +4,7 @@ import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription';
import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle';
import FieldSet from 'Components/FieldSet';
import Link from 'Components/Link/Link';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
@@ -20,6 +21,7 @@ function IndexerInfoModalContent(props) {
language,
indexerUrls,
protocol,
capabilities,
onModalClose
} = props;
@@ -30,41 +32,78 @@ function IndexerInfoModalContent(props) {
</ModalHeader>
<ModalBody>
<DescriptionList>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Id')}
data={id}
/>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Description')}
data={description ? description : '-'}
/>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Encoding')}
data={encoding ? encoding : '-'}
/>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Language')}
data={language ?? '-'}
/>
<DescriptionListItemTitle>Indexer Site</DescriptionListItemTitle>
<DescriptionListItemDescription>
<Link to={indexerUrls[0]}>{indexerUrls[0]}</Link>
</DescriptionListItemDescription>
<DescriptionListItemTitle>{`${protocol === 'usenet' ? 'Newznab' : 'Torznab'} Url`}</DescriptionListItemTitle>
<DescriptionListItemDescription>
{`${window.location.origin}${window.Prowlarr.urlBase}/${id}/api`}
</DescriptionListItemDescription>
</DescriptionList>
<FieldSet legend={translate('IndexerDetails')}>
<div className={styles.groups}>
<DescriptionList>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Id')}
data={id}
/>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Description')}
data={description ? description : '-'}
/>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Encoding')}
data={encoding ? encoding : '-'}
/>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Language')}
data={language ?? '-'}
/>
<DescriptionListItemTitle>{translate('IndexerSite')}</DescriptionListItemTitle>
<DescriptionListItemDescription>
<Link to={indexerUrls[0]}>{indexerUrls[0]}</Link>
</DescriptionListItemDescription>
<DescriptionListItemTitle>{`${protocol === 'usenet' ? 'Newznab' : 'Torznab'} Url`}</DescriptionListItemTitle>
<DescriptionListItemDescription>
{`${window.location.origin}${window.Prowlarr.urlBase}/${id}/api`}
</DescriptionListItemDescription>
</DescriptionList>
</div>
</FieldSet>
<FieldSet legend={translate('SearchCapabilities')}>
<div className={styles.groups}>
<DescriptionList>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('RawSearchSupported')}
data={capabilities.supportsRawSearch ? translate('Yes') : translate('No')}
/>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('SearchTypes')}
data={capabilities.searchParams.length === 0 ? translate('NotSupported') : capabilities.searchParams[0]}
/>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('TVSearchTypes')}
data={capabilities.tvSearchParams.length === 0 ? translate('NotSupported') : capabilities.tvSearchParams.join(', ')}
/>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('MovieSearchTypes')}
data={capabilities.movieSearchParams.length === 0 ? translate('NotSupported') : capabilities.movieSearchParams.join(', ')}
/>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('BookSearchTypes')}
data={capabilities.bookSearchParams.length === 0 ? translate('NotSupported') : capabilities.bookSearchParams.join(', ')}
/>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('MusicSearchTypes')}
data={capabilities.musicSearchParams.length === 0 ? translate('NotSupported') : capabilities.musicSearchParams.join(', ')}
/>
</DescriptionList>
</div>
</FieldSet>
</ModalBody>
</ModalContent>
</ModalContent >
);
}
@@ -76,6 +115,7 @@ IndexerInfoModalContent.propTypes = {
language: PropTypes.string.isRequired,
indexerUrls: PropTypes.arrayOf(PropTypes.string).isRequired,
protocol: PropTypes.string.isRequired,
capabilities: PropTypes.object.isRequired,
onModalClose: PropTypes.func.isRequired
};

View File

@@ -248,7 +248,7 @@ class QueryParameterModal extends Component {
onSelectionChange={this.onInputSelectionChange}
/>
<Button onPress={onModalClose}>
Close
{translate('Close')}
</Button>
</ModalFooter>
</ModalContent>

View File

@@ -37,7 +37,10 @@ class SearchFooter extends Component {
searchingReleases: false,
searchQuery: defaultSearchQuery || '',
searchIndexerIds: defaultIndexerIds,
searchCategories: defaultCategories
searchCategories: defaultCategories,
searchLimit: 100,
searchOffset: 0,
newSearch: true
};
}
@@ -115,11 +118,28 @@ class SearchFooter extends Component {
};
onSearchPress = () => {
this.props.onSearchPress(this.state.searchQuery, this.state.searchIndexerIds, this.state.searchCategories, this.state.searchType);
const {
searchLimit,
searchOffset,
searchQuery,
searchIndexerIds,
searchCategories,
searchType
} = this.state;
this.props.onSearchPress(searchQuery, searchIndexerIds, searchCategories, searchType, searchLimit, searchOffset);
this.setState({ searchOffset: searchOffset + 100, newSearch: false });
};
onSearchInputChange = ({ value }) => {
this.setState({ searchQuery: value });
this.setState({ searchQuery: value, newSearch: true, searchOffset: 0 });
};
onInputChange = ({ name, value }) => {
this.props.onInputChange({ name, value });
this.setState({ newSearch: true, searchOffset: 0 });
};
//
@@ -141,6 +161,7 @@ class SearchFooter extends Component {
searchQuery,
searchIndexerIds,
searchCategories,
newSearch,
isQueryParameterModalOpen,
queryModalOptions,
searchType
@@ -206,7 +227,7 @@ class SearchFooter extends Component {
name='searchIndexerIds'
value={searchIndexerIds}
isDisabled={isFetching}
onChange={onInputChange}
onChange={this.onInputChange}
/>
</div>
@@ -220,7 +241,7 @@ class SearchFooter extends Component {
name='searchCategories'
value={searchCategories}
isDisabled={isFetching}
onChange={onInputChange}
onChange={this.onInputChange}
/>
</div>
@@ -243,7 +264,7 @@ class SearchFooter extends Component {
isDisabled={isFetching || !hasIndexers || selectedCount === 0}
onPress={onBulkGrabPress}
>
{translate('Grab Releases')}
{translate('GrabReleases')}
</SpinnerButton>
}
@@ -253,7 +274,7 @@ class SearchFooter extends Component {
isDisabled={isFetching || !hasIndexers}
onPress={this.onSearchPress}
>
{translate('Search')}
{newSearch ? translate('Search') : translate('More')}
</SpinnerButton>
</div>
</div>

View File

@@ -196,8 +196,8 @@ class SearchIndex extends Component {
this.setState({ jumpToCharacter });
};
onSearchPress = (query, indexerIds, categories, type) => {
this.props.onSearchPress({ query, indexerIds, categories, type });
onSearchPress = (query, indexerIds, categories, type, limit, offset) => {
this.props.onSearchPress({ query, indexerIds, categories, type, limit, offset });
};
onBulkGrabPress = () => {

View File

@@ -5,13 +5,14 @@ import createAllIndexersSelector from './createAllIndexersSelector';
function createProfileInUseSelector(profileProp) {
return createSelector(
(state, { id }) => id,
(state) => state.settings.appProfiles.items,
createAllIndexersSelector(),
(id, indexers) => {
(id, profiles, indexers) => {
if (!id) {
return false;
}
if (_.some(indexers, { [profileProp]: id })) {
if (_.some(indexers, { [profileProp]: id }) || profiles.length <= 1) {
return true;
}

View File

@@ -25,17 +25,17 @@
"not chrome < 60"
],
"dependencies": {
"@fortawesome/fontawesome-free": "5.15.3",
"@fortawesome/fontawesome-svg-core": "1.2.35",
"@fortawesome/free-regular-svg-icons": "5.15.3",
"@fortawesome/free-solid-svg-icons": "5.15.3",
"@fortawesome/react-fontawesome": "0.1.14",
"@microsoft/signalr": "6.0.0",
"@sentry/browser": "6.15.0",
"@sentry/integrations": "6.15.0",
"chart.js": "3.2.0",
"@fortawesome/fontawesome-free": "6.1.1",
"@fortawesome/fontawesome-svg-core": "6.1.1",
"@fortawesome/free-regular-svg-icons": "6.1.1",
"@fortawesome/free-solid-svg-icons": "6.1.1",
"@fortawesome/react-fontawesome": "0.1.18",
"@microsoft/signalr": "6.0.3",
"@sentry/browser": "6.19.2",
"@sentry/integrations": "6.19.2",
"chart.js": "3.7.1",
"classnames": "2.3.1",
"clipboard": "2.0.8",
"clipboard": "2.0.10",
"connected-react-router": "6.9.1",
"element-class": "0.2.2",
"filesize": "6.3.0",
@@ -45,16 +45,16 @@
"jquery": "3.6.0",
"lodash": "4.17.21",
"mobile-detect": "1.4.5",
"moment": "2.29.1",
"moment": "2.29.2",
"mousetrap": "1.6.5",
"normalize.css": "8.0.1",
"prop-types": "15.7.2",
"qs": "6.10.1",
"prop-types": "15.8.1",
"qs": "6.10.3",
"react": "17.0.2",
"react-addons-shallow-compare": "15.6.3",
"react-async-script": "1.2.0",
"react-autosuggest": "10.1.0",
"react-custom-scrollbars": "4.2.1",
"react-custom-scrollbars-2": "4.4.0",
"react-dnd": "14.0.4",
"react-dnd-html5-backend": "14.0.2",
"react-dnd-multi-backend": "6.0.2",
@@ -78,41 +78,41 @@
"reselect": "4.0.0"
},
"devDependencies": {
"@babel/core": "7.16.0",
"@babel/eslint-parser": "7.16.3",
"@babel/plugin-proposal-class-properties": "7.16.0",
"@babel/plugin-proposal-decorators": "7.16.4",
"@babel/plugin-proposal-export-default-from": "7.16.0",
"@babel/plugin-proposal-export-namespace-from": "7.16.0",
"@babel/plugin-proposal-function-sent": "7.16.0",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.16.0",
"@babel/plugin-proposal-numeric-separator": "7.16.0",
"@babel/plugin-proposal-optional-chaining": "7.16.0",
"@babel/plugin-proposal-throw-expressions": "7.16.0",
"@babel/core": "7.17.8",
"@babel/eslint-parser": "7.17.0",
"@babel/plugin-proposal-class-properties": "7.16.7",
"@babel/plugin-proposal-decorators": "7.17.8",
"@babel/plugin-proposal-export-default-from": "7.16.7",
"@babel/plugin-proposal-export-namespace-from": "7.16.7",
"@babel/plugin-proposal-function-sent": "7.16.7",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.16.7",
"@babel/plugin-proposal-numeric-separator": "7.16.7",
"@babel/plugin-proposal-optional-chaining": "7.16.7",
"@babel/plugin-proposal-throw-expressions": "7.16.7",
"@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/preset-env": "7.16.4",
"@babel/preset-react": "7.16.0",
"autoprefixer": "10.2.5",
"babel-loader": "8.2.3",
"@babel/preset-env": "7.16.11",
"@babel/preset-react": "7.16.7",
"autoprefixer": "10.4.4",
"babel-loader": "8.2.4",
"babel-plugin-inline-classnames": "2.0.1",
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
"core-js": "3.11.0",
"css-loader": "6.5.1",
"eslint": "8.3.0",
"core-js": "3.21.1",
"css-loader": "6.7.1",
"eslint": "8.11.0",
"eslint-plugin-filenames": "1.3.2",
"eslint-plugin-import": "2.25.3",
"eslint-plugin-react": "7.27.1",
"eslint-plugin-import": "2.25.4",
"eslint-plugin-react": "7.29.4",
"eslint-plugin-simple-import-sort": "7.0.0",
"esprint": "3.1.0",
"esprint": "3.3.0",
"file-loader": "6.2.0",
"filemanager-webpack-plugin": "6.1.7",
"html-webpack-plugin": "5.5.0",
"loader-utils": "^3.0.0",
"mini-css-extract-plugin": "2.4.5",
"postcss": "8.3.11",
"mini-css-extract-plugin": "2.6.0",
"postcss": "8.4.12",
"postcss-color-function": "4.1.0",
"postcss-loader": "6.2.0",
"postcss-mixins": "8.1.0",
"postcss-loader": "6.2.1",
"postcss-mixins": "9.0.2",
"postcss-nested": "5.0.6",
"postcss-simple-vars": "6.0.3",
"postcss-url": "10.1.3",
@@ -121,11 +121,11 @@
"run-sequence": "2.2.1",
"streamqueue": "1.1.2",
"style-loader": "3.3.1",
"stylelint": "14.1.0",
"stylelint": "14.6.0",
"stylelint-order": "5.0.0",
"url-loader": "4.1.1",
"webpack": "5.64.2",
"webpack-cli": "4.9.1",
"webpack": "5.70.0",
"webpack-cli": "4.9.2",
"webpack-livereload-plugin": "3.0.2"
}
}

View File

@@ -94,10 +94,10 @@
<!-- Standard testing packages -->
<ItemGroup Condition="'$(TestProject)'=='true'">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.1" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Include="NunitXml.TestLogger" Version="3.0.97" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageReference Include="NunitXml.TestLogger" Version="3.0.117" />
<PackageReference Include="coverlet.collector" Version="3.0.4-preview.27.ge7cb7c3b40" />
</ItemGroup>

View File

@@ -20,7 +20,7 @@ namespace NzbDrone.Automation.Test
public abstract class AutomationTest
{
private NzbDroneRunner _runner;
protected RemoteWebDriver driver;
protected WebDriver driver;
public AutomationTest()
{

View File

@@ -1,16 +1,15 @@
using System;
using System.Threading;
using OpenQA.Selenium;
using OpenQA.Selenium.Remote;
using OpenQA.Selenium.Support.UI;
namespace NzbDrone.Automation.Test.PageModel
{
public class PageBase
{
private readonly RemoteWebDriver _driver;
private readonly WebDriver _driver;
public PageBase(RemoteWebDriver driver)
public PageBase(WebDriver driver)
{
_driver = driver;
driver.Manage().Window.Maximize();

View File

@@ -3,8 +3,8 @@
<TargetFrameworks>net6.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Selenium.Support" Version="3.141.0" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="90.0.4430.2400" />
<PackageReference Include="Selenium.Support" Version="4.1.0" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="99.0.4844.5100" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NzbDrone.Test.Common\Prowlarr.Test.Common.csproj" />

View File

@@ -23,6 +23,7 @@ namespace NzbDrone.Common.Test.InstrumentationTests
[TestCase(@"https://hd-space.org/index.php?page=login: uid=mySecret&pwd=mySecret")]
[TestCase(@"https://beyond-hd.me/api/torrents/2b51db35e1912ffc138825a12b9933d2")]
[TestCase(@"Req: [POST] https://www3.yggtorrent.nz/user/login: id=mySecret&pass=mySecret&ci_csrf_token=2b51db35e1912ffc138825a12b9933d2")]
[TestCase(@"https://torrentseeds.org/api/torrents/filter?api_token=2b51db35e1912ffc138825a12b9933d2&name=&sortField=created_at&sortDirection=desc&perPage=100&page=1")]
//Indexer Responses

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using NzbDrone.Common.EnvironmentInfo;
@@ -24,7 +24,8 @@ namespace NzbDrone.Common.Disk
"/boot",
"/lib",
"/sbin",
"/proc"
"/proc",
"/usr/bin"
};
}
}

View File

@@ -75,7 +75,7 @@ namespace NzbDrone.Common.Http
do
{
request.Url += new HttpUri(response.Headers.GetSingleValue("Location"));
request.Url = new HttpUri(response.RedirectUrl);
autoRedirectChain.Add(request.Url.ToString());
_logger.Trace("Redirected to {0}", request.Url);

View File

@@ -9,7 +9,7 @@ namespace NzbDrone.Common.Http
{
public class HttpResponse
{
private static readonly Regex RegexSetCookie = new Regex("^(.*?)=(.*?)(?:;|$)", RegexOptions.Compiled);
private static readonly Regex RegexRefresh = new Regex("^(.*?url)=(.*?)(?:;|$)", RegexOptions.Compiled);
public HttpResponse(HttpRequest request, HttpHeader headers, CookieCollection cookies, byte[] binaryData, long elapsedTime = 0, HttpStatusCode statusCode = HttpStatusCode.OK)
{
@@ -67,7 +67,8 @@ namespace NzbDrone.Common.Http
StatusCode == HttpStatusCode.MovedPermanently ||
StatusCode == HttpStatusCode.RedirectMethod ||
StatusCode == HttpStatusCode.TemporaryRedirect ||
StatusCode == HttpStatusCode.Found;
StatusCode == HttpStatusCode.Found ||
Headers.ContainsKey("Refresh");
public string RedirectUrl
{
@@ -76,6 +77,20 @@ namespace NzbDrone.Common.Http
var newUrl = Headers["Location"];
if (newUrl == null)
{
newUrl = Headers["Refresh"];
if (newUrl == null)
{
return string.Empty;
}
var match = RegexRefresh.Match(newUrl);
if (match.Success)
{
return (Request.Url += new HttpUri(match.Groups[2].Value)).FullUri;
}
return string.Empty;
}

View File

@@ -11,7 +11,7 @@ namespace NzbDrone.Common.Instrumentation
private static readonly Regex[] CleansingRules = new[]
{
// Url
new Regex(@"(?<=[?&: ;])(apikey|(?:access[-_]?)?token|pass(?:key|wd)?|auth|authkey|user|u?id|api|[a-z_]*apikey|account|pwd)=(?<secret>[^&=]+?)(?= |&|$|<)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
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(@"(?<=[?& ;])[^=]*?(_?(?<!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/download/[0-9]+/(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),

View File

@@ -4,14 +4,14 @@
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DryIoc.dll" Version="4.8.1" />
<PackageReference Include="DryIoc.dll" Version="4.8.8" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.2" />
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="NLog" Version="4.7.9" />
<PackageReference Include="Sentry" Version="3.8.3" />
<PackageReference Include="SharpZipLib" Version="1.3.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="NLog" Version="4.7.14" />
<PackageReference Include="Sentry" Version="3.15.0" />
<PackageReference Include="SharpZipLib" Version="1.3.3" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.0" />

View File

@@ -0,0 +1,54 @@
using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Applications;
using NzbDrone.Core.Housekeeping.Housekeepers;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
{
[TestFixture]
public class CleanupOrphanedApplicationFixture : DbTest<CleanupOrphanedApplicationStatus, ApplicationStatus>
{
private ApplicationDefinition _application;
[SetUp]
public void Setup()
{
_application = Builder<ApplicationDefinition>.CreateNew()
.BuildNew();
}
private void GivenApplication()
{
Db.Insert(_application);
}
[Test]
public void should_delete_orphaned_applicationstatus()
{
var status = Builder<ApplicationStatus>.CreateNew()
.With(h => h.ProviderId = _application.Id)
.BuildNew();
Db.Insert(status);
Subject.Clean();
AllStoredModels.Should().BeEmpty();
}
[Test]
public void should_not_delete_unorphaned_applicationstatus()
{
GivenApplication();
var status = Builder<ApplicationStatus>.CreateNew()
.With(h => h.ProviderId = _application.Id)
.BuildNew();
Db.Insert(status);
Subject.Clean();
AllStoredModels.Should().HaveCount(1);
AllStoredModels.Should().Contain(h => h.ProviderId == _application.Id);
}
}
}

View File

@@ -0,0 +1,56 @@
using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.Clients.Flood;
using NzbDrone.Core.Housekeeping.Housekeepers;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
{
[TestFixture]
public class CleanupOrphanedDownloadClientStatusFixture : DbTest<CleanupOrphanedDownloadClientStatus, DownloadClientStatus>
{
private DownloadClientDefinition _downloadClient;
[SetUp]
public void Setup()
{
_downloadClient = Builder<DownloadClientDefinition>.CreateNew()
.With(c => c.Settings = new FloodSettings())
.BuildNew();
}
private void GivenClient()
{
Db.Insert(_downloadClient);
}
[Test]
public void should_delete_orphaned_downloadclientstatus()
{
var status = Builder<DownloadClientStatus>.CreateNew()
.With(h => h.ProviderId = _downloadClient.Id)
.BuildNew();
Db.Insert(status);
Subject.Clean();
AllStoredModels.Should().BeEmpty();
}
[Test]
public void should_not_delete_unorphaned_downloadclientstatus()
{
GivenClient();
var status = Builder<DownloadClientStatus>.CreateNew()
.With(h => h.ProviderId = _downloadClient.Id)
.BuildNew();
Db.Insert(status);
Subject.Clean();
AllStoredModels.Should().HaveCount(1);
AllStoredModels.Should().Contain(h => h.ProviderId == _downloadClient.Id);
}
}
}

View File

@@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FizzWare.NBuilder;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.Applications;
using NzbDrone.Core.Housekeeping.Housekeepers;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.ThingiProvider.Status;
namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
{
[TestFixture]
public class FixFutureApplicationStatusTimesFixture : CoreTest<FixFutureApplicationStatusTimes>
{
[Test]
public void should_set_disabled_till_when_its_too_far_in_the_future()
{
var disabledTillTime = EscalationBackOff.Periods[1];
var applicationStatuses = Builder<ApplicationStatus>.CreateListOfSize(5)
.All()
.With(t => t.DisabledTill = DateTime.UtcNow.AddDays(5))
.With(t => t.InitialFailure = DateTime.UtcNow.AddDays(-5))
.With(t => t.MostRecentFailure = DateTime.UtcNow.AddDays(-5))
.With(t => t.EscalationLevel = 1)
.BuildListOfNew();
Mocker.GetMock<IApplicationStatusRepository>()
.Setup(s => s.All())
.Returns(applicationStatuses);
Subject.Clean();
Mocker.GetMock<IApplicationStatusRepository>()
.Verify(v => v.UpdateMany(
It.Is<List<ApplicationStatus>>(i => i.All(
s => s.DisabledTill.Value <= DateTime.UtcNow.AddMinutes(disabledTillTime)))));
}
[Test]
public void should_set_initial_failure_when_its_in_the_future()
{
var applicationStatuses = Builder<ApplicationStatus>.CreateListOfSize(5)
.All()
.With(t => t.DisabledTill = DateTime.UtcNow.AddDays(-5))
.With(t => t.InitialFailure = DateTime.UtcNow.AddDays(5))
.With(t => t.MostRecentFailure = DateTime.UtcNow.AddDays(-5))
.With(t => t.EscalationLevel = 1)
.BuildListOfNew();
Mocker.GetMock<IApplicationStatusRepository>()
.Setup(s => s.All())
.Returns(applicationStatuses);
Subject.Clean();
Mocker.GetMock<IApplicationStatusRepository>()
.Verify(v => v.UpdateMany(
It.Is<List<ApplicationStatus>>(i => i.All(
s => s.InitialFailure.Value <= DateTime.UtcNow))));
}
[Test]
public void should_set_most_recent_failure_when_its_in_the_future()
{
var applicationStatuses = Builder<ApplicationStatus>.CreateListOfSize(5)
.All()
.With(t => t.DisabledTill = DateTime.UtcNow.AddDays(-5))
.With(t => t.InitialFailure = DateTime.UtcNow.AddDays(-5))
.With(t => t.MostRecentFailure = DateTime.UtcNow.AddDays(5))
.With(t => t.EscalationLevel = 1)
.BuildListOfNew();
Mocker.GetMock<IApplicationStatusRepository>()
.Setup(s => s.All())
.Returns(applicationStatuses);
Subject.Clean();
Mocker.GetMock<IApplicationStatusRepository>()
.Verify(v => v.UpdateMany(
It.Is<List<ApplicationStatus>>(i => i.All(
s => s.MostRecentFailure.Value <= DateTime.UtcNow))));
}
[Test]
public void should_not_change_statuses_when_times_are_in_the_past()
{
var indexerStatuses = Builder<ApplicationStatus>.CreateListOfSize(5)
.All()
.With(t => t.DisabledTill = DateTime.UtcNow.AddDays(-5))
.With(t => t.InitialFailure = DateTime.UtcNow.AddDays(-5))
.With(t => t.MostRecentFailure = DateTime.UtcNow.AddDays(-5))
.With(t => t.EscalationLevel = 0)
.BuildListOfNew();
Mocker.GetMock<IApplicationStatusRepository>()
.Setup(s => s.All())
.Returns(indexerStatuses);
Subject.Clean();
Mocker.GetMock<IApplicationStatusRepository>()
.Verify(v => v.UpdateMany(
It.Is<List<ApplicationStatus>>(i => i.Count == 0)));
}
}
}

View File

@@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FizzWare.NBuilder;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.Download;
using NzbDrone.Core.Housekeeping.Housekeepers;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.ThingiProvider.Status;
namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
{
[TestFixture]
public class FixFutureDownloadClientStatusTimesFixture : CoreTest<FixFutureDownloadClientStatusTimes>
{
[Test]
public void should_set_disabled_till_when_its_too_far_in_the_future()
{
var disabledTillTime = EscalationBackOff.Periods[1];
var clientStatuses = Builder<DownloadClientStatus>.CreateListOfSize(5)
.All()
.With(t => t.DisabledTill = DateTime.UtcNow.AddDays(5))
.With(t => t.InitialFailure = DateTime.UtcNow.AddDays(-5))
.With(t => t.MostRecentFailure = DateTime.UtcNow.AddDays(-5))
.With(t => t.EscalationLevel = 1)
.BuildListOfNew();
Mocker.GetMock<IDownloadClientStatusRepository>()
.Setup(s => s.All())
.Returns(clientStatuses);
Subject.Clean();
Mocker.GetMock<IDownloadClientStatusRepository>()
.Verify(v => v.UpdateMany(
It.Is<List<DownloadClientStatus>>(i => i.All(
s => s.DisabledTill.Value <= DateTime.UtcNow.AddMinutes(disabledTillTime)))));
}
[Test]
public void should_set_initial_failure_when_its_in_the_future()
{
var clientStatuses = Builder<DownloadClientStatus>.CreateListOfSize(5)
.All()
.With(t => t.DisabledTill = DateTime.UtcNow.AddDays(-5))
.With(t => t.InitialFailure = DateTime.UtcNow.AddDays(5))
.With(t => t.MostRecentFailure = DateTime.UtcNow.AddDays(-5))
.With(t => t.EscalationLevel = 1)
.BuildListOfNew();
Mocker.GetMock<IDownloadClientStatusRepository>()
.Setup(s => s.All())
.Returns(clientStatuses);
Subject.Clean();
Mocker.GetMock<IDownloadClientStatusRepository>()
.Verify(v => v.UpdateMany(
It.Is<List<DownloadClientStatus>>(i => i.All(
s => s.InitialFailure.Value <= DateTime.UtcNow))));
}
[Test]
public void should_set_most_recent_failure_when_its_in_the_future()
{
var clientStatuses = Builder<DownloadClientStatus>.CreateListOfSize(5)
.All()
.With(t => t.DisabledTill = DateTime.UtcNow.AddDays(-5))
.With(t => t.InitialFailure = DateTime.UtcNow.AddDays(-5))
.With(t => t.MostRecentFailure = DateTime.UtcNow.AddDays(5))
.With(t => t.EscalationLevel = 1)
.BuildListOfNew();
Mocker.GetMock<IDownloadClientStatusRepository>()
.Setup(s => s.All())
.Returns(clientStatuses);
Subject.Clean();
Mocker.GetMock<IDownloadClientStatusRepository>()
.Verify(v => v.UpdateMany(
It.Is<List<DownloadClientStatus>>(i => i.All(
s => s.MostRecentFailure.Value <= DateTime.UtcNow))));
}
[Test]
public void should_not_change_statuses_when_times_are_in_the_past()
{
var clientStatuses = Builder<DownloadClientStatus>.CreateListOfSize(5)
.All()
.With(t => t.DisabledTill = DateTime.UtcNow.AddDays(-5))
.With(t => t.InitialFailure = DateTime.UtcNow.AddDays(-5))
.With(t => t.MostRecentFailure = DateTime.UtcNow.AddDays(-5))
.With(t => t.EscalationLevel = 0)
.BuildListOfNew();
Mocker.GetMock<IDownloadClientStatusRepository>()
.Setup(s => s.All())
.Returns(clientStatuses);
Subject.Clean();
Mocker.GetMock<IDownloadClientStatusRepository>()
.Verify(v => v.UpdateMany(
It.Is<List<DownloadClientStatus>>(i => i.Count == 0)));
}
}
}

View File

@@ -3,7 +3,7 @@
<TargetFrameworks>net6.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dapper" Version="2.0.90" />
<PackageReference Include="Dapper" Version="2.0.123" />
<PackageReference Include="NBuilder" Version="6.1.0" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
<PackageReference Include="YamlDotNet" Version="11.2.1" />

View File

@@ -116,6 +116,12 @@ namespace NzbDrone.Core.Applications.Lidarr
return new ValidationFailure("ProwlarrUrl", "Prowlarr url is invalid, Lidarr cannot connect to Prowlarr");
}
if (ex.Response.StatusCode == HttpStatusCode.SeeOther)
{
_logger.Error(ex, "Lidarr returned redirect and is invalid");
return new ValidationFailure("BaseUrl", "Lidarr url is invalid, Prowlarr cannot connect to Lidarr - are you missing a url base?");
}
_logger.Error(ex, "Unable to send test message");
return new ValidationFailure("BaseUrl", "Unable to complete application test");
}

View File

@@ -116,6 +116,12 @@ namespace NzbDrone.Core.Applications.Radarr
return new ValidationFailure("ProwlarrUrl", "Prowlarr url is invalid, Radarr cannot connect to Prowlarr");
}
if (ex.Response.StatusCode == HttpStatusCode.SeeOther)
{
_logger.Error(ex, "Radarr returned redirect and is invalid");
return new ValidationFailure("BaseUrl", "Radarr url is invalid, Prowlarr cannot connect to Radarr - are you missing a url base?");
}
_logger.Error(ex, "Unable to send test message");
return new ValidationFailure("BaseUrl", "Unable to complete application test");
}

View File

@@ -116,6 +116,12 @@ namespace NzbDrone.Core.Applications.Readarr
return new ValidationFailure("ProwlarrUrl", "Prowlarr url is invalid, Readarr cannot connect to Prowlarr");
}
if (ex.Response.StatusCode == HttpStatusCode.SeeOther)
{
_logger.Error(ex, "Readarr returned redirect and is invalid");
return new ValidationFailure("BaseUrl", "Readarr url is invalid, Prowlarr cannot connect to Readarr - are you missing a url base?");
}
_logger.Error(ex, "Unable to send test message");
return new ValidationFailure("BaseUrl", "Unable to complete application test");
}

View File

@@ -116,6 +116,18 @@ namespace NzbDrone.Core.Applications.Sonarr
return new ValidationFailure("ProwlarrUrl", "Prowlarr url is invalid, Sonarr cannot connect to Prowlarr");
}
if (ex.Response.StatusCode == HttpStatusCode.SeeOther)
{
_logger.Error(ex, "Sonarr returned redirect and is invalid");
return new ValidationFailure("BaseUrl", "Sonarr url is invalid, Prowlarr cannot connect to Sonarr - are you missing a url base?");
}
if (ex.Response.StatusCode == HttpStatusCode.NotFound)
{
_logger.Error(ex, "Sonarr not found");
return new ValidationFailure("BaseUrl", "Sonarr url is invalid, Prowlarr cannot connect to Sonarr. Is Sonarr running and accessible? Sonarr v2 is not supported.");
}
_logger.Error(ex, "Unable to send test message");
return new ValidationFailure("BaseUrl", "Unable to complete application test");
}

View File

@@ -0,0 +1,179 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using FluentValidation.Results;
using Newtonsoft.Json.Linq;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers;
namespace NzbDrone.Core.Applications.Whisparr
{
public class Whisparr : ApplicationBase<WhisparrSettings>
{
public override string Name => "Whisparr";
private readonly IWhisparrV3Proxy _whisparrV3Proxy;
private readonly ICached<List<WhisparrIndexer>> _schemaCache;
private readonly IConfigFileProvider _configFileProvider;
public Whisparr(ICacheManager cacheManager, IWhisparrV3Proxy whisparrV3Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, Logger logger)
: base(appIndexerMapService, logger)
{
_schemaCache = cacheManager.GetCache<List<WhisparrIndexer>>(GetType());
_whisparrV3Proxy = whisparrV3Proxy;
_configFileProvider = configFileProvider;
}
public override ValidationResult Test()
{
var failures = new List<ValidationFailure>();
var testIndexer = new IndexerDefinition
{
Id = 0,
Name = "Test",
Protocol = DownloadProtocol.Usenet,
Capabilities = new IndexerCapabilities()
};
foreach (var cat in NewznabStandardCategory.AllCats)
{
testIndexer.Capabilities.Categories.AddCategoryMapping(1, cat);
}
try
{
failures.AddIfNotNull(_whisparrV3Proxy.TestConnection(BuildWhisparrIndexer(testIndexer, DownloadProtocol.Usenet), Settings));
}
catch (WebException ex)
{
_logger.Error(ex, "Unable to send test message");
failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Unable to complete application test, cannot connect to Whisparr"));
}
return new ValidationResult(failures);
}
public override List<AppIndexerMap> GetIndexerMappings()
{
var indexers = _whisparrV3Proxy.GetIndexers(Settings)
.Where(i => i.Implementation == "Newznab" || i.Implementation == "Torznab");
var mappings = new List<AppIndexerMap>();
foreach (var indexer in indexers)
{
if ((string)indexer.Fields.FirstOrDefault(x => x.Name == "apiKey")?.Value == _configFileProvider.ApiKey)
{
var match = AppIndexerRegex.Match((string)indexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value);
if (match.Groups["indexer"].Success && int.TryParse(match.Groups["indexer"].Value, out var indexerId))
{
//Add parsed mapping if it's mapped to a Indexer in this Prowlarr instance
mappings.Add(new AppIndexerMap { RemoteIndexerId = indexer.Id, IndexerId = indexerId });
}
}
}
return mappings;
}
public override void AddIndexer(IndexerDefinition indexer)
{
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
{
var radarrIndexer = BuildWhisparrIndexer(indexer, indexer.Protocol);
var remoteIndexer = _whisparrV3Proxy.AddIndexer(radarrIndexer, Settings);
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id });
}
}
public override void RemoveIndexer(int indexerId)
{
var appMappings = _appIndexerMapService.GetMappingsForApp(Definition.Id);
var indexerMapping = appMappings.FirstOrDefault(m => m.IndexerId == indexerId);
if (indexerMapping != null)
{
//Remove Indexer remotely and then remove the mapping
_whisparrV3Proxy.RemoveIndexer(indexerMapping.RemoteIndexerId, Settings);
_appIndexerMapService.Delete(indexerMapping.Id);
}
}
public override void UpdateIndexer(IndexerDefinition indexer)
{
_logger.Debug("Updating indexer {0} [{1}]", indexer.Name, indexer.Id);
var appMappings = _appIndexerMapService.GetMappingsForApp(Definition.Id);
var indexerMapping = appMappings.FirstOrDefault(m => m.IndexerId == indexer.Id);
var radarrIndexer = BuildWhisparrIndexer(indexer, indexer.Protocol, indexerMapping?.RemoteIndexerId ?? 0);
var remoteIndexer = _whisparrV3Proxy.GetIndexer(indexerMapping.RemoteIndexerId, Settings);
if (remoteIndexer != null)
{
_logger.Debug("Remote indexer found, syncing with current settings");
if (!radarrIndexer.Equals(remoteIndexer))
{
_whisparrV3Proxy.UpdateIndexer(radarrIndexer, Settings);
}
}
else
{
_appIndexerMapService.Delete(indexerMapping.Id);
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
{
_logger.Debug("Remote indexer not found, re-adding {0} to Whisparr", indexer.Name);
radarrIndexer.Id = 0;
var newRemoteIndexer = _whisparrV3Proxy.AddIndexer(radarrIndexer, Settings);
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = newRemoteIndexer.Id });
}
else
{
_logger.Debug("Remote indexer not found for {0}, skipping re-add to Radarr due to indexer capabilities", indexer.Name);
}
}
}
private WhisparrIndexer BuildWhisparrIndexer(IndexerDefinition indexer, DownloadProtocol protocol, int id = 0)
{
var cacheKey = $"{Settings.BaseUrl}";
var schemas = _schemaCache.Get(cacheKey, () => _whisparrV3Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
var newznab = schemas.Where(i => i.Implementation == "Newznab").First();
var torznab = schemas.Where(i => i.Implementation == "Torznab").First();
var schema = protocol == DownloadProtocol.Usenet ? newznab : torznab;
var whisparrIndexer = new WhisparrIndexer
{
Id = id,
Name = $"{indexer.Name} (Prowlarr)",
EnableRss = indexer.Enable && indexer.AppProfile.Value.EnableRss,
EnableAutomaticSearch = indexer.Enable && indexer.AppProfile.Value.EnableAutomaticSearch,
EnableInteractiveSearch = indexer.Enable && indexer.AppProfile.Value.EnableInteractiveSearch,
Priority = indexer.Priority,
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
ConfigContract = schema.ConfigContract,
Fields = schema.Fields,
};
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/";
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api";
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey;
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "categories").Value = JArray.FromObject(indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()));
return whisparrIndexer;
}
}
}

View File

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

View File

@@ -0,0 +1,44 @@
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json.Linq;
namespace NzbDrone.Core.Applications.Whisparr
{
public class WhisparrIndexer
{
public int Id { get; set; }
public bool EnableRss { get; set; }
public bool EnableAutomaticSearch { get; set; }
public bool EnableInteractiveSearch { get; set; }
public int Priority { get; set; }
public string Name { get; set; }
public string ImplementationName { get; set; }
public string Implementation { get; set; }
public string ConfigContract { get; set; }
public string InfoLink { get; set; }
public HashSet<int> Tags { get; set; }
public List<WhisparrField> Fields { get; set; }
public bool Equals(WhisparrIndexer other)
{
if (ReferenceEquals(null, other))
{
return false;
}
var baseUrl = (string)Fields.FirstOrDefault(x => x.Name == "baseUrl").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value;
var apiPath = (string)Fields.FirstOrDefault(x => x.Name == "apiPath").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiKey = (string)Fields.FirstOrDefault(x => x.Name == "apiKey").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "apiKey").Value;
var cats = JToken.DeepEquals((JArray)Fields.FirstOrDefault(x => x.Name == "categories").Value, (JArray)other.Fields.FirstOrDefault(x => x.Name == "categories").Value);
return other.EnableRss == EnableRss &&
other.EnableAutomaticSearch == EnableAutomaticSearch &&
other.EnableInteractiveSearch == EnableInteractiveSearch &&
other.Name == Name &&
other.Implementation == Implementation &&
other.Priority == Priority &&
other.Id == Id &&
apiKey && apiPath && baseUrl && cats;
}
}
}

View File

@@ -0,0 +1,46 @@
using System.Collections.Generic;
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Applications.Whisparr
{
public class WhisparrSettingsValidator : AbstractValidator<WhisparrSettings>
{
public WhisparrSettingsValidator()
{
RuleFor(c => c.BaseUrl).IsValidUrl();
RuleFor(c => c.ProwlarrUrl).IsValidUrl();
RuleFor(c => c.ApiKey).NotEmpty();
RuleFor(c => c.SyncCategories).NotEmpty();
}
}
public class WhisparrSettings : IApplicationSettings
{
private static readonly WhisparrSettingsValidator Validator = new WhisparrSettingsValidator();
public WhisparrSettings()
{
SyncCategories = new[] { 6000, 6010, 6020, 6030, 6040, 6045, 6050, 6070, 6080, 6090 };
}
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Whisparr sees it, including http(s)://, port, and urlbase if needed", Placeholder = "http://localhost:9696")]
public string ProwlarrUrl { get; set; }
[FieldDefinition(1, Label = "Whisparr Server", HelpText = "URL used to connect to Whisparr server, including http(s)://, port, and urlbase if required", Placeholder = "http://localhost:6969")]
public string BaseUrl { get; set; }
[FieldDefinition(2, Label = "ApiKey", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by Whisparr in Settings/General")]
public string ApiKey { get; set; }
[FieldDefinition(3, Label = "Sync Categories", Type = FieldType.Select, SelectOptions = typeof(NewznabCategoryFieldConverter), Advanced = true, HelpText = "Only Indexers that support these categories will be synced")]
public IEnumerable<int> SyncCategories { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}

View File

@@ -0,0 +1,7 @@
namespace NzbDrone.Core.Applications.Whisparr
{
public class WhisparrStatus
{
public string Version { get; set; }
}
}

View File

@@ -0,0 +1,163 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using FluentValidation.Results;
using Newtonsoft.Json;
using NLog;
using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer;
namespace NzbDrone.Core.Applications.Whisparr
{
public interface IWhisparrV3Proxy
{
WhisparrIndexer AddIndexer(WhisparrIndexer indexer, WhisparrSettings settings);
List<WhisparrIndexer> GetIndexers(WhisparrSettings settings);
WhisparrIndexer GetIndexer(int indexerId, WhisparrSettings settings);
List<WhisparrIndexer> GetIndexerSchema(WhisparrSettings settings);
void RemoveIndexer(int indexerId, WhisparrSettings settings);
WhisparrIndexer UpdateIndexer(WhisparrIndexer indexer, WhisparrSettings settings);
ValidationFailure TestConnection(WhisparrIndexer indexer, WhisparrSettings settings);
}
public class WhisparrV3Proxy : IWhisparrV3Proxy
{
private readonly IHttpClient _httpClient;
private readonly Logger _logger;
public WhisparrV3Proxy(IHttpClient httpClient, Logger logger)
{
_httpClient = httpClient;
_logger = logger;
}
public WhisparrStatus GetStatus(WhisparrSettings settings)
{
var request = BuildRequest(settings, "/api/v3/system/status", HttpMethod.Get);
return Execute<WhisparrStatus>(request);
}
public List<WhisparrIndexer> GetIndexers(WhisparrSettings settings)
{
var request = BuildRequest(settings, "/api/v3/indexer", HttpMethod.Get);
return Execute<List<WhisparrIndexer>>(request);
}
public WhisparrIndexer GetIndexer(int indexerId, WhisparrSettings settings)
{
try
{
var request = BuildRequest(settings, $"/api/v3/indexer/{indexerId}", HttpMethod.Get);
return Execute<WhisparrIndexer>(request);
}
catch (HttpException ex)
{
if (ex.Response.StatusCode != HttpStatusCode.NotFound)
{
throw;
}
}
return null;
}
public void RemoveIndexer(int indexerId, WhisparrSettings settings)
{
var request = BuildRequest(settings, $"/api/v3/indexer/{indexerId}", HttpMethod.Delete);
_httpClient.Execute(request);
}
public List<WhisparrIndexer> GetIndexerSchema(WhisparrSettings settings)
{
var request = BuildRequest(settings, "/api/v3/indexer/schema", HttpMethod.Get);
return Execute<List<WhisparrIndexer>>(request);
}
public WhisparrIndexer AddIndexer(WhisparrIndexer indexer, WhisparrSettings settings)
{
var request = BuildRequest(settings, "/api/v3/indexer", HttpMethod.Post);
request.SetContent(indexer.ToJson());
return Execute<WhisparrIndexer>(request);
}
public WhisparrIndexer UpdateIndexer(WhisparrIndexer indexer, WhisparrSettings settings)
{
var request = BuildRequest(settings, $"/api/v3/indexer/{indexer.Id}", HttpMethod.Put);
request.SetContent(indexer.ToJson());
return Execute<WhisparrIndexer>(request);
}
public ValidationFailure TestConnection(WhisparrIndexer indexer, WhisparrSettings settings)
{
var request = BuildRequest(settings, $"/api/v3/indexer/test", HttpMethod.Post);
request.SetContent(indexer.ToJson());
try
{
Execute<WhisparrIndexer>(request);
}
catch (HttpException ex)
{
if (ex.Response.StatusCode == HttpStatusCode.Unauthorized)
{
_logger.Error(ex, "API Key is invalid");
return new ValidationFailure("ApiKey", "API Key is invalid");
}
if (ex.Response.StatusCode == HttpStatusCode.BadRequest)
{
_logger.Error(ex, "Prowlarr URL is invalid");
return new ValidationFailure("ProwlarrUrl", "Prowlarr url is invalid, Whisparr cannot connect to Prowlarr");
}
if (ex.Response.StatusCode == HttpStatusCode.SeeOther)
{
_logger.Error(ex, "Whisparr returned redirect and is invalid");
return new ValidationFailure("BaseUrl", "Whisparr url is invalid, Prowlarr cannot connect to Whisparr - are you missing a url base?");
}
_logger.Error(ex, "Unable to send test message");
return new ValidationFailure("BaseUrl", "Unable to complete application test");
}
catch (Exception ex)
{
_logger.Error(ex, "Unable to send test message");
return new ValidationFailure("", "Unable to send test message");
}
return null;
}
private HttpRequest BuildRequest(WhisparrSettings settings, string resource, HttpMethod method)
{
var baseUrl = settings.BaseUrl.TrimEnd('/');
var request = new HttpRequestBuilder(baseUrl).Resource(resource)
.SetHeader("X-Api-Key", settings.ApiKey)
.Build();
request.Headers.ContentType = "application/json";
request.Method = method;
request.AllowAutoRedirect = true;
return request;
}
private TResource Execute<TResource>(HttpRequest request)
where TResource : new()
{
var response = _httpClient.Execute(request);
var results = JsonConvert.DeserializeObject<TResource>(response.Content);
return results;
}
}
}

View File

@@ -90,6 +90,8 @@ namespace NzbDrone.Core.Backup
_archiveService.CreateZip(backupPath, _diskProvider.GetFiles(_backupTempFolder, SearchOption.TopDirectoryOnly));
Cleanup();
_logger.ProgressDebug("Backup zip created");
}

View File

@@ -66,7 +66,7 @@ namespace NzbDrone.Core.Datastore
_updateSql = GetUpdateSql(_properties);
}
protected virtual SqlBuilder Builder() => new SqlBuilder();
protected virtual SqlBuilder Builder() => new SqlBuilder(_database.DatabaseType);
protected virtual List<TModel> Query(SqlBuilder builder) => _database.Query<TModel>(builder).ToList();

View File

@@ -0,0 +1,19 @@
using System;
using System.Data;
using Dapper;
namespace NzbDrone.Core.Datastore.Converters
{
public class DapperTimeSpanConverter : SqlMapper.TypeHandler<TimeSpan>
{
public override void SetValue(IDbDataParameter parameter, TimeSpan value)
{
parameter.Value = value.ToString();
}
public override TimeSpan Parse(object value)
{
return TimeSpan.Parse((string)value);
}
}
}

View File

@@ -42,21 +42,21 @@ namespace NzbDrone.Core.Datastore
public static SqlBuilder Where<TModel>(this SqlBuilder builder, Expression<Func<TModel, bool>> filter)
{
var wb = new WhereBuilder(filter, true, builder.Sequence);
var wb = GetWhereBuilder(builder.DatabaseType, filter, true, builder.Sequence);
return builder.Where(wb.ToString(), wb.Parameters);
}
public static SqlBuilder OrWhere<TModel>(this SqlBuilder builder, Expression<Func<TModel, bool>> filter)
{
var wb = new WhereBuilder(filter, true, builder.Sequence);
var wb = GetWhereBuilder(builder.DatabaseType, filter, true, builder.Sequence);
return builder.OrWhere(wb.ToString(), wb.Parameters);
}
public static SqlBuilder Join<TLeft, TRight>(this SqlBuilder builder, Expression<Func<TLeft, TRight, bool>> filter)
{
var wb = new WhereBuilder(filter, false, builder.Sequence);
var wb = GetWhereBuilder(builder.DatabaseType, filter, false, builder.Sequence);
var rightTable = TableMapping.Mapper.TableNameMapping(typeof(TRight));
@@ -65,7 +65,7 @@ namespace NzbDrone.Core.Datastore
public static SqlBuilder LeftJoin<TLeft, TRight>(this SqlBuilder builder, Expression<Func<TLeft, TRight, bool>> filter)
{
var wb = new WhereBuilder(filter, false, builder.Sequence);
var wb = GetWhereBuilder(builder.DatabaseType, filter, false, builder.Sequence);
var rightTable = TableMapping.Mapper.TableNameMapping(typeof(TRight));
@@ -138,6 +138,18 @@ namespace NzbDrone.Core.Datastore
return sb.ToString();
}
private static WhereBuilder GetWhereBuilder(DatabaseType databaseType, Expression filter, bool requireConcrete, int seq)
{
if (databaseType == DatabaseType.PostgreSQL)
{
return new WhereBuilderPostgres(filter, requireConcrete, seq);
}
else
{
return new WhereBuilderSqlite(filter, requireConcrete, seq);
}
}
private static Dictionary<string, object> ToDictionary(this DynamicParameters dynamicParams)
{
var argsDictionary = new Dictionary<string, object>();

View File

@@ -0,0 +1,18 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(15)]
public class IndexerVersions : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Create.TableForModel("IndexerDefinitionVersions")
.WithColumn("DefinitionId").AsString().NotNullable().Unique()
.WithColumn("File").AsString().NotNullable().Unique()
.WithColumn("Sha").AsString().Nullable()
.WithColumn("LastUpdated").AsDateTime().Nullable();
}
}
}

View File

@@ -8,9 +8,17 @@ namespace NzbDrone.Core.Datastore
public class SqlBuilder
{
private readonly Dictionary<string, Clauses> _data = new Dictionary<string, Clauses>();
private readonly DatabaseType _databaseType;
public SqlBuilder(DatabaseType databaseType)
{
_databaseType = databaseType;
}
public int Sequence { get; private set; }
public DatabaseType DatabaseType => _databaseType;
public Template AddTemplate(string sql, dynamic parameters = null) =>
new Template(this, sql, parameters);

View File

@@ -183,7 +183,7 @@ namespace NzbDrone.Core.Datastore
(db, parent) =>
{
var id = childIdSelector(parent);
return db.Query<TChild>(new SqlBuilder().Where<TChild>(x => x.Id == id)).SingleOrDefault();
return db.Query<TChild>(new SqlBuilder(db.DatabaseType).Where<TChild>(x => x.Id == id)).SingleOrDefault();
},
parent => childIdSelector(parent) > 0);
}

View File

@@ -11,6 +11,7 @@ using NzbDrone.Core.Datastore.Converters;
using NzbDrone.Core.Download;
using NzbDrone.Core.IndexerProxies;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.IndexerVersions;
using NzbDrone.Core.Instrumentation;
using NzbDrone.Core.Jobs;
using NzbDrone.Core.Languages;
@@ -94,6 +95,7 @@ namespace NzbDrone.Core.Datastore
Mapper.Entity<UpdateHistory>("UpdateHistory").RegisterModel();
Mapper.Entity<AppSyncProfile>("AppSyncProfiles").RegisterModel();
Mapper.Entity<IndexerDefinitionVersion>("IndexerDefinitionVersions").RegisterModel();
}
private static void RegisterMappers()
@@ -103,6 +105,7 @@ namespace NzbDrone.Core.Datastore
SqlMapper.RemoveTypeMap(typeof(DateTime));
SqlMapper.AddTypeHandler(new DapperUtcConverter());
SqlMapper.AddTypeHandler(new DapperTimeSpanConverter());
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<Dictionary<string, string>>());
SqlMapper.AddTypeHandler(new CookieConverter());
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<int>>());

View File

@@ -1,389 +1,9 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using Dapper;
namespace NzbDrone.Core.Datastore
{
public class WhereBuilder : ExpressionVisitor
public abstract class WhereBuilder : ExpressionVisitor
{
protected StringBuilder _sb;
private const DbType EnumerableMultiParameter = (DbType)(-1);
private readonly string _paramNamePrefix;
private readonly bool _requireConcreteValue = false;
private int _paramCount = 0;
private bool _gotConcreteValue = false;
public WhereBuilder(Expression filter, bool requireConcreteValue, int seq)
{
_paramNamePrefix = string.Format("Clause{0}", seq + 1);
_requireConcreteValue = requireConcreteValue;
_sb = new StringBuilder();
Parameters = new DynamicParameters();
if (filter != null)
{
Visit(filter);
}
}
public DynamicParameters Parameters { get; private set; }
private string AddParameter(object value, DbType? dbType = null)
{
_gotConcreteValue = true;
_paramCount++;
var name = _paramNamePrefix + "_P" + _paramCount;
Parameters.Add(name, value, dbType);
return '@' + name;
}
protected override Expression VisitBinary(BinaryExpression expression)
{
_sb.Append('(');
Visit(expression.Left);
_sb.AppendFormat(" {0} ", Decode(expression));
Visit(expression.Right);
_sb.Append(')');
return expression;
}
protected override Expression VisitMethodCall(MethodCallExpression expression)
{
var method = expression.Method.Name;
switch (expression.Method.Name)
{
case "Contains":
ParseContainsExpression(expression);
break;
case "StartsWith":
ParseStartsWith(expression);
break;
case "EndsWith":
ParseEndsWith(expression);
break;
default:
var msg = string.Format("'{0}' expressions are not yet implemented in the where clause expression tree parser.", method);
throw new NotImplementedException(msg);
}
return expression;
}
protected override Expression VisitMemberAccess(MemberExpression expression)
{
var tableName = expression?.Expression?.Type != null ? TableMapping.Mapper.TableNameMapping(expression.Expression.Type) : null;
var gotValue = TryGetRightValue(expression, out var value);
// Only use the SQL condition if the expression didn't resolve to an actual value
if (tableName != null && !gotValue)
{
_sb.Append($"\"{tableName}\".\"{expression.Member.Name}\"");
}
else
{
if (value != null)
{
// string is IEnumerable<Char> but we don't want to pick up that case
var type = value.GetType();
var typeInfo = type.GetTypeInfo();
var isEnumerable =
type != typeof(string) && (
typeInfo.ImplementedInterfaces.Any(ti => ti.IsGenericType && ti.GetGenericTypeDefinition() == typeof(IEnumerable<>)) ||
(typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(IEnumerable<>)));
var paramName = isEnumerable ? AddParameter(value, EnumerableMultiParameter) : AddParameter(value);
_sb.Append(paramName);
}
else
{
_gotConcreteValue = true;
_sb.Append("NULL");
}
}
return expression;
}
protected override Expression VisitConstant(ConstantExpression expression)
{
if (expression.Value != null)
{
var paramName = AddParameter(expression.Value);
_sb.Append(paramName);
}
else
{
_gotConcreteValue = true;
_sb.Append("NULL");
}
return expression;
}
private bool TryGetConstantValue(Expression expression, out object result)
{
result = null;
if (expression is ConstantExpression constExp)
{
result = constExp.Value;
return true;
}
return false;
}
private bool TryGetPropertyValue(MemberExpression expression, out object result)
{
result = null;
if (expression.Expression is MemberExpression nested)
{
// Value is passed in as a property on a parent entity
var container = (nested.Expression as ConstantExpression)?.Value;
if (container == null)
{
return false;
}
var entity = GetFieldValue(container, nested.Member);
result = GetFieldValue(entity, expression.Member);
return true;
}
return false;
}
private bool TryGetVariableValue(MemberExpression expression, out object result)
{
result = null;
// Value is passed in as a variable
if (expression.Expression is ConstantExpression nested)
{
result = GetFieldValue(nested.Value, expression.Member);
return true;
}
return false;
}
private bool TryGetRightValue(Expression expression, out object value)
{
value = null;
if (TryGetConstantValue(expression, out value))
{
return true;
}
var memberExp = expression as MemberExpression;
if (TryGetPropertyValue(memberExp, out value))
{
return true;
}
if (TryGetVariableValue(memberExp, out value))
{
return true;
}
return false;
}
private object GetFieldValue(object entity, MemberInfo member)
{
if (member.MemberType == MemberTypes.Field)
{
return (member as FieldInfo).GetValue(entity);
}
if (member.MemberType == MemberTypes.Property)
{
return (member as PropertyInfo).GetValue(entity);
}
throw new ArgumentException(string.Format("WhereBuilder could not get the value for {0}.{1}.", entity.GetType().Name, member.Name));
}
private bool IsNullVariable(Expression expression)
{
if (expression.NodeType == ExpressionType.Constant &&
TryGetConstantValue(expression, out var constResult) &&
constResult == null)
{
return true;
}
if (expression.NodeType == ExpressionType.MemberAccess &&
expression is MemberExpression member &&
((TryGetPropertyValue(member, out var result) && result == null) ||
(TryGetVariableValue(member, out result) && result == null)))
{
return true;
}
return false;
}
private string Decode(BinaryExpression expression)
{
if (IsNullVariable(expression.Right))
{
switch (expression.NodeType)
{
case ExpressionType.Equal: return "IS";
case ExpressionType.NotEqual: return "IS NOT";
}
}
switch (expression.NodeType)
{
case ExpressionType.AndAlso: return "AND";
case ExpressionType.And: return "AND";
case ExpressionType.Equal: return "=";
case ExpressionType.GreaterThan: return ">";
case ExpressionType.GreaterThanOrEqual: return ">=";
case ExpressionType.LessThan: return "<";
case ExpressionType.LessThanOrEqual: return "<=";
case ExpressionType.NotEqual: return "<>";
case ExpressionType.OrElse: return "OR";
case ExpressionType.Or: return "OR";
default: throw new NotSupportedException(string.Format("{0} statement is not supported", expression.NodeType.ToString()));
}
}
private void ParseContainsExpression(MethodCallExpression expression)
{
var list = expression.Object;
if (list != null && (list.Type == typeof(string)))
{
ParseStringContains(expression);
return;
}
ParseEnumerableContains(expression);
}
private void ParseEnumerableContains(MethodCallExpression body)
{
// Fish out the list and the item to compare
// It's in a different form for arrays and Lists
var list = body.Object;
Expression item;
if (list != null)
{
// Generic collection
item = body.Arguments[0];
}
else
{
// Static method
// Must be Enumerable.Contains(source, item)
if (body.Method.DeclaringType != typeof(Enumerable) || body.Arguments.Count != 2)
{
throw new NotSupportedException("Unexpected form of Enumerable.Contains");
}
list = body.Arguments[0];
item = body.Arguments[1];
}
_sb.Append('(');
Visit(item);
_sb.Append(" IN ");
// hardcode the integer list if it exists to bypass parameter limit
if (item.Type == typeof(int) && TryGetRightValue(list, out var value))
{
var items = (IEnumerable<int>)value;
_sb.Append('(');
_sb.Append(string.Join(", ", items));
_sb.Append(')');
_gotConcreteValue = true;
}
else
{
Visit(list);
}
_sb.Append(')');
}
private void ParseStringContains(MethodCallExpression body)
{
_sb.Append('(');
Visit(body.Object);
_sb.Append(" LIKE '%' || ");
Visit(body.Arguments[0]);
_sb.Append(" || '%')");
}
private void ParseStartsWith(MethodCallExpression body)
{
_sb.Append('(');
Visit(body.Object);
_sb.Append(" LIKE ");
Visit(body.Arguments[0]);
_sb.Append(" || '%')");
}
private void ParseEndsWith(MethodCallExpression body)
{
_sb.Append('(');
Visit(body.Object);
_sb.Append(" LIKE '%' || ");
Visit(body.Arguments[0]);
_sb.Append(')');
}
public override string ToString()
{
var sql = _sb.ToString();
if (_requireConcreteValue && !_gotConcreteValue)
{
var e = new InvalidOperationException("WhereBuilder requires a concrete condition");
e.Data.Add("sql", sql);
throw e;
}
return sql;
}
public DynamicParameters Parameters { get; protected set; }
}
}

View File

@@ -0,0 +1,374 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using Dapper;
namespace NzbDrone.Core.Datastore
{
public class WhereBuilderPostgres : WhereBuilder
{
protected StringBuilder _sb;
private const DbType EnumerableMultiParameter = (DbType)(-1);
private readonly string _paramNamePrefix;
private readonly bool _requireConcreteValue = false;
private int _paramCount = 0;
private bool _gotConcreteValue = false;
public WhereBuilderPostgres(Expression filter, bool requireConcreteValue, int seq)
{
_paramNamePrefix = string.Format("Clause{0}", seq + 1);
_requireConcreteValue = requireConcreteValue;
_sb = new StringBuilder();
Parameters = new DynamicParameters();
if (filter != null)
{
Visit(filter);
}
}
private string AddParameter(object value, DbType? dbType = null)
{
_gotConcreteValue = true;
_paramCount++;
var name = _paramNamePrefix + "_P" + _paramCount;
Parameters.Add(name, value, dbType);
return '@' + name;
}
protected override Expression VisitBinary(BinaryExpression expression)
{
_sb.Append('(');
Visit(expression.Left);
_sb.AppendFormat(" {0} ", Decode(expression));
Visit(expression.Right);
_sb.Append(')');
return expression;
}
protected override Expression VisitMethodCall(MethodCallExpression expression)
{
var method = expression.Method.Name;
switch (expression.Method.Name)
{
case "Contains":
ParseContainsExpression(expression);
break;
case "StartsWith":
ParseStartsWith(expression);
break;
case "EndsWith":
ParseEndsWith(expression);
break;
default:
var msg = string.Format("'{0}' expressions are not yet implemented in the where clause expression tree parser.", method);
throw new NotImplementedException(msg);
}
return expression;
}
protected override Expression VisitMemberAccess(MemberExpression expression)
{
var tableName = expression?.Expression?.Type != null ? TableMapping.Mapper.TableNameMapping(expression.Expression.Type) : null;
var gotValue = TryGetRightValue(expression, out var value);
// Only use the SQL condition if the expression didn't resolve to an actual value
if (tableName != null && !gotValue)
{
_sb.Append($"\"{tableName}\".\"{expression.Member.Name}\"");
}
else
{
if (value != null)
{
// string is IEnumerable<Char> but we don't want to pick up that case
var type = value.GetType();
var typeInfo = type.GetTypeInfo();
var isEnumerable =
type != typeof(string) && (
typeInfo.ImplementedInterfaces.Any(ti => ti.IsGenericType && ti.GetGenericTypeDefinition() == typeof(IEnumerable<>)) ||
(typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(IEnumerable<>)));
var paramName = isEnumerable ? AddParameter(value, EnumerableMultiParameter) : AddParameter(value);
_sb.Append(paramName);
}
else
{
_gotConcreteValue = true;
_sb.Append("NULL");
}
}
return expression;
}
protected override Expression VisitConstant(ConstantExpression expression)
{
if (expression.Value != null)
{
var paramName = AddParameter(expression.Value);
_sb.Append(paramName);
}
else
{
_gotConcreteValue = true;
_sb.Append("NULL");
}
return expression;
}
private bool TryGetConstantValue(Expression expression, out object result)
{
result = null;
if (expression is ConstantExpression constExp)
{
result = constExp.Value;
return true;
}
return false;
}
private bool TryGetPropertyValue(MemberExpression expression, out object result)
{
result = null;
if (expression.Expression is MemberExpression nested)
{
// Value is passed in as a property on a parent entity
var container = (nested.Expression as ConstantExpression)?.Value;
if (container == null)
{
return false;
}
var entity = GetFieldValue(container, nested.Member);
result = GetFieldValue(entity, expression.Member);
return true;
}
return false;
}
private bool TryGetVariableValue(MemberExpression expression, out object result)
{
result = null;
// Value is passed in as a variable
if (expression.Expression is ConstantExpression nested)
{
result = GetFieldValue(nested.Value, expression.Member);
return true;
}
return false;
}
private bool TryGetRightValue(Expression expression, out object value)
{
value = null;
if (TryGetConstantValue(expression, out value))
{
return true;
}
var memberExp = expression as MemberExpression;
if (TryGetPropertyValue(memberExp, out value))
{
return true;
}
if (TryGetVariableValue(memberExp, out value))
{
return true;
}
return false;
}
private object GetFieldValue(object entity, MemberInfo member)
{
if (member.MemberType == MemberTypes.Field)
{
return (member as FieldInfo).GetValue(entity);
}
if (member.MemberType == MemberTypes.Property)
{
return (member as PropertyInfo).GetValue(entity);
}
throw new ArgumentException(string.Format("WhereBuilder could not get the value for {0}.{1}.", entity.GetType().Name, member.Name));
}
private bool IsNullVariable(Expression expression)
{
if (expression.NodeType == ExpressionType.Constant &&
TryGetConstantValue(expression, out var constResult) &&
constResult == null)
{
return true;
}
if (expression.NodeType == ExpressionType.MemberAccess &&
expression is MemberExpression member &&
((TryGetPropertyValue(member, out var result) && result == null) ||
(TryGetVariableValue(member, out result) && result == null)))
{
return true;
}
return false;
}
private string Decode(BinaryExpression expression)
{
if (IsNullVariable(expression.Right))
{
switch (expression.NodeType)
{
case ExpressionType.Equal: return "IS";
case ExpressionType.NotEqual: return "IS NOT";
}
}
switch (expression.NodeType)
{
case ExpressionType.AndAlso: return "AND";
case ExpressionType.And: return "AND";
case ExpressionType.Equal: return "=";
case ExpressionType.GreaterThan: return ">";
case ExpressionType.GreaterThanOrEqual: return ">=";
case ExpressionType.LessThan: return "<";
case ExpressionType.LessThanOrEqual: return "<=";
case ExpressionType.NotEqual: return "<>";
case ExpressionType.OrElse: return "OR";
case ExpressionType.Or: return "OR";
default: throw new NotSupportedException(string.Format("{0} statement is not supported", expression.NodeType.ToString()));
}
}
private void ParseContainsExpression(MethodCallExpression expression)
{
var list = expression.Object;
if (list != null && (list.Type == typeof(string)))
{
ParseStringContains(expression);
return;
}
ParseEnumerableContains(expression);
}
private void ParseEnumerableContains(MethodCallExpression body)
{
// Fish out the list and the item to compare
// It's in a different form for arrays and Lists
var list = body.Object;
Expression item;
if (list != null)
{
// Generic collection
item = body.Arguments[0];
}
else
{
// Static method
// Must be Enumerable.Contains(source, item)
if (body.Method.DeclaringType != typeof(Enumerable) || body.Arguments.Count != 2)
{
throw new NotSupportedException("Unexpected form of Enumerable.Contains");
}
list = body.Arguments[0];
item = body.Arguments[1];
}
_sb.Append('(');
Visit(item);
_sb.Append(" = ANY (");
Visit(list);
_sb.Append("))");
}
private void ParseStringContains(MethodCallExpression body)
{
_sb.Append('(');
Visit(body.Object);
_sb.Append(" LIKE '%' || ");
Visit(body.Arguments[0]);
_sb.Append(" || '%')");
}
private void ParseStartsWith(MethodCallExpression body)
{
_sb.Append('(');
Visit(body.Object);
_sb.Append(" LIKE ");
Visit(body.Arguments[0]);
_sb.Append(" || '%')");
}
private void ParseEndsWith(MethodCallExpression body)
{
_sb.Append('(');
Visit(body.Object);
_sb.Append(" LIKE '%' || ");
Visit(body.Arguments[0]);
_sb.Append(')');
}
public override string ToString()
{
var sql = _sb.ToString();
if (_requireConcreteValue && !_gotConcreteValue)
{
var e = new InvalidOperationException("WhereBuilder requires a concrete condition");
e.Data.Add("sql", sql);
throw e;
}
return sql;
}
}
}

View File

@@ -0,0 +1,387 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using Dapper;
namespace NzbDrone.Core.Datastore
{
public class WhereBuilderSqlite : WhereBuilder
{
protected StringBuilder _sb;
private const DbType EnumerableMultiParameter = (DbType)(-1);
private readonly string _paramNamePrefix;
private readonly bool _requireConcreteValue = false;
private int _paramCount = 0;
private bool _gotConcreteValue = false;
public WhereBuilderSqlite(Expression filter, bool requireConcreteValue, int seq)
{
_paramNamePrefix = string.Format("Clause{0}", seq + 1);
_requireConcreteValue = requireConcreteValue;
_sb = new StringBuilder();
Parameters = new DynamicParameters();
if (filter != null)
{
Visit(filter);
}
}
private string AddParameter(object value, DbType? dbType = null)
{
_gotConcreteValue = true;
_paramCount++;
var name = _paramNamePrefix + "_P" + _paramCount;
Parameters.Add(name, value, dbType);
return '@' + name;
}
protected override Expression VisitBinary(BinaryExpression expression)
{
_sb.Append('(');
Visit(expression.Left);
_sb.AppendFormat(" {0} ", Decode(expression));
Visit(expression.Right);
_sb.Append(')');
return expression;
}
protected override Expression VisitMethodCall(MethodCallExpression expression)
{
var method = expression.Method.Name;
switch (expression.Method.Name)
{
case "Contains":
ParseContainsExpression(expression);
break;
case "StartsWith":
ParseStartsWith(expression);
break;
case "EndsWith":
ParseEndsWith(expression);
break;
default:
var msg = string.Format("'{0}' expressions are not yet implemented in the where clause expression tree parser.", method);
throw new NotImplementedException(msg);
}
return expression;
}
protected override Expression VisitMemberAccess(MemberExpression expression)
{
var tableName = expression?.Expression?.Type != null ? TableMapping.Mapper.TableNameMapping(expression.Expression.Type) : null;
var gotValue = TryGetRightValue(expression, out var value);
// Only use the SQL condition if the expression didn't resolve to an actual value
if (tableName != null && !gotValue)
{
_sb.Append($"\"{tableName}\".\"{expression.Member.Name}\"");
}
else
{
if (value != null)
{
// string is IEnumerable<Char> but we don't want to pick up that case
var type = value.GetType();
var typeInfo = type.GetTypeInfo();
var isEnumerable =
type != typeof(string) && (
typeInfo.ImplementedInterfaces.Any(ti => ti.IsGenericType && ti.GetGenericTypeDefinition() == typeof(IEnumerable<>)) ||
(typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(IEnumerable<>)));
var paramName = isEnumerable ? AddParameter(value, EnumerableMultiParameter) : AddParameter(value);
_sb.Append(paramName);
}
else
{
_gotConcreteValue = true;
_sb.Append("NULL");
}
}
return expression;
}
protected override Expression VisitConstant(ConstantExpression expression)
{
if (expression.Value != null)
{
var paramName = AddParameter(expression.Value);
_sb.Append(paramName);
}
else
{
_gotConcreteValue = true;
_sb.Append("NULL");
}
return expression;
}
private bool TryGetConstantValue(Expression expression, out object result)
{
result = null;
if (expression is ConstantExpression constExp)
{
result = constExp.Value;
return true;
}
return false;
}
private bool TryGetPropertyValue(MemberExpression expression, out object result)
{
result = null;
if (expression.Expression is MemberExpression nested)
{
// Value is passed in as a property on a parent entity
var container = (nested.Expression as ConstantExpression)?.Value;
if (container == null)
{
return false;
}
var entity = GetFieldValue(container, nested.Member);
result = GetFieldValue(entity, expression.Member);
return true;
}
return false;
}
private bool TryGetVariableValue(MemberExpression expression, out object result)
{
result = null;
// Value is passed in as a variable
if (expression.Expression is ConstantExpression nested)
{
result = GetFieldValue(nested.Value, expression.Member);
return true;
}
return false;
}
private bool TryGetRightValue(Expression expression, out object value)
{
value = null;
if (TryGetConstantValue(expression, out value))
{
return true;
}
var memberExp = expression as MemberExpression;
if (TryGetPropertyValue(memberExp, out value))
{
return true;
}
if (TryGetVariableValue(memberExp, out value))
{
return true;
}
return false;
}
private object GetFieldValue(object entity, MemberInfo member)
{
if (member.MemberType == MemberTypes.Field)
{
return (member as FieldInfo).GetValue(entity);
}
if (member.MemberType == MemberTypes.Property)
{
return (member as PropertyInfo).GetValue(entity);
}
throw new ArgumentException(string.Format("WhereBuilder could not get the value for {0}.{1}.", entity.GetType().Name, member.Name));
}
private bool IsNullVariable(Expression expression)
{
if (expression.NodeType == ExpressionType.Constant &&
TryGetConstantValue(expression, out var constResult) &&
constResult == null)
{
return true;
}
if (expression.NodeType == ExpressionType.MemberAccess &&
expression is MemberExpression member &&
((TryGetPropertyValue(member, out var result) && result == null) ||
(TryGetVariableValue(member, out result) && result == null)))
{
return true;
}
return false;
}
private string Decode(BinaryExpression expression)
{
if (IsNullVariable(expression.Right))
{
switch (expression.NodeType)
{
case ExpressionType.Equal: return "IS";
case ExpressionType.NotEqual: return "IS NOT";
}
}
switch (expression.NodeType)
{
case ExpressionType.AndAlso: return "AND";
case ExpressionType.And: return "AND";
case ExpressionType.Equal: return "=";
case ExpressionType.GreaterThan: return ">";
case ExpressionType.GreaterThanOrEqual: return ">=";
case ExpressionType.LessThan: return "<";
case ExpressionType.LessThanOrEqual: return "<=";
case ExpressionType.NotEqual: return "<>";
case ExpressionType.OrElse: return "OR";
case ExpressionType.Or: return "OR";
default: throw new NotSupportedException(string.Format("{0} statement is not supported", expression.NodeType.ToString()));
}
}
private void ParseContainsExpression(MethodCallExpression expression)
{
var list = expression.Object;
if (list != null && (list.Type == typeof(string)))
{
ParseStringContains(expression);
return;
}
ParseEnumerableContains(expression);
}
private void ParseEnumerableContains(MethodCallExpression body)
{
// Fish out the list and the item to compare
// It's in a different form for arrays and Lists
var list = body.Object;
Expression item;
if (list != null)
{
// Generic collection
item = body.Arguments[0];
}
else
{
// Static method
// Must be Enumerable.Contains(source, item)
if (body.Method.DeclaringType != typeof(Enumerable) || body.Arguments.Count != 2)
{
throw new NotSupportedException("Unexpected form of Enumerable.Contains");
}
list = body.Arguments[0];
item = body.Arguments[1];
}
_sb.Append('(');
Visit(item);
_sb.Append(" IN ");
// hardcode the integer list if it exists to bypass parameter limit
if (item.Type == typeof(int) && TryGetRightValue(list, out var value))
{
var items = (IEnumerable<int>)value;
_sb.Append('(');
_sb.Append(string.Join(", ", items));
_sb.Append(')');
_gotConcreteValue = true;
}
else
{
Visit(list);
}
_sb.Append(')');
}
private void ParseStringContains(MethodCallExpression body)
{
_sb.Append('(');
Visit(body.Object);
_sb.Append(" LIKE '%' || ");
Visit(body.Arguments[0]);
_sb.Append(" || '%')");
}
private void ParseStartsWith(MethodCallExpression body)
{
_sb.Append('(');
Visit(body.Object);
_sb.Append(" LIKE ");
Visit(body.Arguments[0]);
_sb.Append(" || '%')");
}
private void ParseEndsWith(MethodCallExpression body)
{
_sb.Append('(');
Visit(body.Object);
_sb.Append(" LIKE '%' || ");
Visit(body.Arguments[0]);
_sb.Append(')');
}
public override string ToString()
{
var sql = _sb.ToString();
if (_requireConcreteValue && !_gotConcreteValue)
{
var e = new InvalidOperationException("WhereBuilder requires a concrete condition");
e.Data.Add("sql", sql);
throw e;
}
return sql;
}
}
}

View File

@@ -57,7 +57,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
[FieldDefinition(8, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(RTorrentPriority), HelpText = "Priority to use when grabbing items")]
public int Priority { get; set; }
[FieldDefinition(9, Label = "Add Stopped", Type = FieldType.Checkbox, HelpText = "Enabling will prevent magnets from downloading before downloading")]
[FieldDefinition(9, Label = "Add Stopped", Type = FieldType.Checkbox, HelpText = "Enabling will add torrents and magnets to ruTorrent in a stopped state")]
public bool AddStopped { get; set; }
public NzbDroneValidationResult Validate()

View File

@@ -0,0 +1,48 @@
using System.Linq;
using NLog;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.Cardigann;
using NzbDrone.Core.IndexerVersions;
using NzbDrone.Core.Localization;
using NzbDrone.Core.ThingiProvider.Events;
namespace NzbDrone.Core.HealthCheck.Checks
{
[CheckOn(typeof(ProviderDeletedEvent<IIndexer>))]
public class NoDefinitionCheck : HealthCheckBase
{
private readonly IIndexerDefinitionUpdateService _indexerDefinitionUpdateService;
private readonly IIndexerFactory _indexerFactory;
public NoDefinitionCheck(IIndexerDefinitionUpdateService indexerDefinitionUpdateService, IIndexerFactory indexerFactory, ILocalizationService localizationService)
: base(localizationService)
{
_indexerDefinitionUpdateService = indexerDefinitionUpdateService;
_indexerFactory = indexerFactory;
}
public override HealthCheck Check()
{
var currentDefs = _indexerDefinitionUpdateService.All();
var noDefIndexers = _indexerFactory.AllProviders(false)
.Where(i => i.Definition.Implementation == "Cardigann" && !currentDefs.Any(d => d.File == ((CardigannSettings)i.Definition.Settings).DefinitionFile)).ToList();
if (noDefIndexers.Count == 0)
{
return new HealthCheck(GetType());
}
var healthType = HealthCheckResult.Error;
var healthMessage = string.Format(_localizationService.GetLocalizedString("IndexerNoDefCheckMessage"),
string.Join(", ", noDefIndexers.Select(v => v.Definition.Name)));
return new HealthCheck(GetType(),
healthType,
healthMessage,
"#indexers-have-no-definition");
}
public override bool CheckOnSchedule => false;
}
}

View File

@@ -35,10 +35,13 @@ namespace NzbDrone.Core.HealthCheck.Checks
return new HealthCheck(GetType());
}
var healthType = HealthCheckResult.Warning;
var healthMessage = string.Format(_localizationService.GetLocalizedString("IndexerObsoleteCheckMessage"),
string.Join(", ", oldIndexers.Select(v => v.Definition.Name)));
return new HealthCheck(GetType(),
HealthCheckResult.Warning,
string.Format(_localizationService.GetLocalizedString("IndexerObsoleteCheckMessage"),
string.Join(", ", oldIndexers.Select(v => v.Definition.Name))),
healthType,
healthMessage,
"#indexers-are-obsolete");
}

View File

@@ -100,11 +100,13 @@ namespace NzbDrone.Core.History
public int CountSince(int indexerId, DateTime date, List<HistoryEventType> eventTypes)
{
var builder = new SqlBuilder()
var intEvents = eventTypes.Select(t => (int)t).ToList();
var builder = new SqlBuilder(_database.DatabaseType)
.SelectCount()
.Where<History>(x => x.IndexerId == indexerId)
.Where<History>(x => x.Date >= date)
.Where<History>(x => eventTypes.Contains(x.EventType));
.Where<History>(x => intEvents.Contains((int)x.EventType));
var sql = builder.AddPageCountTemplate(typeof(History));

View File

@@ -0,0 +1,27 @@
using Dapper;
using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.Housekeeping.Housekeepers
{
public class CleanupOrphanedApplicationStatus : IHousekeepingTask
{
private readonly IMainDatabase _database;
public CleanupOrphanedApplicationStatus(IMainDatabase database)
{
_database = database;
}
public void Clean()
{
var mapper = _database.OpenConnection();
mapper.Execute(@"DELETE FROM ""ApplicationStatus""
WHERE ""Id"" IN (
SELECT ""ApplicationStatus"".""Id"" FROM ""ApplicationStatus""
LEFT OUTER JOIN ""Applications""
ON ""ApplicationStatus"".""ProviderId"" = ""Applications"".""Id""
WHERE ""Applications"".""Id"" IS NULL)");
}
}
}

View File

@@ -91,10 +91,12 @@ namespace NzbDrone.Core.IndexerSearch
r.IndexerFlags == null ? null : from f in r.IndexerFlags select GetNabElement("tag", f.Name, protocol),
r.Languages == null ? null : from c in r.Languages select GetNabElement("language", c.Id, protocol),
r.Subs == null ? null : from c in r.Subs select GetNabElement("subs", c.Id, protocol),
r.Genres == null ? null : GetNabElement("genre", string.Join(", ", r.Genres), protocol),
GetNabElement("rageid", r.TvRageId, protocol),
GetNabElement("tvdbid", r.TvdbId, protocol),
GetNabElement("imdb", r.ImdbId.ToString("D7"), protocol),
GetNabElement("tmdbid", r.TmdbId, protocol),
GetNabElement("traktid", r.TraktId, protocol),
GetNabElement("seeders", t.Seeders, protocol),
GetNabElement("files", r.Files, protocol),
GetNabElement("grabs", r.Grabs, protocol),

View File

@@ -1,16 +1,16 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers.Cardigann;
using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
@@ -19,16 +19,18 @@ namespace NzbDrone.Core.IndexerVersions
public interface IIndexerDefinitionUpdateService
{
List<CardigannMetaDefinition> All();
CardigannDefinition GetDefinition(string fileKey);
CardigannDefinition GetCachedDefinition(string fileKey);
List<string> GetBlocklist();
}
public class IndexerDefinitionUpdateService : IIndexerDefinitionUpdateService, IExecute<IndexerDefinitionUpdateCommand>
public class IndexerDefinitionUpdateService : IIndexerDefinitionUpdateService, IExecute<IndexerDefinitionUpdateCommand>, IHandle<ApplicationStartedEvent>
{
/* Update Service will fall back if version # does not exist for an indexer per Ta */
private const string DEFINITION_BRANCH = "master";
private const int DEFINITION_VERSION = 3;
private const int DEFINITION_VERSION = 5;
//Used when moving yml to C#
private readonly List<string> _defintionBlocklist = new List<string>()
{
"aither",
@@ -51,6 +53,7 @@ namespace NzbDrone.Core.IndexerVersions
private readonly IHttpClient _httpClient;
private readonly IAppFolderInfo _appFolderInfo;
private readonly IDiskProvider _diskProvider;
private readonly IIndexerDefinitionVersionService _versionService;
private readonly ICached<CardigannDefinition> _cache;
private readonly Logger _logger;
@@ -62,11 +65,13 @@ namespace NzbDrone.Core.IndexerVersions
public IndexerDefinitionUpdateService(IHttpClient httpClient,
IAppFolderInfo appFolderInfo,
IDiskProvider diskProvider,
IIndexerDefinitionVersionService versionService,
ICacheManager cacheManager,
Logger logger)
{
_appFolderInfo = appFolderInfo;
_diskProvider = diskProvider;
_versionService = versionService;
_cache = cacheManager.GetCache<CardigannDefinition>(typeof(CardigannDefinition), "definitions");
_httpClient = httpClient;
_logger = logger;
@@ -78,43 +83,24 @@ namespace NzbDrone.Core.IndexerVersions
try
{
var request = new HttpRequest($"https://indexers.prowlarr.com/{DEFINITION_BRANCH}/{DEFINITION_VERSION}");
var response = _httpClient.Get<List<CardigannMetaDefinition>>(request);
indexerList = response.Resource.Where(i => !_defintionBlocklist.Contains(i.File)).ToList();
var definitionFolder = Path.Combine(_appFolderInfo.AppDataFolder, "Definitions", "Custom");
var directoryInfo = new DirectoryInfo(definitionFolder);
if (directoryInfo.Exists)
// Grab latest def list from server or fallback to disk
try
{
var files = directoryInfo.GetFiles($"*.yml");
foreach (var file in files)
{
_logger.Debug("Loading Custom Cardigann definition " + file.FullName);
try
{
var definitionString = File.ReadAllText(file.FullName);
var definition = _deserializer.Deserialize<CardigannMetaDefinition>(definitionString);
definition.File = Path.GetFileNameWithoutExtension(file.Name);
if (indexerList.Any(i => i.File == definition.File || i.Name == definition.Name))
{
_logger.Warn("Custom Cardigann definition {0} does not have unique file name or Indexer name", file.FullName);
continue;
}
indexerList.Add(definition);
}
catch (Exception e)
{
_logger.Error($"Error while parsing custom Cardigann definition {file.FullName}\n{e}");
}
}
var request = new HttpRequest($"https://indexers.prowlarr.com/{DEFINITION_BRANCH}/{DEFINITION_VERSION}");
var response = _httpClient.Get<List<CardigannMetaDefinition>>(request);
indexerList = response.Resource.Where(i => !_defintionBlocklist.Contains(i.File)).ToList();
}
catch
{
var definitionFolder = Path.Combine(_appFolderInfo.AppDataFolder, "Definitions");
indexerList = ReadDefinitionsFromDisk(indexerList, definitionFolder);
}
//Check for custom definitions
var customDefinitionFolder = Path.Combine(_appFolderInfo.AppDataFolder, "Definitions", "Custom");
indexerList = ReadDefinitionsFromDisk(indexerList, customDefinitionFolder);
}
catch
{
@@ -124,14 +110,14 @@ namespace NzbDrone.Core.IndexerVersions
return indexerList;
}
public CardigannDefinition GetDefinition(string file)
public CardigannDefinition GetCachedDefinition(string fileKey)
{
if (string.IsNullOrEmpty(file))
if (string.IsNullOrEmpty(fileKey))
{
throw new ArgumentNullException(nameof(file));
throw new ArgumentNullException(nameof(fileKey));
}
var definition = _cache.Get(file, () => LoadIndexerDef(file));
var definition = _cache.Get(fileKey, () => GetUncachedDefinition(fileKey));
return definition;
}
@@ -141,15 +127,46 @@ namespace NzbDrone.Core.IndexerVersions
return _defintionBlocklist;
}
private CardigannDefinition GetHttpDefinition(string id)
private List<CardigannMetaDefinition> ReadDefinitionsFromDisk(List<CardigannMetaDefinition> defs, string path, SearchOption options = SearchOption.TopDirectoryOnly)
{
var req = new HttpRequest($"https://indexers.prowlarr.com/{DEFINITION_BRANCH}/{DEFINITION_VERSION}/{id}");
var response = _httpClient.Get(req);
var definition = _deserializer.Deserialize<CardigannDefinition>(response.Content);
return CleanIndexerDefinition(definition);
var indexerList = defs;
var directoryInfo = new DirectoryInfo(path);
if (directoryInfo.Exists)
{
var files = directoryInfo.GetFiles($"*.yml", options);
foreach (var file in files)
{
_logger.Debug("Loading definition " + file.FullName);
try
{
var definitionString = File.ReadAllText(file.FullName);
var definition = _deserializer.Deserialize<CardigannMetaDefinition>(definitionString);
definition.File = Path.GetFileNameWithoutExtension(file.Name);
if (indexerList.Any(i => i.File == definition.File || i.Name == definition.Name))
{
_logger.Warn("Definition {0} does not have unique file name or Indexer name", file.FullName);
continue;
}
indexerList.Add(definition);
}
catch (Exception e)
{
_logger.Error($"Error while parsing Cardigann definition {file.FullName}\n{e}");
}
}
}
return indexerList;
}
private CardigannDefinition LoadIndexerDef(string fileKey)
private CardigannDefinition GetUncachedDefinition(string fileKey)
{
if (string.IsNullOrEmpty(fileKey))
{
@@ -184,9 +201,26 @@ namespace NzbDrone.Core.IndexerVersions
}
}
var dbDefs = _versionService.All();
//Check to ensure it's in versioned defs before we go to web
if (dbDefs.Count > 0 && !dbDefs.Any(x => x.File == fileKey))
{
throw new ArgumentNullException(nameof(fileKey));
}
//No definition was returned locally, go to the web
return GetHttpDefinition(fileKey);
}
private CardigannDefinition GetHttpDefinition(string id)
{
var req = new HttpRequest($"https://indexers.prowlarr.com/{DEFINITION_BRANCH}/{DEFINITION_VERSION}/{id}");
var response = _httpClient.Get(req);
var definition = _deserializer.Deserialize<CardigannDefinition>(response.Content);
return CleanIndexerDefinition(definition);
}
private CardigannDefinition CleanIndexerDefinition(CardigannDefinition definition)
{
if (definition.Settings == null)
@@ -226,6 +260,12 @@ namespace NzbDrone.Core.IndexerVersions
return definition;
}
public void Handle(ApplicationStartedEvent message)
{
// Sync indexers on app start
UpdateLocalDefinitions();
}
public void Execute(IndexerDefinitionUpdateCommand message)
{
UpdateLocalDefinitions();
@@ -242,29 +282,44 @@ namespace NzbDrone.Core.IndexerVersions
{
var startupFolder = _appFolderInfo.AppDataFolder;
var request = new HttpRequest($"https://indexers.prowlarr.com/{DEFINITION_BRANCH}/{DEFINITION_VERSION}");
var response = _httpClient.Get<List<CardigannMetaDefinition>>(request);
var currentDefs = _versionService.All().ToDictionary(x => x.DefinitionId, x => x.Sha);
try
{
EnsureDefinitionsFolder();
var definitionsFolder = Path.Combine(startupFolder, "Definitions");
var saveFile = Path.Combine(startupFolder, "Definitions", $"indexers.zip");
_httpClient.DownloadFile($"https://indexers.prowlarr.com/{DEFINITION_BRANCH}/{DEFINITION_VERSION}/package.zip", saveFile);
using (ZipArchive archive = ZipFile.OpenRead(saveFile))
foreach (var def in response.Resource)
{
archive.ExtractToDirectory(definitionsFolder, true);
try
{
var saveFile = Path.Combine(startupFolder, "Definitions", $"{def.File}.yml");
if (currentDefs.TryGetValue(def.Id, out var defSha) && defSha == def.Sha)
{
_logger.Trace("Indexer already up to date: {0}", def.File);
continue;
}
_httpClient.DownloadFile($"https://indexers.prowlarr.com/{DEFINITION_BRANCH}/{DEFINITION_VERSION}/{def.File}", saveFile);
_versionService.Upsert(new IndexerDefinitionVersion { Sha = def.Sha, DefinitionId = def.Id, File = def.File, LastUpdated = DateTime.UtcNow });
_cache.Remove(def.File);
_logger.Debug("Updated definition: {0}", def.File);
}
catch (Exception ex)
{
_logger.Error("Definition download failed: {0}, {1}", def.File, ex.Message);
}
}
_diskProvider.DeleteFile(saveFile);
_cache.Clear();
_logger.Debug("Updated indexer definitions");
}
catch (Exception ex)
{
_logger.Error(ex, "Definition update failed");
_logger.Error(ex, "Definition download failed, error creating definitions folder in {0}", startupFolder);
}
}
}

View File

@@ -0,0 +1,13 @@
using System;
using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.IndexerVersions
{
public class IndexerDefinitionVersion : ModelBase
{
public string File { get; set; }
public string Sha { get; set; }
public DateTime LastUpdated { get; set; }
public string DefinitionId { get; set; }
}
}

View File

@@ -0,0 +1,25 @@
using System;
using System.Linq;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.IndexerVersions
{
public interface IIndexerDefinitionVersionRepository : IBasicRepository<IndexerDefinitionVersion>
{
public IndexerDefinitionVersion GetByDefId(string defId);
}
public class IndexerDefinitionVersionRepository : BasicRepository<IndexerDefinitionVersion>, IIndexerDefinitionVersionRepository
{
public IndexerDefinitionVersionRepository(IMainDatabase database, IEventAggregator eventAggregator)
: base(database, eventAggregator)
{
}
public IndexerDefinitionVersion GetByDefId(string defId)
{
return Query(x => x.DefinitionId == defId).SingleOrDefault();
}
}
}

View File

@@ -0,0 +1,66 @@
using System.Collections.Generic;
using System.Linq;
namespace NzbDrone.Core.IndexerVersions
{
public interface IIndexerDefinitionVersionService
{
IndexerDefinitionVersion Get(int indexerVersionId);
IndexerDefinitionVersion GetByDefId(string defId);
List<IndexerDefinitionVersion> All();
IndexerDefinitionVersion Add(IndexerDefinitionVersion defVersion);
IndexerDefinitionVersion Upsert(IndexerDefinitionVersion defVersion);
void Delete(int indexerVersionId);
}
public class IndexerDefinitionVersionService : IIndexerDefinitionVersionService
{
private readonly IIndexerDefinitionVersionRepository _repo;
public IndexerDefinitionVersionService(IIndexerDefinitionVersionRepository repo)
{
_repo = repo;
}
public IndexerDefinitionVersion Get(int indexerVersionId)
{
return _repo.Get(indexerVersionId);
}
public IndexerDefinitionVersion GetByDefId(string defId)
{
return _repo.GetByDefId(defId);
}
public List<IndexerDefinitionVersion> All()
{
return _repo.All().ToList();
}
public IndexerDefinitionVersion Add(IndexerDefinitionVersion defVersion)
{
_repo.Insert(defVersion);
return defVersion;
}
public IndexerDefinitionVersion Upsert(IndexerDefinitionVersion defVersion)
{
var existing = _repo.GetByDefId(defVersion.DefinitionId);
if (existing != null)
{
defVersion.Id = existing.Id;
}
defVersion = _repo.Upsert(defVersion);
return defVersion;
}
public void Delete(int indexerVersionId)
{
_repo.Delete(indexerVersionId);
}
}
}

View File

@@ -48,14 +48,17 @@ namespace NzbDrone.Core.Indexers.Definitions
caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.TVAnime, "Anime Series");
caps.Categories.AddCategoryMapping(3, NewznabStandardCategory.Audio, "Anime Musik/OST");
caps.Categories.AddCategoryMapping(4, NewznabStandardCategory.PCGames, "Anime Spiele");
caps.Categories.AddCategoryMapping(5, NewznabStandardCategory.XXX, "Hentai");
caps.Categories.AddCategoryMapping(6, NewznabStandardCategory.PCGames, "Spiele Linux");
caps.Categories.AddCategoryMapping(5, NewznabStandardCategory.XXX, "Anime Hentai");
caps.Categories.AddCategoryMapping(6, NewznabStandardCategory.PCGames, "Software");
caps.Categories.AddCategoryMapping(7, NewznabStandardCategory.Other, "Sonstiges");
caps.Categories.AddCategoryMapping(8, NewznabStandardCategory.Movies, "Filme");
caps.Categories.AddCategoryMapping(9, NewznabStandardCategory.TV, "Serien");
caps.Categories.AddCategoryMapping(10, NewznabStandardCategory.PCGames, "Spiele");
caps.Categories.AddCategoryMapping(11, NewznabStandardCategory.Audio, "Musik");
caps.Categories.AddCategoryMapping(12, NewznabStandardCategory.BooksComics, "Mangas");
caps.Categories.AddCategoryMapping(13, NewznabStandardCategory.Movies, "Cartoon Filme");
caps.Categories.AddCategoryMapping(14, NewznabStandardCategory.TV, "Cartoon Serie");
caps.Categories.AddCategoryMapping(15, NewznabStandardCategory.XXX, "H-Manga / Doujinshi");
return caps;
}

View File

@@ -148,7 +148,7 @@ namespace NzbDrone.Core.Indexers.Definitions
{ "order_way", "desc" },
{ "action", "basic" },
{ "searchsubmit", "1" },
{ "searchstr", imdbId.IsNotNullOrWhiteSpace() ? imdbId : term }
{ "searchstr", imdbId.IsNotNullOrWhiteSpace() ? imdbId : term.Replace(".", " ") }
};
var catList = Capabilities.Categories.MapTorznabCapsToTrackers(categories);

View File

@@ -46,6 +46,11 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
public List<AvistazRelease> Data { get; set; }
}
public class AvistazErrorResponse
{
public string Message { get; set; }
}
public class AvistazIdInfo
{
public string Tmdb { get; set; }

View File

@@ -89,6 +89,22 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
{
await GetToken();
}
catch (HttpException ex)
{
if (ex.Response.StatusCode == HttpStatusCode.Unauthorized)
{
_logger.Warn(ex, "Unauthorized request to indexer");
var jsonResponse = new HttpResponse<AvistazErrorResponse>(ex.Response);
return new ValidationFailure(string.Empty, jsonResponse.Resource?.Message ?? "Unauthorized request to indexer");
}
else
{
_logger.Warn(ex, "Unable to connect to indexer");
return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log for more details");
}
}
catch (Exception ex)
{
_logger.Warn(ex, "Unable to connect to indexer");

View File

@@ -182,18 +182,23 @@ namespace NzbDrone.Core.Indexers.Definitions
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
{
var torrentInfos = new List<TorrentInfo>();
if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
var indexerHttpResponse = indexerResponse.HttpResponse;
if (indexerHttpResponse.StatusCode != HttpStatusCode.OK)
{
throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from API request");
throw new IndexerException(indexerResponse, $"Unexpected response status {indexerHttpResponse.StatusCode} code from API request");
}
if (!indexerResponse.HttpResponse.Headers.ContentType.Contains(HttpAccept.Json.Value))
if (!indexerHttpResponse.Headers.ContentType.Contains(HttpAccept.Json.Value))
{
throw new IndexerException(indexerResponse, $"Unexpected response header {indexerResponse.HttpResponse.Headers.ContentType} from API request, expected {HttpAccept.Json.Value}");
throw new IndexerException(indexerResponse, $"Unexpected response header {indexerHttpResponse.Headers.ContentType} from API request, expected {HttpAccept.Json.Value}");
}
var jsonResponse = new HttpResponse<BeyondHDResponse>(indexerResponse.HttpResponse);
if (indexerResponse.Content.ContainsIgnoreCase("Invalid API Key"))
{
throw new IndexerAuthException("API Key invalid or not authorized");
}
var jsonResponse = new HttpResponse<BeyondHDResponse>(indexerHttpResponse);
foreach (var row in jsonResponse.Resource.Results)
{
@@ -213,7 +218,7 @@ namespace NzbDrone.Core.Indexers.Definitions
Grabs = row.Grabs,
Seeders = row.Seeders,
ImdbId = ParseUtil.GetImdbID(row.ImdbId).GetValueOrDefault(),
TmdbId = row.TmdbId.IsNullOrWhiteSpace() ? 0 : ParseUtil.CoerceInt(row.TmdbId.Split("/")[1]),
TmdbId = row.TmdbId.IsNullOrWhiteSpace() ? 0 : (int)ParseUtil.CoerceLong(row.TmdbId.Split("/")[1]),
Peers = row.Leechers + row.Seeders,
DownloadVolumeFactor = row.Freeleech || row.Limited ? 0 : row.Promo75 ? 0.25 : row.Promo50 ? 0.5 : row.Promo25 ? 0.75 : 1,
UploadVolumeFactor = 1,

View File

@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using NLog;
using NzbDrone.Common.Http;
@@ -16,6 +17,7 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
public override bool SupportsSearch => true;
public override int PageSize => 100;
public override IndexerCapabilities Capabilities => SetCapabilities();
public override TimeSpan RateLimit => TimeSpan.FromSeconds(5);
public override string[] IndexerUrls => new string[] { "http://api.broadcasthe.net/" };
public override string Description => "BroadcasTheNet (BTN) is an invite-only torrent tracker focused on TV shows";

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text.RegularExpressions;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.Parser.Model;
@@ -25,8 +26,9 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
{
var results = new List<ReleaseInfo>();
var indexerHttpResponse = indexerResponse.HttpResponse;
switch (indexerResponse.HttpResponse.StatusCode)
switch (indexerHttpResponse.StatusCode)
{
case HttpStatusCode.Unauthorized:
throw new IndexerAuthException("API Key invalid or not authorized");
@@ -35,25 +37,30 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
case HttpStatusCode.ServiceUnavailable:
throw new RequestLimitReachedException(indexerResponse, "Cannot do more than 150 API requests per hour.");
default:
if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
if (indexerHttpResponse.StatusCode != HttpStatusCode.OK)
{
throw new IndexerException(indexerResponse, "Indexer API call returned an unexpected StatusCode [{0}]", indexerResponse.HttpResponse.StatusCode);
throw new IndexerException(indexerResponse, "Indexer API call returned an unexpected StatusCode [{0}]", indexerHttpResponse.StatusCode);
}
break;
}
if (indexerResponse.HttpResponse.Headers.ContentType != null && indexerResponse.HttpResponse.Headers.ContentType.Contains("text/html"))
if (indexerHttpResponse.Headers.ContentType != null && indexerHttpResponse.Headers.ContentType.Contains("text/html"))
{
throw new IndexerException(indexerResponse, "Indexer responded with html content. Site is likely blocked or unavailable.");
}
if (indexerResponse.Content.ContainsIgnoreCase("Call Limit Exceeded"))
{
throw new RequestLimitReachedException(indexerResponse, "Cannot do more than 150 API requests per hour.");
}
if (indexerResponse.Content == "Query execution was interrupted")
{
throw new IndexerException(indexerResponse, "Indexer API returned an internal server error");
}
JsonRpcResponse<BroadcastheNetTorrents> jsonResponse = new HttpResponse<JsonRpcResponse<BroadcastheNetTorrents>>(indexerResponse.HttpResponse).Resource;
JsonRpcResponse<BroadcastheNetTorrents> jsonResponse = new HttpResponse<JsonRpcResponse<BroadcastheNetTorrents>>(indexerHttpResponse).Resource;
if (jsonResponse.Error != null || jsonResponse.Result == null)
{

View File

@@ -37,7 +37,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
{
var generator = _generatorCache.Get(Settings.DefinitionFile, () =>
new CardigannRequestGenerator(_configService,
_definitionService.GetDefinition(Settings.DefinitionFile),
_definitionService.GetCachedDefinition(Settings.DefinitionFile),
_logger)
{
HttpClient = _httpClient,
@@ -57,7 +57,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
public override IParseIndexerResponse GetParser()
{
return new CardigannParser(_configService,
_definitionService.GetDefinition(Settings.DefinitionFile),
_definitionService.GetCachedDefinition(Settings.DefinitionFile),
_logger)
{
Settings = Settings

View File

@@ -48,6 +48,9 @@ namespace NzbDrone.Core.Indexers.Cardigann
protected static readonly Regex _LogicFunctionRegex = new Regex(
$@"\b({string.Join("|", _SupportedLogicFunctions.Select(Regex.Escape))})(?:\s+(\(?\.[^\)\s]+\)?|""[^""]+"")){{2,}}");
// Matches CSS selectors for the JSON parser
protected static readonly Regex _jsonSelectorRegex = new Regex(@"\:(?<filter>.+?)\((?<key>.+?)\)(?=:|\z)", RegexOptions.Compiled);
public CardigannSettings Settings { get; set; }
public CardigannBase(IConfigService configService,
@@ -234,13 +237,20 @@ namespace NzbDrone.Core.Indexers.Cardigann
if (selector.Selector != null)
{
var selector_Selector = ApplyGoTemplateText(selector.Selector.TrimStart('.'), variables);
var selection = parentObj.SelectToken(selector_Selector);
var selectorSelector = ApplyGoTemplateText(selector.Selector.TrimStart('.'), variables);
selectorSelector = JsonParseFieldSelector(parentObj, selectorSelector);
JToken selection = null;
if (selectorSelector != null)
{
selection = parentObj.SelectToken(selectorSelector);
}
if (selection == null)
{
if (required)
{
throw new Exception(string.Format("Selector \"{0}\" didn't match {1}", selector_Selector, parentObj.ToString()));
throw new Exception(string.Format("Selector \"{0}\" didn't match {1}", selectorSelector, parentObj.ToString()));
}
return null;
@@ -375,6 +385,22 @@ namespace NzbDrone.Core.Indexers.Cardigann
return results;
}
public ICollection<IndexerCategory> MapTrackerCatDescToNewznab(string trackerCategoryDesc)
{
if (string.IsNullOrWhiteSpace(trackerCategoryDesc))
{
return new List<IndexerCategory>();
}
var cats = _categoryMapping
.Where(m =>
!string.IsNullOrWhiteSpace(m.TrackerCategoryDesc) &&
string.Equals(m.TrackerCategoryDesc, trackerCategoryDesc, StringComparison.InvariantCultureIgnoreCase))
.Select(c => NewznabStandardCategory.AllCats.FirstOrDefault(n => n.Id == c.NewzNabCategory) ?? new IndexerCategory { Id = c.NewzNabCategory })
.ToList();
return cats;
}
protected delegate string TemplateTextModifier(string str);
protected string ApplyGoTemplateText(string template, Dictionary<string, object> variables = null, TemplateTextModifier modifier = null)
@@ -816,5 +842,108 @@ namespace NzbDrone.Core.Indexers.Cardigann
{
return new Uri(currentUrl ?? new Uri(SiteLink), path);
}
protected string ResolveSiteLink()
{
var settingsBaseUrl = Settings?.BaseUrl;
var defaultLink = _definition.Links.First();
if (settingsBaseUrl == null)
{
return defaultLink;
}
if (_definition?.Legacylinks?.Contains(settingsBaseUrl) ?? false)
{
_logger.Trace("Changing legacy site link from {0} to {1}", settingsBaseUrl, defaultLink);
return defaultLink;
}
return settingsBaseUrl;
}
protected JArray JsonParseRowsSelector(JToken parsedJson, string rowSelector)
{
var selector = rowSelector.Split(':')[0];
var rowsObj = parsedJson.SelectToken(selector).Value<JArray>();
return new JArray(rowsObj.Where(t =>
JsonParseFieldSelector(t.Value<JObject>(), rowSelector.Remove(0, selector.Length)) != null));
}
private string JsonParseFieldSelector(JToken parsedJson, string rowSelector)
{
var selector = rowSelector.Split(':')[0];
JToken parsedObject;
if (string.IsNullOrWhiteSpace(selector))
{
parsedObject = parsedJson;
}
else if (parsedJson.SelectToken(selector) != null)
{
parsedObject = parsedJson.SelectToken(selector);
}
else
{
return null;
}
foreach (Match match in _jsonSelectorRegex.Matches(rowSelector))
{
var filter = match.Result("${filter}");
var key = match.Result("${key}");
Match innerMatch;
switch (filter)
{
case "has":
innerMatch = _jsonSelectorRegex.Match(key);
if (innerMatch.Success)
{
if (JsonParseFieldSelector(parsedObject, key) == null)
{
return null;
}
}
else
{
if (parsedObject.SelectToken(key) == null)
{
return null;
}
}
break;
case "not":
innerMatch = _jsonSelectorRegex.Match(key);
if (innerMatch.Success)
{
if (JsonParseFieldSelector(parsedObject, key) != null)
{
return null;
}
}
else
{
if (parsedObject.SelectToken(key) != null)
{
return null;
}
}
break;
case "contains":
if (!parsedObject.ToString().Contains(key))
{
return null;
}
break;
default:
_logger.Error(string.Format("CardigannIndexer ({0}): Unsupported selector: {1}", _definition.Id, rowSelector));
continue;
}
}
return selector;
}
}
}

View File

@@ -151,6 +151,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
public int After { get; set; }
public SelectorBlock Dateheaders { get; set; }
public SelectorBlock Count { get; set; }
public bool Multiple { get; set; } = false;
}
public class SearchPathBlock : RequestBlock
@@ -200,8 +201,6 @@ namespace NzbDrone.Core.Indexers.Cardigann
public class ResponseBlock
{
public string Type { get; set; }
public string Attribute { get; set; }
public bool Multiple { get; set; }
public string NoResultsMessage { get; set; }
}
}

View File

@@ -21,7 +21,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
{
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
protected override string SiteLink => Settings?.BaseUrl ?? _definition.Links.First();
protected override string SiteLink => ResolveSiteLink();
public CardigannParser(IConfigService configService,
CardigannDefinition definition,
@@ -83,16 +83,16 @@ namespace NzbDrone.Core.Indexers.Cardigann
}
}
var rowsObj = parsedJson.SelectToken(search.Rows.Selector);
if (rowsObj == null)
var rowsArray = JsonParseRowsSelector(parsedJson, search.Rows.Selector);
if (rowsArray == null)
{
throw new IndexerException(indexerResponse, "Error Parsing Rows Selector");
}
foreach (var row in rowsObj.Value<JArray>())
foreach (var row in rowsArray)
{
var selObj = request.SearchPath.Response.Attribute != null ? row.SelectToken(request.SearchPath.Response.Attribute).Value<JToken>() : row;
var mulRows = request.SearchPath.Response.Multiple == true ? selObj.Values<JObject>() : new List<JObject> { selObj.Value<JObject>() };
var selObj = search.Rows.Attribute != null ? row.SelectToken(search.Rows.Attribute).Value<JToken>() : row;
var mulRows = search.Rows.Multiple ? selObj.Values<JObject>() : new List<JObject> { selObj.Value<JObject>() };
foreach (var mulRow in mulRows)
{
@@ -464,6 +464,22 @@ namespace NzbDrone.Core.Indexers.Cardigann
}
}
value = release.Categories.ToString();
break;
case "categorydesc":
var catsDesc = MapTrackerCatDescToNewznab(value);
if (catsDesc.Any())
{
if (release.Categories == null || fieldModifiers.Contains("noappend"))
{
release.Categories = catsDesc;
}
else
{
release.Categories = release.Categories.Union(catsDesc).ToList();
}
}
value = release.Categories.ToString();
break;
case "size":
@@ -545,6 +561,13 @@ namespace NzbDrone.Core.Indexers.Cardigann
release.TvRageId = (int)ParseUtil.CoerceLong(rageID);
value = release.TvRageId.ToString();
break;
case "traktid":
var traktIDRegEx = new Regex(@"(\d+)", RegexOptions.Compiled);
var traktIDMatch = traktIDRegEx.Match(value);
var traktID = traktIDMatch.Groups[1].Value;
release.TraktId = (int)ParseUtil.CoerceLong(traktID);
value = release.TraktId.ToString();
break;
case "tvdbid":
var tvdbIdRegEx = new Regex(@"(\d+)", RegexOptions.Compiled);
var tvdbIdMatch = tvdbIdRegEx.Match(value);
@@ -561,6 +584,14 @@ namespace NzbDrone.Core.Indexers.Cardigann
value = release.PosterUrl;
break;
case "genre":
release.Genres = release.Genres.Union(value.Split(',')).ToList();
value = release.Genres.ToString();
break;
case "year":
release.Year = ParseUtil.CoerceInt(value);
value = release.Year.ToString();
break;
case "author":
release.Author = value;
break;

View File

@@ -8,13 +8,6 @@ namespace NzbDrone.Core.Indexers.Cardigann
public Dictionary<string, object> Variables { get; private set; }
public SearchPathBlock SearchPath { get; private set; }
public CardigannRequest(string url, HttpAccept httpAccept, Dictionary<string, object> variables, SearchPathBlock searchPath)
: base(url, httpAccept)
{
Variables = variables;
SearchPath = searchPath;
}
public CardigannRequest(HttpRequest httpRequest, Dictionary<string, object> variables, SearchPathBlock searchPath)
: base(httpRequest)
{

View File

@@ -27,7 +27,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
public IDictionary<string, string> Cookies { get; set; }
protected HttpResponse landingResult;
protected IHtmlDocument landingResultDocument;
protected override string SiteLink => Settings?.BaseUrl ?? _definition.Links.First();
protected override string SiteLink => ResolveSiteLink();
public CardigannRequestGenerator(IConfigService configService,
CardigannDefinition definition,
@@ -41,7 +41,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
{
_logger.Trace("Getting search");
_logger.Trace("Getting Movie search");
var pageableRequests = new IndexerPageableRequestChain();
@@ -61,6 +61,8 @@ namespace NzbDrone.Core.Indexers.Cardigann
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
{
_logger.Trace("Getting Music search");
var pageableRequests = new IndexerPageableRequestChain();
var variables = GetQueryVariableDefaults(searchCriteria);
@@ -77,6 +79,8 @@ namespace NzbDrone.Core.Indexers.Cardigann
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
{
_logger.Trace("Getting TV search");
var pageableRequests = new IndexerPageableRequestChain();
var variables = GetQueryVariableDefaults(searchCriteria);
@@ -99,6 +103,8 @@ namespace NzbDrone.Core.Indexers.Cardigann
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
{
_logger.Trace("Getting Book search");
var pageableRequests = new IndexerPageableRequestChain();
var variables = GetQueryVariableDefaults(searchCriteria);
@@ -113,6 +119,8 @@ namespace NzbDrone.Core.Indexers.Cardigann
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
{
_logger.Trace("Getting Basic search");
var pageableRequests = new IndexerPageableRequestChain();
var variables = GetQueryVariableDefaults(searchCriteria);
@@ -647,10 +655,13 @@ namespace NzbDrone.Core.Indexers.Cardigann
protected string GetRedirectDomainHint(string requestUrl, string redirectUrl)
{
if (requestUrl.StartsWith(SiteLink) && !redirectUrl.StartsWith(SiteLink))
var siteLinkUri = new HttpUri(SiteLink);
var requestUri = new HttpUri(requestUrl);
var redirectUri = new HttpUri(redirectUrl);
if (requestUri.Host.StartsWith(siteLinkUri.Host) && !redirectUri.Host.StartsWith(siteLinkUri.Host))
{
var uri = new HttpUri(redirectUrl);
return uri.Scheme + "://" + uri.Host + "/";
return redirectUri.Scheme + "://" + redirectUri.Host + "/";
}
return null;

View File

@@ -19,6 +19,7 @@ using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Definitions
{
[Obsolete("Moved to YML")]
public class DanishBytes : TorrentIndexerBase<DanishBytesSettings>
{
public override string Name => "DanishBytes";

View File

@@ -53,7 +53,7 @@ namespace NzbDrone.Core.Indexers.Gazelle
if (!string.IsNullOrWhiteSpace(searchString))
{
parameters += string.Format("&searchstr={0}", searchString);
parameters += string.Format("&searchstr={0}", searchString.Replace(".", " "));
}
if (categories != null)

View File

@@ -23,29 +23,29 @@ namespace NzbDrone.Core.Indexers.HDBits
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
{
var torrentInfos = new List<ReleaseInfo>();
var indexerHttpResponse = indexerResponse.HttpResponse;
if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
if (indexerHttpResponse.StatusCode == HttpStatusCode.Forbidden)
{
throw new IndexerException(indexerResponse,
"Unexpected response status {0} code from API request",
indexerResponse.HttpResponse.StatusCode);
throw new RequestLimitReachedException(indexerResponse, "HDBits Query Limit Reached. Please try again later.");
}
if (indexerHttpResponse.StatusCode != HttpStatusCode.OK)
{
throw new IndexerException(indexerResponse, "Unexpected response status {0} code from API request", indexerHttpResponse.StatusCode);
}
var jsonResponse = JsonConvert.DeserializeObject<HDBitsResponse>(indexerResponse.Content);
if (jsonResponse.Status != StatusCode.Success)
{
throw new IndexerException(indexerResponse,
"HDBits API request returned status code {0}: {1}",
jsonResponse.Status,
jsonResponse.Message ?? string.Empty);
throw new IndexerException(indexerResponse, "HDBits API request returned status code {0}: {1}", jsonResponse.Status, jsonResponse.Message ?? string.Empty);
}
var responseData = jsonResponse.Data as JArray;
if (responseData == null)
{
throw new IndexerException(indexerResponse,
"Indexer API call response missing result data");
throw new IndexerException(indexerResponse, "Indexer API call response missing result data");
}
var queryResults = responseData.ToObject<TorrentQueryResponse[]>();

View File

@@ -171,7 +171,7 @@ namespace NzbDrone.Core.Indexers.Definitions
else
{
queryCollection.Add("options", "0");
queryCollection.Add("search", term);
queryCollection.Add("search", term.Replace(".", " "));
}
searchUrl += queryCollection.GetQueryString();

View File

@@ -24,7 +24,7 @@ namespace NzbDrone.Core.Indexers.Definitions
{
public override string Name => "HD-Torrents";
public override string[] IndexerUrls => new string[] { "https://hdts.ru/" };
public override string[] IndexerUrls => new string[] { "https://hdts.ru/", "https://hd-torrents.org/" };
public override string Description => "HD-Torrents is a private torrent website with HD torrents and strict rules on their content.";
private string LoginUrl => Settings.BaseUrl + "login.php";
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;

View File

@@ -170,16 +170,21 @@ namespace NzbDrone.Core.Indexers.Definitions
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories, string imdbId = null)
{
var searchUrl = Settings.BaseUrl + "browse.php";
var searchUrl = Settings.BaseUrl + "browse.php?";
if (term.IsNotNullOrWhiteSpace())
{
searchUrl += string.Format("?do=search&keywords={0}&search_type=t_name&category=0&include_dead_torrents=no", WebUtility.UrlEncode(term));
searchUrl += string.Format("do=search&keywords={0}&search_type=t_name&category=0&include_dead_torrents=no", WebUtility.UrlEncode(term));
}
if (categories != null && categories.Length > 0)
{
searchUrl += "&selectedcats2=" + string.Join(",", Capabilities.Categories.MapTorznabCapsToTrackers(categories));
if (term.IsNotNullOrWhiteSpace())
{
searchUrl += "&";
}
searchUrl += "selectedcats2=" + string.Join(",", Capabilities.Categories.MapTorznabCapsToTrackers(categories));
}
var request = new IndexerRequest(searchUrl, HttpAccept.Html);

View File

@@ -150,14 +150,14 @@ public class MoreThanTVRequestGenerator : IIndexerRequestGenerator
qc.Add("filter_cat[2]", "1"); // SD Movies
break;
case TvSearchCriteria:
qc.Add("filter_cat[3]", "1"); // HD EPISODE
qc.Add("filter_cat[3]", "1"); // HD Episode
qc.Add("filter_cat[4]", "1"); // SD Episode
qc.Add("filter_cat[5]", "1"); // HD Season
qc.Add("filter_cat[6]", "1"); // SD Season
break;
}
return $"{Settings.BaseUrl}torrents.php?{qc.GetQueryString()}";
return $"{Settings.BaseUrl}torrents/browse?{qc.GetQueryString()}";
}
private string GetSearchString(string input)
@@ -188,28 +188,25 @@ public class MoreThanTVParser : IParseIndexerResponse
foreach (var torrent in torrents)
{
// Parse required data
var torrentGroup = torrent.QuerySelectorAll("table a[href^=\"/torrents.php?action=download\"]");
foreach (var downloadAnchor in torrentGroup)
var downloadAnchor = torrent.QuerySelector("span a[href^=\"/torrents.php?action=download\"]");
var title = downloadAnchor.ParentElement.ParentElement.ParentElement.QuerySelector("a[class=\"overlay_torrent\"]").TextContent.Trim();
title = CleanUpTitle(title);
var category = torrent.QuerySelector(".cats_col div").GetAttribute("title");
// default to Other
var indexerCategory = NewznabStandardCategory.Other;
if (movies.Any(category.Contains))
{
var title = downloadAnchor.ParentElement.ParentElement.ParentElement.TextContent.Trim();
title = CleanUpTitle(title);
var category = torrent.QuerySelector(".cats_col div").GetAttribute("title");
// default to Other
var indexerCategory = NewznabStandardCategory.Other;
if (movies.Any(category.Contains))
{
indexerCategory = NewznabStandardCategory.Movies;
}
else if (tv.Any(category.Contains))
{
indexerCategory = NewznabStandardCategory.TV;
}
releases.Add(GetReleaseInfo(torrent, downloadAnchor, title, indexerCategory));
indexerCategory = NewznabStandardCategory.Movies;
}
else if (tv.Any(category.Contains))
{
indexerCategory = NewznabStandardCategory.TV;
}
releases.Add(GetReleaseInfo(torrent, downloadAnchor, title, indexerCategory));
}
return releases;
@@ -231,7 +228,7 @@ public class MoreThanTVParser : IParseIndexerResponse
private ReleaseInfo GetReleaseInfo(IElement row, IElement downloadAnchor, string title, IndexerCategory category)
{
// count from bottom
const int FILES_COL = 8;
const int FILES_COL = 7;
/*const int COMMENTS_COL = 7;*/
const int DATE_COL = 6;
const int FILESIZE_COL = 5;

View File

@@ -4,10 +4,13 @@ using System.Collections.Specialized;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using FluentValidation;
using Newtonsoft.Json;
using NLog;
using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers.Exceptions;
@@ -21,6 +24,8 @@ namespace NzbDrone.Core.Indexers.Definitions
{
public class MyAnonamouse : TorrentIndexerBase<MyAnonamouseSettings>
{
private static readonly Regex TorrentIdRegex = new Regex(@"tor/download.php\?tid=(?<id>\d+)$");
public override string Name => "MyAnonamouse";
public override string[] IndexerUrls => new string[] { "https://www.myanonamouse.net/" };
@@ -44,6 +49,47 @@ namespace NzbDrone.Core.Indexers.Definitions
return new MyAnonamouseParser(Settings, Capabilities.Categories);
}
public override async Task<byte[]> Download(Uri link)
{
if (Settings.Freeleech)
{
_logger.Debug($"Attempting to use freeleech token for {link.AbsoluteUri}");
var idMatch = TorrentIdRegex.Match(link.AbsoluteUri);
if (idMatch.Success)
{
var id = int.Parse(idMatch.Groups["id"].Value);
var timestamp = DateTimeOffset.Now.ToUnixTimeSeconds();
var freeleechUrl = Settings.BaseUrl + $"json/bonusBuy.php/{timestamp}";
var freeleechRequest = new HttpRequestBuilder(freeleechUrl)
.AddQueryParam("spendtype", "personalFL")
.AddQueryParam("torrentid", id)
.AddQueryParam("timestamp", timestamp.ToString())
.Build();
var indexerReq = new IndexerRequest(freeleechRequest);
var response = await FetchIndexerResponse(indexerReq).ConfigureAwait(false);
var resource = Json.Deserialize<MyAnonamouseFreeleechResponse>(response.Content);
if (resource.Success)
{
_logger.Debug($"Successfully to used freeleech token for torrentid ${id}");
}
else
{
_logger.Debug($"Failed to use freeleech token: ${resource.Error}");
}
}
else
{
_logger.Debug($"Could not get torrent id from link ${link.AbsoluteUri}, skipping freeleech");
}
}
return await base.Download(link).ConfigureAwait(false);
}
protected override IDictionary<string, string> GetCookies()
{
return CookieUtil.CookieHeaderToDictionary("mam_id=" + Settings.MamId);
@@ -400,7 +446,10 @@ namespace NzbDrone.Core.Indexers.Definitions
[FieldDefinition(3, Type = FieldType.Checkbox, Label = "Exclude VIP", HelpText = "Exclude VIP Torrents from search results")]
public bool ExcludeVip { get; set; }
[FieldDefinition(4)]
[FieldDefinition(4, Type = FieldType.Checkbox, Label = "Freeleech", HelpText = "Use freeleech token for download")]
public bool Freeleech { get; set; }
[FieldDefinition(5)]
public IndexerBaseSettings BaseSettings { get; set; } = new IndexerBaseSettings();
public NzbDroneValidationResult Validate()
@@ -438,4 +487,10 @@ namespace NzbDrone.Core.Indexers.Definitions
public string Error { get; set; }
public List<MyAnonamouseTorrent> Data { get; set; }
}
public class MyAnonamouseFreeleechResponse
{
public bool Success { get; set; }
public string Error { get; set; }
}
}

View File

@@ -25,26 +25,32 @@ namespace NzbDrone.Core.Indexers.PassThePopcorn
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
{
var torrentInfos = new List<ReleaseInfo>();
var indexerHttpResponse = indexerResponse.HttpResponse;
if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
if (indexerHttpResponse.StatusCode != HttpStatusCode.OK)
{
// Remove cookie cache
if (indexerResponse.HttpResponse.HasHttpRedirect && indexerResponse.HttpResponse.RedirectUrl
if (indexerHttpResponse.HasHttpRedirect && indexerHttpResponse.RedirectUrl
.ContainsIgnoreCase("login.php"))
{
CookiesUpdater(null, null);
throw new IndexerException(indexerResponse, "We are being redirected to the PTP login page. Most likely your session expired or was killed. Try testing the indexer in the settings.");
throw new IndexerAuthException("We are being redirected to the PTP login page. Most likely your session expired or was killed. Try testing the indexer in the settings.");
}
if (indexerHttpResponse.StatusCode == HttpStatusCode.Forbidden)
{
throw new RequestLimitReachedException(indexerResponse, "PTP Query Limit Reached. Please try again later.");
}
throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from API request");
}
if (indexerResponse.HttpResponse.Headers.ContentType != HttpAccept.Json.Value)
if (indexerHttpResponse.Headers.ContentType != HttpAccept.Json.Value)
{
if (indexerResponse.HttpResponse.Request.Url.Path.ContainsIgnoreCase("login.php"))
if (indexerHttpResponse.Request.Url.Path.ContainsIgnoreCase("login.php"))
{
CookiesUpdater(null, null);
throw new IndexerException(indexerResponse, "We are currently on the login page. Most likely your session expired or was killed. Try testing the indexer in the settings.");
throw new IndexerAuthException("We are currently on the login page. Most likely your session expired or was killed. Try testing the indexer in the settings.");
}
// Remove cookie cache

View File

@@ -26,7 +26,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public override string[] IndexerUrls => new string[] { "https://pornolab.net/" };
private string LoginUrl => Settings.BaseUrl + "forum/login.php";
public override string Description => "PornoLab is a Semi-Private Russian site for Adult content";
public override string Language => "ru-ru";
public override string Language => "ru-RU";
public override Encoding Encoding => Encoding.GetEncoding("windows-1251");
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
public override IndexerPrivacy Privacy => IndexerPrivacy.SemiPrivate;

View File

@@ -73,8 +73,8 @@ namespace NzbDrone.Core.Indexers.Definitions
caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.PC, "Applications");
caps.Categories.AddCategoryMapping(3, NewznabStandardCategory.BooksEBook, "E-Books");
caps.Categories.AddCategoryMapping(4, NewznabStandardCategory.AudioAudiobook, "Audiobooks");
caps.Categories.AddCategoryMapping(5, NewznabStandardCategory.MoviesOther, "E-Learning Videos");
caps.Categories.AddCategoryMapping(6, NewznabStandardCategory.TVOther, "Comedy");
caps.Categories.AddCategoryMapping(5, NewznabStandardCategory.Other, "E-Learning Videos");
caps.Categories.AddCategoryMapping(6, NewznabStandardCategory.Other, "Comedy");
caps.Categories.AddCategoryMapping(7, NewznabStandardCategory.BooksComics, "Comics");
return caps;

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,7 @@ using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.Indexers.Definitions.Xthor
{
[Obsolete("Moved to YML for Cardigann v5")]
public class Xthor : TorrentIndexerBase<XthorSettings>
{
public override string Name => "Xthor";

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