Compare commits

...

134 Commits

Author SHA1 Message Date
Bogdan
7890ef6f9d Remove invalid Turkish translations 2023-12-17 04:01:35 +02:00
Weblate
df8e4e5acb Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: lifeisfreedom048 <koyuncu.ozgur@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/tr/
Translation: Servarr/Prowlarr
2023-12-17 04:00:13 +02:00
Agneev Mukherjee
1b36951879 Enable browser navigation buttons for PWA
(cherry picked from commit da9a60691f363323565a293ed9eaeb6349ceccb6)
2023-12-16 15:44:25 +02:00
Weblate
d8d5170ab8 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Menno Liefstingh <mennoliefstingh@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
Translation: Servarr/Prowlarr
2023-12-15 16:43:07 +02:00
Bogdan
a443a87603 Fixed: (AvistaZ) Disable pagination and implement MST formula 2023-12-14 20:37:31 +02:00
Weblate
df18ee77e7 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: ROSERAT Ugo <roserat.ugo@gmail.com>
Co-authored-by: RicardoVelaC <ricardovelac@gmail.com>
Co-authored-by: SHUAI.W <x@ousui.org>
Co-authored-by: matiasba <matiasabarros@hotmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/lv/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2023-12-14 14:20:36 +02:00
Bogdan
426159b452 Fixed: (RetroFlix) Remove unrelated results 2023-12-13 21:41:13 +02:00
Bogdan
8704bef69a Update year format in titles for RED/OPS 2023-12-13 19:49:54 +02:00
Bogdan
0f1b01adab Log invalid torrent files contents as info 2023-12-12 16:53:06 +02:00
Qstick
4dbf5aa9f4 Fixed: Correctly handle Migration when PG Host has ".db"
(cherry picked from commit 97ee24507f4306e3b62c3d00cd3ade6a09d1b957)
2023-12-12 15:49:54 +02:00
Bogdan
e1264d7cda Implement DatabaseConnectionInfo
Co-authored-by: Qstick <qstick@gmail.com>
2023-12-12 15:49:50 +02:00
Bogdan
d4bbb2e14a Fixed: (FL/BHD/IPT) Filter releases by query 2023-12-10 20:06:33 +02:00
Servarr
a2395dccb5 Automated API Docs update 2023-12-10 16:08:08 +02:00
Bogdan
df89450428 New: External links for search results 2023-12-10 16:02:24 +02:00
Bogdan
7b5e1f40ba Bump version to 1.11.2 2023-12-10 13:44:44 +02:00
Bogdan
fe142b8a1c Fixed: (IPTorrents) Parse size column using index by name 2023-12-09 23:22:33 +02:00
Weblate
8c7f73ed16 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Jurriaan Den Toonder <jur.den.toonder@gmail.com>
Co-authored-by: RicardoVelaC <ricardovelac@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
Translation: Servarr/Prowlarr
2023-12-08 16:02:46 +02:00
Weblate
a4ae800603 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Augusto Poletti <augustpolet@gmail.com>
Co-authored-by: Dominika Matějková <dominika.matejkova@outlook.cz>
Co-authored-by: VisoTC <szlytlyt@outlook.com>
Co-authored-by: 米大饭 <1246333567@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2023-12-06 16:01:59 +02:00
Bogdan
e5d7a21714 New: (Discord) Add size grab field and improve field selection 2023-12-06 10:24:08 +02:00
Bogdan
67355ec07b Fixed: (aro.lol) Removed, site has shutdown 2023-12-05 14:00:02 +02:00
Bogdan
43d56f6817 New: (NorBits) Add FreeLeech Only option 2023-12-04 15:58:10 +02:00
Bogdan
515f4dff8b New: (NorBits) Login with 2FA support 2023-12-04 15:46:15 +02:00
Bogdan
c3aa377ed5 Fixed: (PTP) Parse half leech 2023-12-04 10:41:29 +02:00
Mark McDowall
fbc3c09094 Always validate Custom Script path
(cherry picked from commit c922cc5dc617dd776d4523cbf62376821c5a4ad9)
2023-12-03 20:38:54 +02:00
Maksym Ivanchenko
b88f8e5fde Fixed: (Toloka) Support RAW search (#1944) 2023-12-03 09:37:39 +02:00
Bogdan
aaf4c73c83 Bump version to 1.11.1 2023-12-03 08:46:25 +02:00
Bogdan
7c38ec2baa Fixed: (AvistaZ) Improve message for failed logins 2023-12-02 12:07:40 +02:00
Bogdan
dcfdd49119 Rollback YamlDotNet 2023-12-02 11:30:59 +02:00
Weblate
badb9dee61 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Appoxo <appoxo@appoxo.de>
Co-authored-by: David Molero <contact@dolvem.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Patatra <patrice.chevreau@gmail.com>
Co-authored-by: Zalhera <tobias.bechen@gmail.com>
Co-authored-by: liimee <git.taaa@fedora.email>
Co-authored-by: resi23 <x-resistant-x@gmx.de>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/id/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translation: Servarr/Prowlarr
2023-12-01 04:03:11 +02:00
Bogdan
12ca705556 Fixed: (AvistaZ) Increase rate limit and use workaround for Retry-After in auth 2023-11-30 01:57:32 +02:00
Bogdan
a0d0e3e232 Fixed: (OPS) Use canUseToken for download links when using Use Freeleech Tokens 2023-11-29 08:22:17 +02:00
Bogdan
e12cd68772 Fixed: (RED/OPS) Add log score to release titles 2023-11-29 08:18:24 +02:00
Bogdan
9dc117191e Minor cleanup for BTN 2023-11-28 09:22:13 +02:00
Bogdan
fdaca64d60 Fixed: (BTN) Add MST, indexer flags and parse IMDb ID 2023-11-28 03:54:47 +02:00
Bogdan
6d134750ff New: (Redacted) Add Freeload Only option 2023-11-27 12:06:18 +02:00
Bogdan
bbf9945b49 Add missing CSS typing 2023-11-27 05:34:03 +02:00
Bogdan
b66a85269f Add color to links in info inputs 2023-11-27 03:02:03 +02:00
Bogdan
81e9c29d54 Bump version to 1.11.0 2023-11-26 22:46:36 +02:00
Weblate
3df33e1a86 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Appoxo <appoxo@appoxo.de>
Co-authored-by: Charlie <zola@zipmail.pw>
Co-authored-by: Dimitri <dimitridroeck@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: RicardoVelaC <ricardovelac@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translation: Servarr/Prowlarr
2023-11-25 22:32:05 +02:00
Stevie Robinson
c19802c471 New: Remove defunct Boxcar notifications
(cherry picked from commit c6ad2396bb98dc8eb1ad47bf5d066b227a47f8b5)
2023-11-25 22:27:12 +02:00
Bogdan
36c8806f88 New: Add Freeleech Only for HDS, iS and XS 2023-11-25 22:22:56 +02:00
Bogdan
0d69a42ae0 Fix Localization test 2023-11-25 05:59:03 +02:00
Stevie Robinson
fc482d4808 Fixed: Fallback to English translations if invalid UI language in config
(cherry picked from commit 4c7201741276eccaea2fb1f33daecc31e8b2d54e)
2023-11-25 05:30:23 +02:00
Mark McDowall
fab4bd5ead Initial support to use named tokens for backend translations
(cherry picked from commit 11f96c31048c2d1aafca0c91736d439f7f9a95a8)
2023-11-25 05:18:48 +02:00
Bogdan
9c243c7e0d Fixed: (Redacted) Treat freeload torrents as neutral leech
Towards #1933
2023-11-24 22:36:40 +02:00
Bogdan
c107011659 Use album year in Redacted/Orpheus 2023-11-24 22:35:56 +02:00
Bogdan
c690e9a50a New: Add NeutralLeech to indexer flags 2023-11-24 22:31:00 +02:00
Bogdan
c4b0ecdafe Bump dotnet to 6.0.25 2023-11-23 23:44:05 +02:00
Bogdan
f2a709f725 Remove OZnzb from default definitions
Fixes #1932
2023-11-23 23:09:04 +02:00
Bogdan
01a9799ce8 BB has shutdown 2023-11-23 05:53:52 +02:00
Bogdan
8ac68e8946 Fixed: (Cardigann) Use of the default value for new select settings
Fixes #1929
2023-11-23 04:11:56 +02:00
Bogdan
6cb787c17b Fix title in tests 2023-11-22 07:57:37 +02:00
Bogdan
51259ceb30 Fixed: (HDBits) Enable Use Filenames by default 2023-11-22 07:26:43 +02:00
Weblate
92cf329174 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/is/
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/th/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/vi/
Translation: Servarr/Prowlarr
2023-11-21 17:49:53 +02:00
Bogdan
257c9ab248 Return 429 right away after release search 2023-11-20 02:38:26 +02:00
Servarr
cad42fd005 Automated API Docs update 2023-11-19 22:39:39 +02:00
Bogdan
d7927f60fe Cleanup appName tokens
Co-authored-by: Stevie Robinson <stevie.robinson@gmail.com>
2023-11-19 22:32:43 +02:00
Mark McDowall
26a657fa77 New: Require password confirmation when setting or changing password
(cherry picked from commit b248163df598dc611ee919d525eb7357256d73d5)
2023-11-19 22:20:29 +02:00
Mark McDowall
7fa4daae9b Fixed: Disable SSL when using the removed SslCertHash configuration
(cherry picked from commit d95660d3c78d1ee11a7966d58e78a82a8df01393)
2023-11-19 22:18:32 +02:00
Stevie Robinson
e1b063eaa5 Fixed: Replacing 'appName' translation token
(cherry picked from commit 2e51b8792db0d3ec402672dc92c95f3cb886ef44)
2023-11-19 22:17:28 +02:00
Bogdan
75f00436ec Bump version to 1.10.5 2023-11-19 07:05:56 +02:00
Bogdan
343d7088c9 Fixed: Don't die on info indexer when the definition is missing 2023-11-19 01:36:50 +02:00
Bogdan
709dfe453b Fix AB tests 2023-11-18 06:08:12 +02:00
Bogdan
3130fac106 New: (AnimeBytes) Filter old releases on RSS 2023-11-18 05:07:10 +02:00
Weblate
28004dfae1 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Dlgeri123 <bornemiszageri@gmail.com>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Jhonata da Rocha <jhonata182@gmail.com>
Co-authored-by: Jordy <prive@jordyhoebergen.nl>
Co-authored-by: LandonLi <lxx4work@gmail.com>
Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: bai0012 <baicongrui@gmail.com>
Co-authored-by: hpoon <henry.yh.poon@gmail.com>
Co-authored-by: jianl <jianjianfengyun@126.com>
Co-authored-by: 宿命 <331874545@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/cs/
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/hu/
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/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/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2023-11-18 03:13:20 +02:00
Bogdan
9b34c89bc8 Fixed: Enforce validation warnings when testing providers
(cherry picked from commit c3b4126d0c4f449a41e2cf7ea438b20e25370995)
2023-11-17 05:26:04 +02:00
Mark McDowall
28e90acd0d Rename 'ReturnUrl' to 'returnUrl' for forms auth redirection
(cherry picked from commit 812712e2843a738054c065a6d5c1b7c81c5f8e7b)
2023-11-17 05:25:41 +02:00
Erik P
9d11d7e17f Fixed: (PTP) Add IMDb ID to TV Search capabilities (#1920)
Co-authored-by: Erik Persson <erik@erikpersson.me>
2023-11-16 19:33:53 +02:00
Bogdan
2cbdb5bcba New: (HDBits) Add Use Filenames option 2023-11-16 18:50:24 +02:00
Bogdan
118bfb8c28 Fixed: (AvistaZ) Increase rate limit to 5 seconds 2023-11-16 18:50:20 +02:00
Bogdan
942477ecf9 Fixed: (HDBits) Add labels for codecs and mediums 2023-11-14 17:21:07 +02:00
Bogdan
4b4589ed27 Improvements to download factor and show Freeleech only for HDB 2023-11-13 16:23:10 +02:00
Bogdan
bd0609639e New: (HDBits) Add pagination support 2023-11-13 02:54:28 +02:00
Bogdan
ccdad3a44c Bump version to 1.10.4 2023-11-12 16:49:35 +02:00
Bogdan
d99da0481b Fix AvistaZ tests 2023-11-12 15:22:35 +02:00
Bogdan
da1965b18e Fixed: (AvistaZ) Fix PublishDate timezone
Fixes #1917
2023-11-12 14:58:56 +02:00
Bogdan
493114f4e8 Fixed: Record status for notifications on tests 2023-11-10 05:34:15 +02:00
Mark McDowall
6969326092 Don't store status results for invalid providers
(cherry picked from commit de23182d593e2284972103d505e66dd8d812dfdb)
(cherry picked from commit 44d8dbaac81706691124ae5f8317289f0a3e5d73)
2023-11-10 04:01:04 +02:00
Bogdan
95f899131d Fix count in OrpheusFixture 2023-11-07 15:38:49 +02:00
Bogdan
0ba4f3e692 Fixed: (Orpheus) Filter old releases on RSS 2023-11-07 15:03:50 +02:00
Bogdan
a7c00a0fd7 Fixed: (Redacted) Filter old releases on RSS 2023-11-07 15:03:49 +02:00
Bogdan
c84ff60ec9 Fixed: (PTP) Add TV search capabilities 2023-11-05 21:00:24 +02:00
Bogdan
b3f6f54e6e Fixed: (PTP) Add support for TV searches 2023-11-05 20:53:21 +02:00
Bogdan
ed272aaf74 Increase the timeout for CheckHealth command 2023-11-05 20:17:02 +02:00
Bogdan
c0b10f889b Prevent NullRef on header assert 2023-11-05 19:58:30 +02:00
Bogdan
bbfb92bbd8 Bump version to 1.10.3 2023-11-05 11:10:44 +02:00
Bogdan
793de05e3d Fixed: (Beyond-HD) Types filtering 2023-11-04 17:36:01 +02:00
Bogdan
1b1f9d16be Remove default definitions for dead indexers 2023-11-04 16:30:40 +02:00
Bogdan
051dea30c2 Fixed: (Beyond-HD) Category filtering 2023-11-04 16:27:39 +02:00
Bogdan
75d8a3d1d0 Fixed: (FileList) Change TZ to account DST 2023-11-02 19:40:38 +02:00
Bogdan
edf41e2ead Bump version to 1.10.2 2023-10-29 10:35:43 +02:00
Bogdan
c15c71386d New: Set busy timeout for SQLite
(cherry picked from commit 192eb7b62ae60f300a9371ce3ed2e0056b5a1f4d)
2023-10-29 01:28:13 +03:00
Bogdan
71a19efd9a Improve appearance of info fields 2023-10-27 20:54:50 +03:00
Bogdan
2c6c0fcc81 Allow 0 as value in download client Id validation 2023-10-25 17:11:48 +03:00
Bogdan
203e2dbb10 Remove mandatory validation for download client in indexers 2023-10-25 16:24:30 +03:00
Bogdan
6169fc2fa3 New: Add Download Client validation for indexers 2023-10-25 15:55:38 +03:00
Bogdan
768ce14afb Fix integration tests for indexers 2023-10-25 14:17:22 +03:00
Servarr
31d32e8c30 Automated API Docs update 2023-10-25 13:38:07 +03:00
Bogdan
7a61761b2b Add schema endpoint for app profiles 2023-10-25 13:25:16 +03:00
Bogdan
3963807c96 New: Add App Profile validation for indexers
Fixes #1903
2023-10-25 13:04:02 +03:00
Bogdan
e0f6726a3d Fixed: Detect Raw search in Generic Torznab/Newznab feeds
Fixes #1895
2023-10-25 11:51:31 +03:00
Weblate
dd25bff3d6 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dlgeri123 <bornemiszageri@gmail.com>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Jhonata da Rocha <jhonata182@gmail.com>
Co-authored-by: LandonLi <lxx4work@gmail.com>
Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: hpoon <henry.yh.poon@gmail.com>
Co-authored-by: jianl <jianjianfengyun@126.com>
Co-authored-by: 宿命 <331874545@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2023-10-25 11:26:16 +03:00
Jordy
d834c4292e Update link to Docker instructions in readme (#1905) 2023-10-25 11:16:13 +03:00
Bogdan
7e8272ec2b Bump version to 1.10.1 2023-10-22 09:32:32 +03:00
Bogdan
62548f32fe Fixed: (FileList) Skip ID searches for daily episodes 2023-10-21 12:46:50 +03:00
Bogdan
db9f061564 Return TV category if season/episode detected in title for PTP 2023-10-20 00:13:55 +03:00
Bogdan
b37d8799a0 Add acronym for PrivateHD 2023-10-16 18:45:06 +03:00
DaftFuzz
4366530409 Fixed: Calculating value for peers filter (#1900)
Co-authored-by: Bogdan <mynameisbogdan@users.noreply.github.com>
2023-10-16 18:21:40 +03:00
Bogdan
c7959f735e Bump version to 1.10.0 2023-10-15 21:46:33 +03:00
Gabe
be3ee00e1f Fixed: Remove Defunct Tracker PirateTheNet
PTN closed down for good.

They had an official announcement to close down on the 14th October.

Aither offered to accept refugees. See /forums/topics/2535 on aither for more context.
Description

The definition is no longer needed as the tracker ceased all operation.
2023-10-15 19:22:06 +03:00
Bogdan
dace1982d6 Fixed: (FileList) Use air date in search query 2023-10-15 17:44:50 +03:00
Bogdan
980bd35f95 Revert dependencies update 2023-10-14 15:34:41 +03:00
Bogdan
4b2f81bee8 Update postcss, webpack and core-js 2023-10-14 15:19:56 +03:00
Bogdan
30eb481c65 Add year to releases for AnimeBytes 2023-10-14 10:17:54 +03:00
Bogdan
29f1c36f54 Prevent NullRef in Nebulance with null TvMazeIds 2023-10-13 18:55:04 +03:00
Bogdan
f1c01343bf New: (Nebulance) Parse TvMazeId and scene attributes 2023-10-13 14:27:32 +03:00
Weblate
bae79b22ad Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: DavidHenryThoreau <sorau@protonmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translation: Servarr/Prowlarr
2023-10-13 11:55:11 +03:00
Bogdan
229d879f86 New: Show indexer categories in Add Indexer modal 2023-10-13 09:39:51 +03:00
bakerboy448
d1cee950a4 Fixed: New Indexer URL for DICMusic 2023-10-13 04:21:09 +03:00
Bogdan
7e32b54547 Fixed: Prevent NullRef in NewznabRequestGenerator for missing definition 2023-10-12 06:50:13 +03:00
Bogdan
b1f7d30021 Fixed: Ignore case when cleansing announce URLs 2023-10-12 05:10:26 +03:00
Mark McDowall
c41a7e0ccc Fixed: Duplicate notifications for failed health checks
(cherry picked from commit c0e54773e213f526a5046fa46aa7b57532471128)

Mock debouncer for testing

(cherry picked from commit bb7b2808e2f70389157408809ec47cc8860b4938)
2023-10-11 03:34:18 +03:00
Weblate
42c533386b Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Timo <Tclemens@live.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: blankhang <blankhang@gmail.com>
Co-authored-by: 宿命 <331874545@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2023-10-11 02:52:16 +03:00
Servarr
bdae7a2cdc Automated API Docs update 2023-10-11 02:51:54 +03:00
Mark McDowall
5e8d3542f4 Log executing health check
(cherry picked from commit 78b39bd2fecda60e04a1fef17ae17f62bd2b6914)
2023-10-11 02:33:38 +03:00
Mark McDowall
d9d2aa8493 Sync label styling for FormLabel
(cherry picked from commit 87e0a7983a437a4d166aa8b9c9eaf78ea5431969)
2023-10-11 02:30:18 +03:00
Stevie Robinson
09bf1500d6 New: Additional tooltips for icon buttons
(cherry picked from commit 8c07f0d3d19a48ed96d1ded54399c66bf2977b2a)
2023-10-11 02:24:32 +03:00
Mark McDowall
34464160cb Paging params in API docs
(cherry picked from commit bfaa7291e14a8d3847ef2154a52c363944560803)
2023-10-11 02:17:27 +03:00
Bogdan
bada5fe309 New: Add TorrentNetwork 2023-10-10 13:29:26 +03:00
Bogdan
b088febbc4 Use async for http client test 2023-10-10 03:05:16 +03:00
Bogdan
1a307b8e21 Fix test for http client 2023-10-10 03:02:35 +03:00
Bogdan
32db2af0ea Improved http timeout handling
Co-authored-by: Qstick <qstick@gmail.com>
2023-10-10 02:41:05 +03:00
Bogdan
e602862102 Fixed: (FileList) Remove dead domain 2023-10-08 21:31:38 +03:00
Bogdan
bd5336e4c4 Bump version to 1.9.4 2023-10-08 07:08:25 +03:00
188 changed files with 2877 additions and 1996 deletions

View File

@@ -2,7 +2,7 @@
[![Build Status](https://dev.azure.com/Prowlarr/Prowlarr/_apis/build/status/Prowlarr.Prowlarr?branchName=develop)](https://dev.azure.com/Prowlarr/Prowlarr/_build/latest?definitionId=1&branchName=develop)
[![Translated](https://translate.servarr.com/widgets/servarr/-/prowlarr/svg-badge.svg)](https://translate.servarr.com/engage/prowlarr/?utm_source=widget)
[![Docker Pulls](https://img.shields.io/docker/pulls/hotio/prowlarr.svg)](https://wiki.servarr.com/prowlarr/installation#docker)
[![Docker Pulls](https://img.shields.io/docker/pulls/hotio/prowlarr.svg)](https://wiki.servarr.com/prowlarr/installation/docker)
![Github Downloads](https://img.shields.io/github/downloads/Prowlarr/Prowlarr/total.svg)
[![Backers on Open Collective](https://opencollective.com/Prowlarr/backers/badge.svg)](#backers)
[![Sponsors on Open Collective](https://opencollective.com/Prowlarr/sponsors/badge.svg)](#sponsors)

View File

@@ -9,13 +9,13 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '1.9.3'
majorVersion: '1.11.2'
minorVersion: $[counter('minorVersion', 1)]
prowlarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.413'
dotnetVersion: '6.0.417'
nodeVersion: '16.X'
innoVersion: '6.2.0'
windowsImage: 'windows-2022'

View File

@@ -65,12 +65,12 @@ function AppUpdatedModalContent(props) {
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('AppUpdated', { appName: 'Prowlarr' })}
{translate('AppUpdated')}
</ModalHeader>
<ModalBody>
<div>
<InlineMarkdown data={translate('AppUpdatedVersion', { appName: 'Prowlarr', version })} blockClassName={styles.version} />
<InlineMarkdown data={translate('AppUpdatedVersion', { version })} blockClassName={styles.version} />
</div>
{

View File

@@ -28,11 +28,11 @@ function ConnectionLostModal(props) {
<ModalBody>
<div>
{translate('ConnectionLostToBackend', { appName: 'Prowlarr' })}
{translate('ConnectionLostToBackend')}
</div>
<div className={styles.automatic}>
{translate('ConnectionLostReconnect', { appName: 'Prowlarr' })}
{translate('ConnectionLostReconnect')}
</div>
</ModalBody>
<ModalFooter>

View File

@@ -2,8 +2,10 @@
display: flex;
justify-content: flex-end;
margin-right: $formLabelRightMarginWidth;
padding-top: 8px;
min-height: 35px;
text-align: end;
font-weight: bold;
line-height: 35px;
}
.hasError {

View File

@@ -0,0 +1,13 @@
.message {
composes: alert from '~Components/Alert.css';
a {
color: var(--linkColor);
text-decoration: none;
&:hover {
color: var(--linkHoverColor);
text-decoration: underline;
}
}
}

View File

@@ -0,0 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'message': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -1,5 +1,8 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import { kinds } from 'Helpers/Props';
import styles from './InfoInput.css';
class InfoInput extends Component {
@@ -7,12 +10,15 @@ class InfoInput extends Component {
// Render
render() {
const {
value
} = this.props;
const { value } = this.props;
return (
<span dangerouslySetInnerHTML={{ __html: value }} />
<Alert
kind={kinds.INFO}
className={styles.message}
>
<span dangerouslySetInnerHTML={{ __html: value }} />
</Alert>
);
}
}

View File

@@ -3,6 +3,7 @@ import React from 'react';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './ModalContent.css';
function ModalContent(props) {
@@ -28,6 +29,7 @@ function ModalContent(props) {
<Icon
name={icons.CLOSE}
size={18}
title={translate('Close')}
/>
</Link>
}

View File

@@ -78,6 +78,7 @@ class PageHeader extends Component {
aria-label="Donate"
to="https://prowlarr.com/donate"
size={14}
title={translate('Donate')}
/>
<IconButton
className={styles.translate}

View File

@@ -24,6 +24,7 @@ function PageHeaderActionsMenu(props) {
<MenuButton className={styles.menuButton} aria-label="Menu Button">
<Icon
name={icons.INTERACTIVE}
title={translate('Menu')}
/>
</MenuButton>

View File

@@ -15,5 +15,5 @@
"start_url": "../../../../",
"theme_color": "#3a3f51",
"background_color": "#3a3f51",
"display": "standalone"
"display": "minimal-ui"
}

View File

@@ -34,7 +34,8 @@ function AuthenticationRequiredModalContent(props) {
authenticationMethod,
authenticationRequired,
username,
password
password,
passwordConfirmation
} = settings;
const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none';
@@ -63,7 +64,7 @@ function AuthenticationRequiredModalContent(props) {
className={styles.authRequiredAlert}
kind={kinds.WARNING}
>
{translate('AuthenticationRequiredWarning', { appName: 'Prowlarr' })}
{translate('AuthenticationRequiredWarning')}
</Alert>
{
@@ -76,7 +77,7 @@ function AuthenticationRequiredModalContent(props) {
type={inputTypes.SELECT}
name="authenticationMethod"
values={authenticationMethodOptions}
helpText={translate('AuthenticationMethodHelpText', { appName: 'Prowlarr' })}
helpText={translate('AuthenticationMethodHelpText')}
helpTextWarning={authenticationMethod.value === 'none' ? translate('AuthenticationMethodHelpTextWarning') : undefined}
helpLink="https://wiki.servarr.com/prowlarr/faq#forced-authentication"
onChange={onInputChange}
@@ -120,6 +121,18 @@ function AuthenticationRequiredModalContent(props) {
{...password}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('PasswordConfirmation')}</FormLabel>
<FormInputGroup
type={inputTypes.PASSWORD}
name="passwordConfirmation"
onChange={onInputChange}
helpTextWarning={passwordConfirmation?.value ? undefined : translate('AuthenticationRequiredPasswordConfirmationHelpTextWarning')}
{...passwordConfirmation}
/>
</FormGroup>
</div> :
null
}

View File

@@ -1,6 +1,7 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import AddIndexerModalContentConnector from './AddIndexerModalContentConnector';
import styles from './AddIndexerModal.css';
@@ -8,6 +9,7 @@ function AddIndexerModal({ isOpen, onModalClose, onSelectIndexer, ...otherProps
return (
<Modal
isOpen={isOpen}
size={sizes.EXTRA_LARGE}
onModalClose={onModalClose}
className={styles.modal}
>

View File

@@ -16,7 +16,7 @@ import TableBody from 'Components/Table/TableBody';
import { kinds, scrollDirections } from 'Helpers/Props';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import SelectIndexerRowConnector from './SelectIndexerRowConnector';
import SelectIndexerRow from './SelectIndexerRow';
import styles from './AddIndexerModalContent.css';
const columns = [
@@ -49,6 +49,12 @@ const columns = [
label: () => translate('Privacy'),
isSortable: true,
isVisible: true
},
{
name: 'categories',
label: () => translate('Categories'),
isSortable: false,
isVisible: true
}
];
@@ -260,7 +266,7 @@ class AddIndexerModalContent extends Component {
<TableBody>
{
filteredIndexers.map((indexer) => (
<SelectIndexerRowConnector
<SelectIndexerRow
key={`${indexer.implementation}-${indexer.name}`}
implementation={indexer.implementation}
implementationName={indexer.implementationName}

View File

@@ -1,15 +1,18 @@
import { some } from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchIndexerSchema, selectIndexerSchema, setIndexerSchemaSort } from 'Store/Actions/indexerActions';
import createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import AddIndexerModalContent from './AddIndexerModalContent';
function createMapStateToProps() {
return createSelector(
createClientSideCollectionSelector('indexers.schema'),
(indexers) => {
createAllIndexersSelector(),
(indexers, allIndexers) => {
const {
isFetching,
isPopulated,
@@ -19,11 +22,19 @@ function createMapStateToProps() {
sortKey
} = indexers;
const indexerList = items.map((item) => {
const { definitionName } = item;
return {
...item,
isExistingIndexer: some(allIndexers, { definitionName })
};
});
return {
isFetching,
isPopulated,
error,
indexers: items,
indexers: indexerList,
sortKey,
sortDirection
};

View File

@@ -1,90 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Icon from 'Components/Icon';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRowButton from 'Components/Table/TableRowButton';
import { icons } from 'Helpers/Props';
import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel';
import firstCharToUpper from 'Utilities/String/firstCharToUpper';
import translate from 'Utilities/String/translate';
import styles from './SelectIndexerRow.css';
class SelectIndexerRow extends Component {
//
// Listeners
onPress = () => {
const {
implementation,
implementationName,
name
} = this.props;
this.props.onIndexerSelect({ implementation, implementationName, name });
};
//
// Render
render() {
const {
protocol,
privacy,
name,
language,
description,
isExistingIndexer
} = this.props;
return (
<TableRowButton onPress={this.onPress}>
<TableRowCell className={styles.protocol}>
<ProtocolLabel
protocol={protocol}
/>
</TableRowCell>
<TableRowCell>
{name}
{
isExistingIndexer ?
<Icon
className={styles.alreadyExistsIcon}
name={icons.CHECK_CIRCLE}
size={15}
title={translate('IndexerAlreadySetup')}
/> :
null
}
</TableRowCell>
<TableRowCell>
{language}
</TableRowCell>
<TableRowCell>
{description}
</TableRowCell>
<TableRowCell>
{translate(firstCharToUpper(privacy))}
</TableRowCell>
</TableRowButton>
);
}
}
SelectIndexerRow.propTypes = {
name: PropTypes.string.isRequired,
protocol: PropTypes.string.isRequired,
privacy: PropTypes.string.isRequired,
language: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
implementation: PropTypes.string.isRequired,
implementationName: PropTypes.string.isRequired,
onIndexerSelect: PropTypes.func.isRequired,
isExistingIndexer: PropTypes.bool.isRequired
};
export default SelectIndexerRow;

View File

@@ -0,0 +1,75 @@
import React, { useCallback } from 'react';
import Icon from 'Components/Icon';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRowButton from 'Components/Table/TableRowButton';
import { icons } from 'Helpers/Props';
import CapabilitiesLabel from 'Indexer/Index/Table/CapabilitiesLabel';
import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel';
import { IndexerCapabilities } from 'Indexer/Indexer';
import firstCharToUpper from 'Utilities/String/firstCharToUpper';
import translate from 'Utilities/String/translate';
import styles from './SelectIndexerRow.css';
interface SelectIndexerRowProps {
name: string;
protocol: string;
privacy: string;
language: string;
description: string;
capabilities: IndexerCapabilities;
implementation: string;
implementationName: string;
isExistingIndexer: boolean;
onIndexerSelect(...args: unknown[]): void;
}
function SelectIndexerRow(props: SelectIndexerRowProps) {
const {
name,
protocol,
privacy,
language,
description,
capabilities,
implementation,
implementationName,
isExistingIndexer,
onIndexerSelect,
} = props;
const onPress = useCallback(() => {
onIndexerSelect({ implementation, implementationName, name });
}, [implementation, implementationName, name, onIndexerSelect]);
return (
<TableRowButton onPress={onPress}>
<TableRowCell className={styles.protocol}>
<ProtocolLabel protocol={protocol} />
</TableRowCell>
<TableRowCell>
{name}
{isExistingIndexer ? (
<Icon
className={styles.alreadyExistsIcon}
name={icons.CHECK_CIRCLE}
size={15}
title={translate('IndexerAlreadySetup')}
/>
) : null}
</TableRowCell>
<TableRowCell>{language}</TableRowCell>
<TableRowCell>{description}</TableRowCell>
<TableRowCell>{translate(firstCharToUpper(privacy))}</TableRowCell>
<TableRowCell>
<CapabilitiesLabel capabilities={capabilities} />
</TableRowCell>
</TableRowButton>
);
}
export default SelectIndexerRow;

View File

@@ -1,18 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createExistingIndexerSelector from 'Store/Selectors/createExistingIndexerSelector';
import SelectIndexerRow from './SelectIndexerRow';
function createMapStateToProps() {
return createSelector(
createExistingIndexerSelector(),
(isExistingIndexer, dimensions) => {
return {
isExistingIndexer
};
}
);
}
export default connect(createMapStateToProps)(SelectIndexerRow);

View File

@@ -1,3 +1,4 @@
import { uniqBy } from 'lodash';
import React from 'react';
import Label from 'Components/Label';
import { IndexerCapabilities } from 'Indexer/Indexer';
@@ -23,14 +24,18 @@ function CapabilitiesLabel(props: CapabilitiesLabelProps) {
);
}
const nameList = Array.from(
new Set(filteredList.map((item) => item.name).sort())
const indexerCategories = uniqBy(filteredList, 'id').sort(
(a, b) => a.id - b.id
);
return (
<span>
{nameList.map((category) => {
return <Label key={category}>{category}</Label>;
{indexerCategories.map((category) => {
return (
<Label key={category.id} title={`${category.id}`}>
{category.name}
</Label>
);
})}
{filteredList.length === 0 ? <Label>{'None'}</Label> : null}

View File

@@ -3,6 +3,7 @@ import React, { useCallback, useState } from 'react';
import { useSelector } from 'react-redux';
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
import { createSelector } from 'reselect';
import Alert from 'Components/Alert';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription';
@@ -23,7 +24,7 @@ import TagListConnector from 'Components/TagListConnector';
import { kinds } from 'Helpers/Props';
import DeleteIndexerModal from 'Indexer/Delete/DeleteIndexerModal';
import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector';
import Indexer from 'Indexer/Indexer';
import Indexer, { IndexerCapabilities } from 'Indexer/Indexer';
import { createIndexerSelectorForHook } from 'Store/Selectors/createIndexerSelector';
import translate from 'Utilities/String/translate';
import IndexerHistory from './History/IndexerHistory';
@@ -63,7 +64,7 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
fields,
tags,
protocol,
capabilities,
capabilities = {} as IndexerCapabilities,
} = indexer as Indexer;
const { onModalClose } = props;
@@ -207,7 +208,7 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
descriptionClassName={styles.description}
title={translate('RawSearchSupported')}
data={
capabilities.supportsRawSearch
capabilities?.supportsRawSearch
? translate('Yes')
: translate('No')
}
@@ -216,12 +217,12 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
descriptionClassName={styles.description}
title={translate('SearchTypes')}
data={
capabilities.searchParams.length === 0 ? (
translate('NotSupported')
) : (
capabilities?.searchParams?.length > 0 ? (
<Label kind={kinds.PRIMARY}>
{capabilities.searchParams[0]}
</Label>
) : (
translate('NotSupported')
)
}
/>
@@ -229,60 +230,60 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
descriptionClassName={styles.description}
title={translate('TVSearchTypes')}
data={
capabilities.tvSearchParams.length === 0
? translate('NotSupported')
: capabilities.tvSearchParams.map((p) => {
capabilities?.tvSearchParams?.length > 0
? capabilities.tvSearchParams.map((p) => {
return (
<Label key={p} kind={kinds.PRIMARY}>
{p}
</Label>
);
})
: translate('NotSupported')
}
/>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('MovieSearchTypes')}
data={
capabilities.movieSearchParams.length === 0
? translate('NotSupported')
: capabilities.movieSearchParams.map((p) => {
capabilities?.movieSearchParams?.length > 0
? capabilities.movieSearchParams.map((p) => {
return (
<Label key={p} kind={kinds.PRIMARY}>
{p}
</Label>
);
})
: translate('NotSupported')
}
/>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('BookSearchTypes')}
data={
capabilities.bookSearchParams.length === 0
? translate('NotSupported')
: capabilities.bookSearchParams.map((p) => {
capabilities?.bookSearchParams?.length > 0
? capabilities.bookSearchParams.map((p) => {
return (
<Label key={p} kind={kinds.PRIMARY}>
{p}
</Label>
);
})
: translate('NotSupported')
}
/>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('MusicSearchTypes')}
data={
capabilities.musicSearchParams.length === 0
? translate('NotSupported')
: capabilities.musicSearchParams.map((p) => {
capabilities?.musicSearchParams?.length > 0
? capabilities.musicSearchParams.map((p) => {
return (
<Label key={p} kind={kinds.PRIMARY}>
{p}
</Label>
);
})
: translate('NotSupported')
}
/>
</DescriptionList>
@@ -338,7 +339,11 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
})}
</Table>
</FieldSet>
) : null}
) : (
<Alert kind={kinds.INFO}>
{translate('NoIndexerCategories')}
</Alert>
)}
</div>
</TabPanel>
<TabPanel>

View File

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

View File

@@ -0,0 +1,36 @@
import React from 'react';
import Label from 'Components/Label';
import Tooltip from 'Components/Tooltip/Tooltip';
import { kinds, tooltipPositions } from 'Helpers/Props';
import { IndexerCategory } from 'Indexer/Indexer';
import translate from 'Utilities/String/translate';
interface CategoryLabelProps {
categories: IndexerCategory[];
}
function CategoryLabel({ categories = [] }: CategoryLabelProps) {
if (categories?.length === 0) {
return (
<Tooltip
anchor={<Label kind={kinds.DANGER}>{translate('Unknown')}</Label>}
tooltip="Please report this issue to the GitHub as this shouldn't be happening"
position={tooltipPositions.LEFT}
/>
);
}
const sortedCategories = categories
.filter((cat) => cat.name !== undefined)
.sort((a, b) => a.id - b.id);
return (
<span>
{sortedCategories.map((category) => {
return <Label key={category.id}>{category.name}</Label>;
})}
</span>
);
}
export default CategoryLabel;

View File

@@ -0,0 +1,13 @@
.links {
margin: 0;
}
.link {
white-space: nowrap;
}
.linkLabel {
composes: label from '~Components/Label.css';
cursor: pointer;
}

View File

@@ -0,0 +1,9 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'link': string;
'linkLabel': string;
'links': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -0,0 +1,90 @@
import React from 'react';
import Label from 'Components/Label';
import Link from 'Components/Link/Link';
import { kinds, sizes } from 'Helpers/Props';
import { IndexerCategory } from 'Indexer/Indexer';
import styles from './ReleaseLinks.css';
interface ReleaseLinksProps {
categories: IndexerCategory[];
imdbId?: string;
tmdbId?: number;
tvdbId?: number;
tvMazeId?: number;
}
function ReleaseLinks(props: ReleaseLinksProps) {
const { categories = [], imdbId, tmdbId, tvdbId, tvMazeId } = props;
const categoryNames = categories
.filter((item) => item.id < 100000)
.map((c) => c.name);
return (
<div className={styles.links}>
{imdbId ? (
<Link
className={styles.link}
to={`https://imdb.com/title/tt${imdbId.toString().padStart(7, '0')}/`}
>
<Label
className={styles.linkLabel}
kind={kinds.INFO}
size={sizes.LARGE}
>
IMDb
</Label>
</Link>
) : null}
{tmdbId ? (
<Link
className={styles.link}
to={`https://www.themoviedb.org/${
categoryNames.includes('Movies') ? 'movie' : 'tv'
}/${tmdbId}`}
>
<Label
className={styles.linkLabel}
kind={kinds.INFO}
size={sizes.LARGE}
>
TMDb
</Label>
</Link>
) : null}
{tvdbId ? (
<Link
className={styles.link}
to={`https://www.thetvdb.com/?tab=series&id=${tvdbId}`}
>
<Label
className={styles.linkLabel}
kind={kinds.INFO}
size={sizes.LARGE}
>
TVDb
</Label>
</Link>
) : null}
{tvMazeId ? (
<Link
className={styles.link}
to={`https://www.tvmaze.com/shows/${tvMazeId}/_`}
>
<Label
className={styles.linkLabel}
kind={kinds.INFO}
size={sizes.LARGE}
>
TV Maze
</Label>
</Link>
) : null}
</div>
);
}
export default ReleaseLinks;

View File

@@ -63,7 +63,7 @@
}
.externalLinks {
margin: 0 2px;
width: 22px;
text-align: center;
composes: button from '~Components/Link/IconButton.css';
color: var(--textColor);
}

View File

@@ -16,6 +16,7 @@ import titleCase from 'Utilities/String/titleCase';
import translate from 'Utilities/String/translate';
import CategoryLabel from './CategoryLabel';
import Peers from './Peers';
import ReleaseLinks from './ReleaseLinks';
import styles from './SearchIndexRow.css';
function getDownloadIcon(isGrabbing, isGrabbed, grabError) {
@@ -118,6 +119,10 @@ class SearchIndexRow extends Component {
grabs,
seeders,
leechers,
imdbId,
tmdbId,
tvdbId,
tvMazeId,
indexerFlags,
columns,
isGrabbing,
@@ -343,6 +348,32 @@ class SearchIndexRow extends Component {
/> :
null
}
{
imdbId || tmdbId || tvdbId || tvMazeId ? (
<Popover
anchor={
<Icon
className={styles.externalLinks}
name={icons.EXTERNAL_LINK}
size={12}
/>
}
title={translate('Links')}
body={
<ReleaseLinks
categories={categories}
imdbId={imdbId}
tmdbId={tmdbId}
tvdbId={tvdbId}
tvMazeId={tvMazeId}
/>
}
kind={kinds.INVERSE}
position={tooltipPositions.TOP}
/>
) : null
}
</VirtualTableRowCell>
);
}
@@ -375,6 +406,10 @@ SearchIndexRow.propTypes = {
grabs: PropTypes.number,
seeders: PropTypes.number,
leechers: PropTypes.number,
imdbId: PropTypes.number,
tmdbId: PropTypes.number,
tvdbId: PropTypes.number,
tvMazeId: PropTypes.number,
indexerFlags: PropTypes.arrayOf(PropTypes.string).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onGrabPress: PropTypes.func.isRequired,

View File

@@ -124,6 +124,7 @@ class SecuritySettings extends Component {
authenticationRequired,
username,
password,
passwordConfirmation,
apiKey,
certificateValidation
} = settings;
@@ -139,8 +140,8 @@ class SecuritySettings extends Component {
type={inputTypes.SELECT}
name="authenticationMethod"
values={authenticationMethodOptions}
helpText={translate('AuthenticationMethodHelpText', { appName: 'Prowlarr' })}
helpTextWarning={translate('AuthenticationRequiredWarning', { appName: 'Prowlarr' })}
helpText={translate('AuthenticationMethodHelpText')}
helpTextWarning={translate('AuthenticationRequiredWarning')}
onChange={onInputChange}
{...authenticationMethod}
/>
@@ -193,6 +194,21 @@ class SecuritySettings extends Component {
null
}
{
authenticationEnabled ?
<FormGroup>
<FormLabel>{translate('PasswordConfirmation')}</FormLabel>
<FormInputGroup
type={inputTypes.PASSWORD}
name="passwordConfirmation"
onChange={onInputChange}
{...passwordConfirmation}
/>
</FormGroup> :
null
}
<FormGroup>
<FormLabel>{translate('ApiKey')}</FormLabel>

View File

@@ -176,6 +176,13 @@ class UISettings extends Component {
helpTextWarning={translate('UILanguageHelpTextWarning')}
onChange={onInputChange}
{...settings.uiLanguage}
errors={
languages.some((language) => language.key === settings.uiLanguage.value) ?
settings.uiLanguage.errors :
[
...settings.uiLanguage.errors,
{ message: translate('InvalidUILanguage') }
]}
/>
</FormGroup>
</FieldSet>

View File

@@ -3,7 +3,7 @@ import React from 'react';
import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions';
import Icon from 'Components/Icon';
import { filterBuilderTypes, filterBuilderValueTypes, icons, sortDirections } from 'Helpers/Props';
import { filterBuilderTypes, filterBuilderValueTypes, filterTypePredicates, icons, sortDirections } from 'Helpers/Props';
import { createThunk, handleThunks } from 'Store/thunks';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import getSectionState from 'Utilities/State/getSectionState';
@@ -169,6 +169,18 @@ export const defaultState = {
}
],
filterPredicates: {
peers: function(item, filterValue, type) {
const predicate = filterTypePredicates[type];
const seeders = item.seeders || 0;
const leechers = item.leechers || 0;
const peers = seeders + leechers;
return predicate(peers, filterValue);
}
},
filterBuilderProps: [
{
name: 'title',

View File

@@ -114,7 +114,7 @@ class Updates extends Component {
/>
<div className={styles.message}>
{translate('TheLatestVersionIsAlreadyInstalled', { appName: 'Prowlarr' })}
{translate('TheLatestVersionIsAlreadyInstalled')}
</div>
{

View File

@@ -25,20 +25,18 @@ export async function fetchTranslations(): Promise<boolean> {
export default function translate(
key: string,
tokens?: Record<string, string | number | boolean>
tokens: Record<string, string | number | boolean> = {}
) {
const translation = translations[key] || key;
if (tokens) {
// Fallback to the old behaviour for translations not yet updated to use named tokens
Object.values(tokens).forEach((value, index) => {
tokens[index] = value;
});
tokens.appName = 'Prowlarr';
return translation.replace(/\{([a-z0-9]+?)\}/gi, (match, tokenMatch) =>
String(tokens[tokenMatch] ?? match)
);
}
// Fallback to the old behaviour for translations not yet updated to use named tokens
Object.values(tokens).forEach((value, index) => {
tokens[index] = value;
});
return translation;
return translation.replace(/\{([a-z0-9]+?)\}/gi, (match, tokenMatch) =>
String(tokens[tokenMatch] ?? match)
);
}

View File

@@ -29,7 +29,7 @@
"@fortawesome/free-solid-svg-icons": "6.4.0",
"@fortawesome/react-fontawesome": "0.2.0",
"@juggle/resize-observer": "3.4.0",
"@microsoft/signalr": "6.0.21",
"@microsoft/signalr": "6.0.25",
"@sentry/browser": "7.51.2",
"@sentry/integrations": "7.51.2",
"@types/node": "18.15.11",
@@ -105,7 +105,7 @@
"babel-loader": "9.1.3",
"babel-plugin-inline-classnames": "2.0.1",
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
"core-js": "3.32.1",
"core-js": "3.33.0",
"css-loader": "6.7.3",
"css-modules-typescript-loader": "4.0.1",
"eslint": "8.45.0",
@@ -122,7 +122,7 @@
"html-webpack-plugin": "5.5.1",
"loader-utils": "^3.2.1",
"mini-css-extract-plugin": "2.7.5",
"postcss": "8.4.23",
"postcss": "8.4.31",
"postcss-color-function": "4.1.0",
"postcss-loader": "7.3.0",
"postcss-mixins": "9.0.4",
@@ -141,7 +141,7 @@
"ts-loader": "9.4.2",
"typescript-plugin-css-modules": "5.0.1",
"url-loader": "4.1.1",
"webpack": "5.88.1",
"webpack": "5.89.0",
"webpack-cli": "5.1.4",
"webpack-livereload-plugin": "3.0.2"
}

View File

@@ -128,6 +128,16 @@ namespace NzbDrone.Common.Test.Http
response.Content.Should().NotBeNullOrWhiteSpace();
}
[Test]
public void should_throw_timeout_request()
{
var request = new HttpRequest($"https://{_httpBinHost}/delay/10");
request.RequestTimeout = new TimeSpan(0, 0, 5);
Assert.ThrowsAsync<WebException>(async () => await Subject.ExecuteAsync(request));
}
[Test]
public void should_execute_https_get()
{

View File

@@ -89,8 +89,16 @@ namespace NzbDrone.Common.Test.InstrumentationTests
// Download Station
[TestCase(@"webapi/entry.cgi?api=(removed)&version=2&method=login&account=01233210&passwd=mySecret&format=sid&session=DownloadStation")]
// Tracker Responses
[TestCase(@"tracker"":""http://xxx.yyy/announce.php?passkey=9pr04sg601233210imaveql2tyu8xyui"",""info"":""http://xxx.yyy/info?a=b""")]
// Announce URLs (passkeys) Magnet & Tracker
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2f9pr04sg601233210IMAveQL2tyu8xyui%2fannounce""}")]
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2ftracker.php%2f9pr04sg601233210IMAveQL2tyu8xyui%2fannounce""}")]
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2fannounce%2f9pr04sg601233210IMAveQL2tyu8xyui""}")]
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2fannounce.php%3fpasskey%3d9pr04sg601233210IMAveQL2tyu8xyui""}")]
[TestCase(@"tracker"":""https://xxx.yyy/9pr04sg601233210IMAveQL2tyu8xyui/announce""}")]
[TestCase(@"tracker"":""https://xxx.yyy/tracker.php/9pr04sg601233210IMAveQL2tyu8xyui/announce""}")]
[TestCase(@"tracker"":""https://xxx.yyy/announce/9pr04sg601233210IMAveQL2tyu8xyui""}")]
[TestCase(@"tracker"":""https://xxx.yyy/announce.php?passkey=9pr04sg601233210IMAveQL2tyu8xyui""}")]
[TestCase(@"tracker"":""http://xxx.yyy/announce.php?passkey=9pr04sg601233210IMAveQL2tyu8xyui"",""info"":""http://xxx.yyy/info?a=b""")]
// BroadcastheNet
[TestCase(@"method: ""getTorrents"", ""params"": [ ""mySecret"",")]

View File

@@ -107,52 +107,59 @@ namespace NzbDrone.Common.Http.Dispatchers
sw.Start();
using var responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cts.Token);
try
{
byte[] data = null;
try
using var responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cts.Token);
{
if (request.ResponseStream != null && responseMessage.StatusCode == HttpStatusCode.OK)
byte[] data = null;
try
{
await responseMessage.Content.CopyToAsync(request.ResponseStream, null, cts.Token);
}
else
{
data = await responseMessage.Content.ReadAsByteArrayAsync(cts.Token);
}
}
catch (Exception ex)
{
throw new WebException("Failed to read complete http response", ex, WebExceptionStatus.ReceiveFailure, null);
}
var headers = responseMessage.Headers.ToNameValueCollection();
headers.Add(responseMessage.Content.Headers.ToNameValueCollection());
var responseCookies = new CookieContainer();
if (responseMessage.Headers.TryGetValues("Set-Cookie", out var cookieHeaders))
{
foreach (var responseCookieHeader in cookieHeaders)
{
try
if (request.ResponseStream != null && responseMessage.StatusCode == HttpStatusCode.OK)
{
cookies.SetCookies(responseMessage.RequestMessage.RequestUri, responseCookieHeader);
await responseMessage.Content.CopyToAsync(request.ResponseStream, null, cts.Token);
}
catch
else
{
// Ignore invalid cookies
data = await responseMessage.Content.ReadAsByteArrayAsync(cts.Token);
}
}
catch (Exception ex)
{
throw new WebException("Failed to read complete http response", ex, WebExceptionStatus.ReceiveFailure, null);
}
var headers = responseMessage.Headers.ToNameValueCollection();
headers.Add(responseMessage.Content.Headers.ToNameValueCollection());
var responseCookies = new CookieContainer();
if (responseMessage.Headers.TryGetValues("Set-Cookie", out var cookieHeaders))
{
foreach (var responseCookieHeader in cookieHeaders)
{
try
{
cookies.SetCookies(responseMessage.RequestMessage.RequestUri, responseCookieHeader);
}
catch
{
// Ignore invalid cookies
}
}
}
var cookieCollection = cookies.GetCookies(responseMessage.RequestMessage.RequestUri);
sw.Stop();
return new HttpResponse(request, new HttpHeader(headers), cookieCollection, data, sw.ElapsedMilliseconds, responseMessage.StatusCode, responseMessage.Version);
}
var cookieCollection = cookies.GetCookies(responseMessage.RequestMessage.RequestUri);
sw.Stop();
return new HttpResponse(request, new HttpHeader(headers), cookieCollection, data, sw.ElapsedMilliseconds, responseMessage.StatusCode, responseMessage.Version);
}
catch (OperationCanceledException ex) when (cts.IsCancellationRequested)
{
throw new WebException("Http request timed out", ex.InnerException, WebExceptionStatus.Timeout, null);
}
}

View File

@@ -30,7 +30,7 @@ namespace NzbDrone.Common.Instrumentation
new (@"""/(home|Users)/(?<secret>[^/""]+?)(/|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Trackers Announce Keys; Designed for Qbit Json; should work for all in theory
new (@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?<secret>[a-z0-9]{16,})|(?<secret>[a-z0-9]{16,})(/|%2f)announce"),
new (@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?<secret>[a-z0-9]{16,})|(?<secret>[a-z0-9]{16,})(/|%2f)announce", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// NzbGet
new (@"""Name""\s*:\s*""[^""]*(username|password)""\s*,\s*""Value""\s*:\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),

View File

@@ -4,13 +4,13 @@
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DryIoc.dll" Version="5.4.1" />
<PackageReference Include="DryIoc.dll" Version="5.4.3" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NLog" Version="5.2.0" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.0" />
<PackageReference Include="Npgsql" Version="7.0.4" />
<PackageReference Include="Npgsql" Version="7.0.6" />
<PackageReference Include="Sentry" Version="3.29.1" />
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
<PackageReference Include="SharpZipLib" Version="1.4.2" />

View File

@@ -0,0 +1,17 @@
using System;
namespace NzbDrone.Common.TPL
{
public interface IDebounceManager
{
Debouncer CreateDebouncer(Action action, TimeSpan debounceDuration);
}
public class DebounceManager : IDebounceManager
{
public Debouncer CreateDebouncer(Action action, TimeSpan debounceDuration)
{
return new Debouncer(action, debounceDuration);
}
}
}

View File

@@ -4,11 +4,11 @@ namespace NzbDrone.Common.TPL
{
public class Debouncer
{
private readonly Action _action;
private readonly System.Timers.Timer _timer;
protected readonly Action _action;
protected readonly System.Timers.Timer _timer;
private volatile int _paused;
private volatile bool _triggered;
protected volatile int _paused;
protected volatile bool _triggered;
public Debouncer(Action action, TimeSpan debounceDuration)
{
@@ -27,7 +27,7 @@ namespace NzbDrone.Common.TPL
}
}
public void Execute()
public virtual void Execute()
{
lock (_timer)
{
@@ -39,7 +39,7 @@ namespace NzbDrone.Common.TPL
}
}
public void Pause()
public virtual void Pause()
{
lock (_timer)
{
@@ -48,7 +48,7 @@ namespace NzbDrone.Common.TPL
}
}
public void Resume()
public virtual void Resume()
{
lock (_timer)
{

View File

@@ -1,10 +1,14 @@
using System;
using System.Collections.Generic;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Messaging;
using NzbDrone.Common.TPL;
using NzbDrone.Core.HealthCheck;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.HealthCheck
{
@@ -19,10 +23,10 @@ namespace NzbDrone.Core.Test.HealthCheck
Mocker.SetConstant<IEnumerable<IProvideHealthCheck>>(new[] { _healthCheck });
Mocker.SetConstant<ICacheManager>(Mocker.Resolve<CacheManager>());
Mocker.SetConstant<IDebounceManager>(Mocker.Resolve<DebounceManager>());
Mocker.GetMock<IServerSideNotificationService>()
.Setup(v => v.GetServerChecks())
.Returns(new List<Core.HealthCheck.HealthCheck>());
Mocker.GetMock<IDebounceManager>().Setup(s => s.CreateDebouncer(It.IsAny<Action>(), It.IsAny<TimeSpan>()))
.Returns<Action, TimeSpan>((a, t) => new MockDebouncer(a, t));
}
[Test]

View File

@@ -43,7 +43,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AnimeBytesTests
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)));
var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new[] { 2000, 5000 } })).Releases;
var releases = (await Subject.Fetch(new BasicSearchCriteria { SearchTerm = "test", Categories = new[] { 2000, 5000 } })).Releases;
releases.Should().HaveCount(33);
releases.First().Should().BeOfType<TorrentInfo>();

View File

@@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
torrentInfo.InfoUrl.Should().Be("https://avistaz.to/torrent/187240-japan-sinks-people-of-hope-2021-s01e05-720p-nf-web-dl-ddp20-x264-seikel");
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-11-14 21:26:21"));
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-11-14 22:26:21"));
torrentInfo.Size.Should().Be(935127615);
torrentInfo.InfoHash.Should().Be("a879261d4e6e792402f92401141a21de70d51bf2");
torrentInfo.MagnetUrl.Should().Be(null);

View File

@@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
torrentInfo.InfoUrl.Should().Be("https://exoticaz.to/torrent/64040-ssis-419-my-first-experience-is-yua-mikami-from-the-day-i-lost-my-virginity-i-was-devoted-to-sex");
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-06-11 15:04:50"));
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-06-11 10:04:50"));
torrentInfo.Size.Should().Be(7085405541);
torrentInfo.InfoHash.Should().Be("asdjfiasdf54asd7f4a2sdf544asdf");
torrentInfo.MagnetUrl.Should().Be(null);

View File

@@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
torrentInfo.InfoUrl.Should().Be("https://privatehd.to/torrent/78506-godzilla-2014-2160p-uhd-bluray-remux-hdr-hevc-atmos-triton");
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-03-21 04:24:49"));
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-03-21 05:24:49"));
torrentInfo.Size.Should().Be(69914591044);
torrentInfo.InfoHash.Should().Be("a879261d4e6e792402f92401141a21de70d51bf2");
torrentInfo.MagnetUrl.Should().Be(null);

View File

@@ -55,7 +55,7 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
torrentInfo.InfoUrl.Should().Be("https://filelist.io/details.php?id=665873");
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2020-01-25 19:20:19"));
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2020-01-25 20:20:19"));
torrentInfo.Size.Should().Be(8300512414);
torrentInfo.InfoHash.Should().Be(null);
torrentInfo.MagnetUrl.Should().Be(null);

View File

@@ -57,7 +57,7 @@ namespace NzbDrone.Core.Test.IndexerTests.HDBitsTests
var first = torrents.First() as TorrentInfo;
first.Guid.Should().Be("HDBits-257142");
first.Title.Should().Be("Supernatural S10E17 1080p WEB-DL DD5.1 H.264-ECI");
first.Title.Should().Be("Supernatural.S10E17.1080p.WEB-DL.DD5.1.H.264-ECI");
first.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
first.DownloadUrl.Should().Be("https://hdbits.org/download.php?id=257142&passkey=fakekey");
first.InfoUrl.Should().Be("https://hdbits.org/details.php?id=257142");

View File

@@ -68,5 +68,16 @@ namespace NzbDrone.Core.Test.IndexerTests
VerifyNoUpdate();
}
[Test]
public void should_not_record_failure_for_unknown_provider()
{
Subject.RecordFailure(0);
Mocker.GetMock<IIndexerStatusRepository>()
.Verify(v => v.FindByProviderId(1), Times.Never);
VerifyNoUpdate();
}
}
}

View File

@@ -19,6 +19,11 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
[SetUp]
public void SetUp()
{
Subject.Definition = new IndexerDefinition
{
Name = "Newznab"
};
Subject.Settings = new NewznabSettings()
{
BaseUrl = "http://127.0.0.1:1234/",

View File

@@ -39,12 +39,12 @@ namespace NzbDrone.Core.Test.IndexerTests.OrpheusTests
var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new[] { 3000 } })).Releases;
releases.Should().HaveCount(65);
releases.Should().HaveCount(50);
releases.First().Should().BeOfType<TorrentInfo>();
var torrentInfo = releases.First() as TorrentInfo;
torrentInfo.Title.Should().Be("The Beatles - Abbey Road [1969] [Album] [2.0 Mix 2019] [MP3 V2 (VBR)] [BD]");
torrentInfo.Title.Should().Be("The Beatles - Abbey Road (1969) [Album] [2.0 Mix 2019] [MP3 V2 (VBR) / BD]");
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
torrentInfo.DownloadUrl.Should().Be("https://orpheus.network/ajax.php?action=download&id=1902448");
torrentInfo.InfoUrl.Should().Be("https://orpheus.network/torrents.php?id=466&torrentid=1902448");

View File

@@ -44,7 +44,7 @@ namespace NzbDrone.Core.Test.IndexerTests.RedactedTests
var torrentInfo = releases.First() as TorrentInfo;
torrentInfo.Title.Should().Be("Red Hot Chili Peppers - Californication [1999] [Album] [US / Reissue 2020] [FLAC 24bit Lossless] [Vinyl]");
torrentInfo.Title.Should().Be("Red Hot Chili Peppers - Californication (1999) [Album] [US / Reissue 2020] [FLAC 24bit Lossless / Vinyl]");
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
torrentInfo.DownloadUrl.Should().Be("https://redacted.ch/ajax.php?action=download&id=3892313");
torrentInfo.InfoUrl.Should().Be("https://redacted.ch/torrents.php?id=16720&torrentid=3892313");

View File

@@ -5,7 +5,6 @@ using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Localization;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.Localization
{
@@ -29,19 +28,20 @@ namespace NzbDrone.Core.Test.Localization
}
[Test]
public void should_get_string_in_default_dictionary_if_no_lang_exists_and_string_exists()
public void should_get_string_in_french()
{
var localizedString = Subject.GetLocalizedString("BackupNow", "an");
Mocker.GetMock<IConfigService>().Setup(m => m.UILanguage).Returns("fr");
localizedString.Should().Be("Backup Now");
var localizedString = Subject.GetLocalizedString("BackupNow");
ExceptionVerification.ExpectedErrors(1);
localizedString.Should().Be("Sauvegarder maintenant");
}
[Test]
public void should_get_string_in_default_dictionary_if_lang_empty_and_string_exists()
public void should_get_string_in_default_dictionary_if_unknown_language_and_string_exists()
{
var localizedString = Subject.GetLocalizedString("BackupNow", "");
Mocker.GetMock<IConfigService>().Setup(m => m.UILanguage).Returns("");
var localizedString = Subject.GetLocalizedString("BackupNow");
localizedString.Should().Be("Backup Now");
}
@@ -49,7 +49,7 @@ namespace NzbDrone.Core.Test.Localization
[Test]
public void should_return_argument_if_string_doesnt_exists()
{
var localizedString = Subject.GetLocalizedString("BadString", "en");
var localizedString = Subject.GetLocalizedString("BadString");
localizedString.Should().Be("BadString");
}

View File

@@ -325,6 +325,20 @@ namespace NzbDrone.Core.Configuration
}
}
public void MigrateConfigFile()
{
if (!File.Exists(_configFile))
{
return;
}
// If SSL is enabled and a cert hash is still in the config file disable SSL
if (EnableSsl && GetValue("SslCertHash", null).IsNotNullOrWhiteSpace())
{
SetValue("EnableSsl", false);
}
}
private void DeleteOldValues()
{
var xDoc = LoadConfigFile();
@@ -396,6 +410,7 @@ namespace NzbDrone.Core.Configuration
public void HandleAsync(ApplicationStartedEvent message)
{
MigrateConfigFile();
EnsureDefaultConfigFile();
DeleteOldValues();
}

View File

@@ -9,8 +9,8 @@ namespace NzbDrone.Core.Datastore
{
public interface IConnectionStringFactory
{
string MainDbConnectionString { get; }
string LogDbConnectionString { get; }
DatabaseConnectionInfo MainDbConnection { get; }
DatabaseConnectionInfo LogDbConnection { get; }
string GetDatabasePath(string connectionString);
}
@@ -22,15 +22,15 @@ namespace NzbDrone.Core.Datastore
{
_configFileProvider = configFileProvider;
MainDbConnectionString = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresMainDb) :
MainDbConnection = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresMainDb) :
GetConnectionString(appFolderInfo.GetDatabase());
LogDbConnectionString = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresLogDb) :
LogDbConnection = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresLogDb) :
GetConnectionString(appFolderInfo.GetLogDatabase());
}
public string MainDbConnectionString { get; private set; }
public string LogDbConnectionString { get; private set; }
public DatabaseConnectionInfo MainDbConnection { get; private set; }
public DatabaseConnectionInfo LogDbConnection { get; private set; }
public string GetDatabasePath(string connectionString)
{
@@ -39,37 +39,40 @@ namespace NzbDrone.Core.Datastore
return connectionBuilder.DataSource;
}
private static string GetConnectionString(string dbPath)
private static DatabaseConnectionInfo GetConnectionString(string dbPath)
{
var connectionBuilder = new SQLiteConnectionStringBuilder();
connectionBuilder.DataSource = dbPath;
connectionBuilder.CacheSize = (int)-20000;
connectionBuilder.DateTimeKind = DateTimeKind.Utc;
connectionBuilder.JournalMode = OsInfo.IsOsx ? SQLiteJournalModeEnum.Truncate : SQLiteJournalModeEnum.Wal;
connectionBuilder.Pooling = true;
connectionBuilder.Version = 3;
var connectionBuilder = new SQLiteConnectionStringBuilder
{
DataSource = dbPath,
CacheSize = (int)-20000,
DateTimeKind = DateTimeKind.Utc,
JournalMode = OsInfo.IsOsx ? SQLiteJournalModeEnum.Truncate : SQLiteJournalModeEnum.Wal,
Pooling = true,
Version = 3,
BusyTimeout = 100
};
if (OsInfo.IsOsx)
{
connectionBuilder.Add("Full FSync", true);
}
return connectionBuilder.ConnectionString;
return new DatabaseConnectionInfo(DatabaseType.SQLite, connectionBuilder.ConnectionString);
}
private string GetPostgresConnectionString(string dbName)
private DatabaseConnectionInfo GetPostgresConnectionString(string dbName)
{
var connectionBuilder = new NpgsqlConnectionStringBuilder();
var connectionBuilder = new NpgsqlConnectionStringBuilder
{
Database = dbName,
Host = _configFileProvider.PostgresHost,
Username = _configFileProvider.PostgresUser,
Password = _configFileProvider.PostgresPassword,
Port = _configFileProvider.PostgresPort,
Enlist = false
};
connectionBuilder.Database = dbName;
connectionBuilder.Host = _configFileProvider.PostgresHost;
connectionBuilder.Username = _configFileProvider.PostgresUser;
connectionBuilder.Password = _configFileProvider.PostgresPassword;
connectionBuilder.Port = _configFileProvider.PostgresPort;
connectionBuilder.Enlist = false;
return connectionBuilder.ConnectionString;
return new DatabaseConnectionInfo(DatabaseType.PostgreSQL, connectionBuilder.ConnectionString);
}
}
}

View File

@@ -0,0 +1,14 @@
namespace NzbDrone.Core.Datastore
{
public class DatabaseConnectionInfo
{
public DatabaseConnectionInfo(DatabaseType databaseType, string connectionString)
{
DatabaseType = databaseType;
ConnectionString = connectionString;
}
public DatabaseType DatabaseType { get; internal set; }
public string ConnectionString { get; internal set; }
}
}

View File

@@ -2,6 +2,7 @@ using System;
using System.Data.Common;
using System.Data.SQLite;
using System.Net.Sockets;
using System.Threading;
using NLog;
using Npgsql;
using NzbDrone.Common.Disk;
@@ -60,22 +61,22 @@ namespace NzbDrone.Core.Datastore
public IDatabase Create(MigrationContext migrationContext)
{
string connectionString;
DatabaseConnectionInfo connectionInfo;
switch (migrationContext.MigrationType)
{
case MigrationType.Main:
{
connectionString = _connectionStringFactory.MainDbConnectionString;
CreateMain(connectionString, migrationContext);
connectionInfo = _connectionStringFactory.MainDbConnection;
CreateMain(connectionInfo.ConnectionString, migrationContext, connectionInfo.DatabaseType);
break;
}
case MigrationType.Log:
{
connectionString = _connectionStringFactory.LogDbConnectionString;
CreateLog(connectionString, migrationContext);
connectionInfo = _connectionStringFactory.LogDbConnection;
CreateLog(connectionInfo.ConnectionString, migrationContext, connectionInfo.DatabaseType);
break;
}
@@ -90,14 +91,14 @@ namespace NzbDrone.Core.Datastore
{
DbConnection conn;
if (connectionString.Contains(".db"))
if (connectionInfo.DatabaseType == DatabaseType.SQLite)
{
conn = SQLiteFactory.Instance.CreateConnection();
conn.ConnectionString = connectionString;
conn.ConnectionString = connectionInfo.ConnectionString;
}
else
{
conn = new NpgsqlConnection(connectionString);
conn = new NpgsqlConnection(connectionInfo.ConnectionString);
}
conn.Open();
@@ -107,12 +108,12 @@ namespace NzbDrone.Core.Datastore
return db;
}
private void CreateMain(string connectionString, MigrationContext migrationContext)
private void CreateMain(string connectionString, MigrationContext migrationContext, DatabaseType databaseType)
{
try
{
_restoreDatabaseService.Restore();
_migrationController.Migrate(connectionString, migrationContext);
_migrationController.Migrate(connectionString, migrationContext, databaseType);
}
catch (SQLiteException e)
{
@@ -135,15 +136,17 @@ namespace NzbDrone.Core.Datastore
{
Logger.Error(e, "Failure to connect to Postgres DB, {0} retries remaining", retryCount);
Thread.Sleep(5000);
try
{
_migrationController.Migrate(connectionString, migrationContext);
_migrationController.Migrate(connectionString, migrationContext, databaseType);
return;
}
catch (Exception ex)
{
if (--retryCount > 0)
{
System.Threading.Thread.Sleep(5000);
continue;
}
@@ -162,11 +165,11 @@ namespace NzbDrone.Core.Datastore
}
}
private void CreateLog(string connectionString, MigrationContext migrationContext)
private void CreateLog(string connectionString, MigrationContext migrationContext, DatabaseType databaseType)
{
try
{
_migrationController.Migrate(connectionString, migrationContext);
_migrationController.Migrate(connectionString, migrationContext, databaseType);
}
catch (SQLiteException e)
{
@@ -186,7 +189,7 @@ namespace NzbDrone.Core.Datastore
Logger.Error("Unable to recreate logging database automatically. It will need to be removed manually.");
}
_migrationController.Migrate(connectionString, migrationContext);
_migrationController.Migrate(connectionString, migrationContext, databaseType);
}
catch (Exception e)
{

View File

@@ -0,0 +1,16 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(038)]
public class indexers_freeleech_only_config_contract : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Update.Table("Indexers").Set(new { ConfigContract = "HDSpaceSettings" }).Where(new { Implementation = "HDSpace" });
Update.Table("Indexers").Set(new { ConfigContract = "ImmortalSeedSettings" }).Where(new { Implementation = "ImmortalSeed" });
Update.Table("Indexers").Set(new { ConfigContract = "XSpeedsSettings" }).Where(new { Implementation = "XSpeeds" });
}
}
}

View File

@@ -14,7 +14,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
{
public interface IMigrationController
{
void Migrate(string connectionString, MigrationContext migrationContext);
void Migrate(string connectionString, MigrationContext migrationContext, DatabaseType databaseType);
}
public class MigrationController : IMigrationController
@@ -29,7 +29,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
_migrationLoggerProvider = migrationLoggerProvider;
}
public void Migrate(string connectionString, MigrationContext migrationContext)
public void Migrate(string connectionString, MigrationContext migrationContext, DatabaseType databaseType)
{
var sw = Stopwatch.StartNew();
@@ -37,7 +37,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
ServiceProvider serviceProvider;
var db = connectionString.Contains(".db") ? "sqlite" : "postgres";
var db = databaseType == DatabaseType.SQLite ? "sqlite" : "postgres";
serviceProvider = new ServiceCollection()
.AddLogging(b => b.AddNLog())

View File

@@ -6,6 +6,7 @@ using NzbDrone.Common.Cache;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Messaging;
using NzbDrone.Common.Reflection;
using NzbDrone.Common.TPL;
using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
@@ -27,35 +28,35 @@ namespace NzbDrone.Core.HealthCheck
private readonly IProvideHealthCheck[] _startupHealthChecks;
private readonly IProvideHealthCheck[] _scheduledHealthChecks;
private readonly Dictionary<Type, IEventDrivenHealthCheck[]> _eventDrivenHealthChecks;
private readonly IServerSideNotificationService _serverSideNotificationService;
private readonly IEventAggregator _eventAggregator;
private readonly ICacheManager _cacheManager;
private readonly Logger _logger;
private readonly ICached<HealthCheck> _healthCheckResults;
private readonly HashSet<IProvideHealthCheck> _pendingHealthChecks;
private readonly Debouncer _debounce;
private bool _hasRunHealthChecksAfterGracePeriod;
private bool _isRunningHealthChecksAfterGracePeriod;
public HealthCheckService(IEnumerable<IProvideHealthCheck> healthChecks,
IServerSideNotificationService serverSideNotificationService,
IEventAggregator eventAggregator,
ICacheManager cacheManager,
IDebounceManager debounceManager,
IRuntimeInfo runtimeInfo,
Logger logger)
{
_healthChecks = healthChecks.ToArray();
_serverSideNotificationService = serverSideNotificationService;
_eventAggregator = eventAggregator;
_cacheManager = cacheManager;
_logger = logger;
_healthCheckResults = _cacheManager.GetCache<HealthCheck>(GetType());
_healthCheckResults = cacheManager.GetCache<HealthCheck>(GetType());
_pendingHealthChecks = new HashSet<IProvideHealthCheck>();
_debounce = debounceManager.CreateDebouncer(ProcessHealthChecks, TimeSpan.FromSeconds(5));
_startupHealthChecks = _healthChecks.Where(v => v.CheckOnStartup).ToArray();
_scheduledHealthChecks = _healthChecks.Where(v => v.CheckOnSchedule).ToArray();
_eventDrivenHealthChecks = GetEventDrivenHealthChecks();
_startupGracePeriodEndTime = runtimeInfo.StartTime.AddMinutes(15);
_startupGracePeriodEndTime = runtimeInfo.StartTime + TimeSpan.FromMinutes(15);
}
public List<HealthCheck> Results()
@@ -77,63 +78,93 @@ namespace NzbDrone.Core.HealthCheck
.ToDictionary(g => g.Key, g => g.ToArray());
}
private void PerformHealthCheck(IProvideHealthCheck[] healthChecks, bool performServerChecks = false)
private void ProcessHealthChecks()
{
var results = healthChecks.Select(c => c.Check())
.ToList();
List<IProvideHealthCheck> healthChecks;
if (performServerChecks)
lock (_pendingHealthChecks)
{
results.AddRange(_serverSideNotificationService.GetServerChecks());
healthChecks = _pendingHealthChecks.ToList();
_pendingHealthChecks.Clear();
}
foreach (var result in results)
_debounce.Pause();
try
{
if (result.Type == HealthCheckResult.Ok)
{
var previous = _healthCheckResults.Find(result.Source.Name);
if (previous != null)
var results = healthChecks.Select(c =>
{
_eventAggregator.PublishEvent(new HealthCheckRestoredEvent(previous, !_hasRunHealthChecksAfterGracePeriod));
}
_logger.Trace("Check health -> {0}", c.GetType().Name);
var result = c.Check();
_logger.Trace("Check health <- {0}", c.GetType().Name);
_healthCheckResults.Remove(result.Source.Name);
}
else
return result;
})
.ToList();
foreach (var result in results)
{
if (_healthCheckResults.Find(result.Source.Name) == null)
if (result.Type == HealthCheckResult.Ok)
{
_eventAggregator.PublishEvent(new HealthCheckFailedEvent(result, !_hasRunHealthChecksAfterGracePeriod));
}
var previous = _healthCheckResults.Find(result.Source.Name);
_healthCheckResults.Set(result.Source.Name, result);
if (previous != null)
{
_eventAggregator.PublishEvent(new HealthCheckRestoredEvent(previous, !_hasRunHealthChecksAfterGracePeriod));
}
_healthCheckResults.Remove(result.Source.Name);
}
else
{
if (_healthCheckResults.Find(result.Source.Name) == null)
{
_eventAggregator.PublishEvent(new HealthCheckFailedEvent(result, !_hasRunHealthChecksAfterGracePeriod));
}
_healthCheckResults.Set(result.Source.Name, result);
}
}
}
finally
{
_debounce.Resume();
}
_eventAggregator.PublishEvent(new HealthCheckCompleteEvent());
}
public void Execute(CheckHealthCommand message)
{
if (message.Trigger == CommandTrigger.Manual)
var healthChecks = message.Trigger == CommandTrigger.Manual ? _healthChecks : _scheduledHealthChecks;
lock (_pendingHealthChecks)
{
PerformHealthCheck(_healthChecks, true);
}
else
{
PerformHealthCheck(_scheduledHealthChecks, true);
foreach (var healthCheck in healthChecks)
{
_pendingHealthChecks.Add(healthCheck);
}
}
ProcessHealthChecks();
}
public void HandleAsync(ApplicationStartedEvent message)
{
PerformHealthCheck(_startupHealthChecks, true);
lock (_pendingHealthChecks)
{
foreach (var healthCheck in _startupHealthChecks)
{
_pendingHealthChecks.Add(healthCheck);
}
}
ProcessHealthChecks();
}
public void HandleAsync(IEvent message)
{
if (message is HealthCheckCompleteEvent)
if (message is HealthCheckCompleteEvent || message is ApplicationStartedEvent)
{
return;
}
@@ -144,7 +175,16 @@ namespace NzbDrone.Core.HealthCheck
{
_isRunningHealthChecksAfterGracePeriod = true;
PerformHealthCheck(_startupHealthChecks);
lock (_pendingHealthChecks)
{
foreach (var healthCheck in _startupHealthChecks)
{
_pendingHealthChecks.Add(healthCheck);
}
}
// Call it directly so it's not debounced and any alerts can be sent.
ProcessHealthChecks();
// Update after running health checks so new failure notifications aren't sent 2x.
_hasRunHealthChecksAfterGracePeriod = true;
@@ -176,11 +216,16 @@ namespace NzbDrone.Core.HealthCheck
if (eventDrivenHealthCheck.ShouldExecute(message, previouslyFailed))
{
filteredChecks.Add(eventDrivenHealthCheck.HealthCheck);
continue;
}
}
// TODO: Add debounce
PerformHealthCheck(filteredChecks.ToArray());
lock (_pendingHealthChecks)
{
filteredChecks.ForEach(h => _pendingHealthChecks.Add(h));
}
_debounce.Execute();
}
}
}

View File

@@ -9,50 +9,43 @@ using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Localization;
namespace NzbDrone.Core.HealthCheck
{
public interface IServerSideNotificationService
{
public List<HealthCheck> GetServerChecks();
}
public class ServerSideNotificationService : IServerSideNotificationService
public class ServerSideNotificationService : HealthCheckBase
{
private readonly IHttpClient _client;
private readonly IProwlarrCloudRequestBuilder _cloudRequestBuilder;
private readonly IConfigFileProvider _configFileProvider;
private readonly IHttpRequestBuilderFactory _cloudRequestBuilder;
private readonly Logger _logger;
private readonly ICached<List<HealthCheck>> _cache;
private readonly ICached<HealthCheck> _cache;
public ServerSideNotificationService(IHttpClient client,
IConfigFileProvider configFileProvider,
IProwlarrCloudRequestBuilder cloudRequestBuilder,
ICacheManager cacheManager,
Logger logger)
public ServerSideNotificationService(IHttpClient client, IProwlarrCloudRequestBuilder cloudRequestBuilder, IConfigFileProvider configFileProvider, ICacheManager cacheManager, ILocalizationService localizationService, Logger logger)
: base(localizationService)
{
_client = client;
_configFileProvider = configFileProvider;
_cloudRequestBuilder = cloudRequestBuilder.Services;
_cloudRequestBuilder = cloudRequestBuilder;
_logger = logger;
_cache = cacheManager.GetCache<List<HealthCheck>>(GetType());
_cache = cacheManager.GetCache<HealthCheck>(GetType());
}
public List<HealthCheck> GetServerChecks()
public override HealthCheck Check()
{
return _cache.Get("ServerChecks", RetrieveServerChecks, TimeSpan.FromHours(2));
}
private List<HealthCheck> RetrieveServerChecks()
private HealthCheck RetrieveServerChecks()
{
if (BuildInfo.IsDebug)
{
return new List<HealthCheck>();
return new HealthCheck(GetType());
}
var request = _cloudRequestBuilder.Create()
var request = _cloudRequestBuilder.Services.Create()
.Resource("/notification")
.AddQueryParam("version", BuildInfo.Version)
.AddQueryParam("os", OsInfo.Os.ToString().ToLowerInvariant())
@@ -63,17 +56,22 @@ namespace NzbDrone.Core.HealthCheck
try
{
_logger.Trace("Getting server side health notifications");
_logger.Trace("Getting notifications");
var response = _client.Execute(request);
var result = Json.Deserialize<List<ServerNotificationResponse>>(response.Content);
return result.Select(x => new HealthCheck(GetType(), x.Type, x.Message, x.WikiUrl)).ToList();
var checks = result.Select(x => new HealthCheck(GetType(), x.Type, x.Message, x.WikiUrl)).ToList();
// Only one health check is supported, services returns an ordered list, so use the first one
return checks.FirstOrDefault() ?? new HealthCheck(GetType());
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to retrieve server notifications");
}
_logger.Error(ex, "Failed to retrieve notifications");
return new List<HealthCheck>();
return new HealthCheck(GetType());
}
}
}

View File

@@ -39,19 +39,14 @@ namespace NzbDrone.Core.IndexerSearch
public Task<NewznabResults> Search(NewznabRequest request, List<int> indexerIds, bool interactiveSearch)
{
switch (request.t)
return request.t switch
{
case "movie":
return MovieSearch(request, indexerIds, interactiveSearch);
case "music":
return MusicSearch(request, indexerIds, interactiveSearch);
case "tvsearch":
return TvSearch(request, indexerIds, interactiveSearch);
case "book":
return BookSearch(request, indexerIds, interactiveSearch);
default:
return BasicSearch(request, indexerIds, interactiveSearch);
}
"movie" => MovieSearch(request, indexerIds, interactiveSearch),
"music" => MusicSearch(request, indexerIds, interactiveSearch),
"tvsearch" => TvSearch(request, indexerIds, interactiveSearch),
"book" => BookSearch(request, indexerIds, interactiveSearch),
_ => BasicSearch(request, indexerIds, interactiveSearch)
};
}
private async Task<NewznabResults> MovieSearch(NewznabRequest request, List<int> indexerIds, bool interactiveSearch)

View File

@@ -91,6 +91,11 @@ namespace NzbDrone.Core.Indexers.Definitions
{
var cleanReleases = base.CleanupReleases(releases, searchCriteria);
if (searchCriteria.IsRssSearch)
{
cleanReleases = cleanReleases.Where(r => r.PublishDate > DateTime.Now.AddDays(-1)).ToList();
}
return cleanReleases.Select(r => (ReleaseInfo)r.Clone()).ToList();
}
@@ -555,6 +560,7 @@ namespace NzbDrone.Core.Indexers.Definitions
MinimumRatio = 1,
MinimumSeedTime = minimumSeedTime,
Title = fileName,
Year = year.GetValueOrDefault(),
InfoUrl = details.AbsoluteUri,
Guid = guid.AbsoluteUri,
DownloadUrl = link.AbsoluteUri,
@@ -587,6 +593,7 @@ namespace NzbDrone.Core.Indexers.Definitions
MinimumRatio = 1,
MinimumSeedTime = minimumSeedTime,
Title = releaseTitle.Trim(),
Year = year.GetValueOrDefault(),
InfoUrl = details.AbsoluteUri,
Guid = guid.AbsoluteUri,
DownloadUrl = link.AbsoluteUri,

View File

@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using AngleSharp.Html.Parser;
using NLog;
@@ -10,6 +11,7 @@ using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.Indexers.Definitions;
[Obsolete("Site has shutdown")]
public class AroLol : GazelleBase<AroLolSettings>
{
public override string Name => "aro.lol";

View File

@@ -84,6 +84,6 @@ namespace NzbDrone.Core.Indexers.Definitions
public class AvistaZParser : AvistazParserBase
{
protected override string TimezoneOffset => "+02:00";
protected override string TimezoneOffset => "+01:00";
}
}

View File

@@ -7,6 +7,7 @@ using NLog;
using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.Indexers.Definitions.Avistaz
@@ -15,9 +16,9 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
{
public override bool SupportsRss => true;
public override bool SupportsSearch => true;
public override bool SupportsPagination => true;
public override bool SupportsPagination => false;
public override int PageSize => 50;
public override TimeSpan RateLimit => TimeSpan.FromSeconds(4);
public override TimeSpan RateLimit => TimeSpan.FromSeconds(6);
public override IndexerCapabilities Capabilities => SetCapabilities();
protected virtual string LoginUrl => Settings.BaseUrl + "api/v1/jackett/auth";
private IIndexerRepository _indexerRepository;
@@ -50,21 +51,28 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
return new AvistazParserBase();
}
protected virtual IndexerCapabilities SetCapabilities()
{
return new IndexerCapabilities();
}
protected abstract IndexerCapabilities SetCapabilities();
protected override async Task DoLogin()
{
Settings.Token = await GetToken();
if (Definition.Id > 0)
try
{
_indexerRepository.UpdateSettings((IndexerDefinition)Definition);
}
Settings.Token = await GetToken();
_logger.Debug("Avistaz authentication succeeded.");
if (Definition.Id > 0)
{
_indexerRepository.UpdateSettings((IndexerDefinition)Definition);
}
_logger.Debug("Avistaz authentication succeeded.");
}
catch (HttpException ex) when (ex.Response.StatusCode == HttpStatusCode.Unauthorized)
{
_logger.Warn(ex, "Failed to authenticate with Avistaz");
var jsonResponse = STJson.Deserialize<AvistazErrorResponse>(ex.Response.Content);
throw new IndexerAuthException(jsonResponse?.Message ?? "Unauthorized request to indexer");
}
}
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
@@ -116,11 +124,12 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
Method = HttpMethod.Post
};
// TODO: Change to HttpAccept.Json after they fix the issue with missing headers
var authLoginRequest = requestBuilder
.AddFormParameter("username", Settings.Username)
.AddFormParameter("password", Settings.Password)
.AddFormParameter("pid", Settings.Pid.Trim())
.Accept(HttpAccept.Json)
.Accept(HttpAccept.Html)
.Build();
var response = await ExecuteAuth(authLoginRequest);

View File

@@ -14,7 +14,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
{
public class AvistazParserBase : IParseIndexerResponse
{
protected virtual string TimezoneOffset => "-04:00"; // Avistaz does not specify a timezone & returns server time
protected virtual string TimezoneOffset => "-05:00"; // Avistaz does not specify a timezone & returns server time
private readonly HashSet<string> _hdResolutions = new () { "1080p", "1080i", "720p" };
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
@@ -69,11 +69,22 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
DownloadVolumeFactor = row.DownloadMultiply,
UploadVolumeFactor = row.UploadMultiply,
MinimumRatio = 1,
MinimumSeedTime = 172800, // 48 hours
MinimumSeedTime = 259200, // 72 hours
Languages = row.Audio?.Select(x => x.Language).ToList() ?? new List<string>(),
Subs = row.Subtitle?.Select(x => x.Language).ToList() ?? new List<string>()
};
if (row.FileSize is > 0)
{
var sizeGigabytes = row.FileSize.Value / Math.Pow(1024, 3);
release.MinimumSeedTime = sizeGigabytes switch
{
> 50.0 => (long)((100 * Math.Log(sizeGigabytes)) - 219.2023) * 3600,
_ => 259200 + (long)(sizeGigabytes * 7200)
};
}
if (row.MovieTvinfo != null)
{
release.ImdbId = ParseUtil.GetImdbId(row.MovieTvinfo.Imdb).GetValueOrDefault();

View File

@@ -20,6 +20,7 @@ using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Indexers.Definitions
{
[Obsolete("Site has shutdown")]
public class BB : TorrentIndexerBase<UserPassTorrentBaseSettings>
{
public override string Name => "BB";

View File

@@ -48,6 +48,13 @@ namespace NzbDrone.Core.Indexers.Definitions
return new BeyondHDParser(Capabilities.Categories);
}
protected override IList<ReleaseInfo> CleanupReleases(IEnumerable<ReleaseInfo> releases, SearchCriteriaBase searchCriteria)
{
var cleanReleases = base.CleanupReleases(releases, searchCriteria);
return FilterReleasesByQuery(cleanReleases, searchCriteria).ToList();
}
private IndexerCapabilities SetCapabilities()
{
var caps = new IndexerCapabilities
@@ -130,7 +137,7 @@ namespace NzbDrone.Core.Indexers.Definitions
if (cats.Count > 0)
{
body.Add("categories", string.Join(",", cats));
body.Add("categories", cats.Select(int.Parse).ToArray());
}
if (_settings.SearchTypes.Any())
@@ -143,7 +150,7 @@ namespace NzbDrone.Core.Indexers.Definitions
if (searchTypes.Any())
{
body.Add("types", string.Join(",", searchTypes));
body.Add("types", searchTypes.ToArray());
}
}

View File

@@ -9,7 +9,9 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
public class BroadcastheNet : TorrentIndexerBase<BroadcastheNetSettings>
{
public override string Name => "BroadcasTheNet";
public override string[] IndexerUrls => new[] { "https://api.broadcasthe.net/" };
public override string[] LegacyUrls => new[] { "http://api.broadcasthe.net/" };
public override string Description => "BroadcasTheNet (BTN) is an invite-only torrent tracker focused on TV shows";
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override bool SupportsRss => true;
public override bool SupportsSearch => true;
@@ -18,11 +20,6 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
public override IndexerCapabilities Capabilities => SetCapabilities();
public override TimeSpan RateLimit => TimeSpan.FromSeconds(5);
public override string[] IndexerUrls => new string[] { "https://api.broadcasthe.net/" };
public override string[] LegacyUrls => new string[] { "http://api.broadcasthe.net/" };
public override string Description => "BroadcasTheNet (BTN) is an invite-only torrent tracker focused on TV shows";
public BroadcastheNet(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
@@ -30,18 +27,7 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
public override IIndexerRequestGenerator GetRequestGenerator()
{
var requestGenerator = new BroadcastheNetRequestGenerator() { Settings = Settings, PageSize = PageSize, Capabilities = Capabilities };
var releaseInfo = _indexerStatusService.GetLastRssSyncReleaseInfo(Definition.Id);
if (releaseInfo != null)
{
if (int.TryParse(releaseInfo.Guid.Replace("BTN-", string.Empty), out var torrentId))
{
requestGenerator.LastRecentTorrentID = torrentId;
}
}
return requestGenerator;
return new BroadcastheNetRequestGenerator { Settings = Settings, Capabilities = Capabilities, PageSize = PageSize };
}
public override IParseIndexerResponse GetParser()
@@ -65,7 +51,7 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
caps.Categories.AddCategoryMapping("720p", NewznabStandardCategory.TVHD, "720p");
caps.Categories.AddCategoryMapping("1080p", NewznabStandardCategory.TVHD, "1080p");
caps.Categories.AddCategoryMapping("1080i", NewznabStandardCategory.TVHD, "1080i");
caps.Categories.AddCategoryMapping("2160p", NewznabStandardCategory.TVHD, "2160p");
caps.Categories.AddCategoryMapping("2160p", NewznabStandardCategory.TVUHD, "2160p");
caps.Categories.AddCategoryMapping("Portable Device", NewznabStandardCategory.TVSD, "Portable Device");
return caps;

View File

@@ -25,7 +25,7 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
{
var results = new List<ReleaseInfo>();
var releaseInfos = new List<ReleaseInfo>();
var indexerHttpResponse = indexerResponse.HttpResponse;
switch (indexerHttpResponse.StatusCode)
@@ -69,67 +69,76 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
if (jsonResponse.Result.Results == 0 || jsonResponse.Result?.Torrents?.Values == null)
{
return results;
return releaseInfos;
}
var protocol = indexerResponse.HttpRequest.Url.Scheme + ":";
foreach (var torrent in jsonResponse.Result.Torrents.Values)
{
var torrentInfo = new TorrentInfo();
var flags = new HashSet<IndexerFlag>();
torrentInfo.Guid = string.Format("BTN-{0}", torrent.TorrentID);
torrentInfo.Title = CleanReleaseName(torrent.ReleaseName);
torrentInfo.Size = torrent.Size;
torrentInfo.DownloadUrl = RegexProtocol.Replace(torrent.DownloadURL, protocol);
torrentInfo.InfoUrl = string.Format("{0}//broadcasthe.net/torrents.php?id={1}&torrentid={2}", protocol, torrent.GroupID, torrent.TorrentID);
//torrentInfo.CommentUrl =
if (torrent.TvdbID.HasValue)
if (torrent.Origin.ToUpperInvariant() == "INTERNAL")
{
torrentInfo.TvdbId = torrent.TvdbID.Value;
flags.Add(IndexerFlag.Internal);
}
if (torrent.TvrageID.HasValue)
var releaseInfo = new TorrentInfo
{
torrentInfo.TvRageId = torrent.TvrageID.Value;
Guid = $"BTN-{torrent.TorrentID}",
InfoUrl = $"{protocol}//broadcasthe.net/torrents.php?id={torrent.GroupID}&torrentid={torrent.TorrentID}",
DownloadUrl = RegexProtocol.Replace(torrent.DownloadURL, protocol),
Title = CleanReleaseName(torrent.ReleaseName),
Categories = _categories.MapTrackerCatToNewznab(torrent.Resolution),
InfoHash = torrent.InfoHash,
Size = torrent.Size,
Grabs = torrent.Snatched,
Seeders = torrent.Seeders,
Peers = torrent.Leechers + torrent.Seeders,
PublishDate = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc).ToUniversalTime().AddSeconds(torrent.Time),
Origin = torrent.Origin,
Source = torrent.Source,
Container = torrent.Container,
Codec = torrent.Codec,
Resolution = torrent.Resolution,
Scene = torrent.Origin.ToUpperInvariant() == "SCENE",
IndexerFlags = flags,
DownloadVolumeFactor = 0,
UploadVolumeFactor = 1,
MinimumRatio = 1,
MinimumSeedTime = torrent.Category.ToUpperInvariant() == "SEASON" ? 432000 : 86400, // 120 hours for seasons and 24 hours for episodes
};
if (torrent.TvdbID is > 0)
{
releaseInfo.TvdbId = torrent.TvdbID.Value;
}
torrentInfo.PublishDate = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc).ToUniversalTime().AddSeconds(torrent.Time);
if (torrent.TvrageID is > 0)
{
releaseInfo.TvRageId = torrent.TvrageID.Value;
}
//torrentInfo.MagnetUrl =
torrentInfo.InfoHash = torrent.InfoHash;
torrentInfo.Seeders = torrent.Seeders;
torrentInfo.Peers = torrent.Leechers + torrent.Seeders;
torrentInfo.Origin = torrent.Origin;
torrentInfo.Source = torrent.Source;
torrentInfo.Container = torrent.Container;
torrentInfo.Codec = torrent.Codec;
torrentInfo.Resolution = torrent.Resolution;
torrentInfo.UploadVolumeFactor = 1;
torrentInfo.DownloadVolumeFactor = 0;
torrentInfo.MinimumRatio = 1;
torrentInfo.Categories = _categories.MapTrackerCatToNewznab(torrent.Resolution);
if (torrent.ImdbID.IsNotNullOrWhiteSpace() && int.TryParse(torrent.ImdbID, out var imdbId))
{
releaseInfo.ImdbId = imdbId;
}
// Default to TV if category could not be mapped
if (torrentInfo.Categories == null || !torrentInfo.Categories.Any())
if (releaseInfo.Categories == null || !releaseInfo.Categories.Any())
{
torrentInfo.Categories = new List<IndexerCategory> { NewznabStandardCategory.TV };
releaseInfo.Categories = new List<IndexerCategory> { NewznabStandardCategory.TV };
}
results.Add(torrentInfo);
releaseInfos.Add(releaseInfo);
}
return results;
return releaseInfos;
}
private string CleanReleaseName(string releaseName)
{
releaseName = releaseName.Replace("\\", "");
return releaseName;
return releaseName.Replace("\\", "");
}
}
}

View File

@@ -9,31 +9,13 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
{
public class BroadcastheNetRequestGenerator : IIndexerRequestGenerator
{
public int MaxPages { get; set; }
public int PageSize { get; set; }
public BroadcastheNetSettings Settings { get; set; }
public IndexerCapabilities Capabilities { get; set; }
public int? LastRecentTorrentID { get; set; }
public int PageSize { get; set; }
public Func<IDictionary<string, string>> GetCookies { get; set; }
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
public BroadcastheNetRequestGenerator()
{
MaxPages = 10;
PageSize = 100;
}
private IEnumerable<IndexerRequest> GetPagedRequests(BroadcastheNetTorrentQuery parameters, int results, int offset)
{
var builder = new JsonRpcRequestBuilder(Settings.BaseUrl)
.Call("getTorrents", Settings.ApiKey, parameters, results, offset);
builder.SuppressHttpError = true;
yield return new IndexerRequest(builder.Build());
}
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
{
return new IndexerPageableRequestChain();
@@ -132,11 +114,24 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
var btnOffset = searchCriteria.Offset.GetValueOrDefault(0);
parameters.Search = searchString.Replace(" ", "%");
if (searchString.IsNotNullOrWhiteSpace())
{
parameters.Search = searchString.Replace(" ", "%");
}
pageableRequests.Add(GetPagedRequests(parameters, btnResults, btnOffset));
return pageableRequests;
}
private IEnumerable<IndexerRequest> GetPagedRequests(BroadcastheNetTorrentQuery parameters, int results, int offset)
{
var builder = new JsonRpcRequestBuilder(Settings.BaseUrl)
.Call("getTorrents", Settings.ApiKey, parameters, results, offset);
builder.SuppressHttpError = true;
yield return new IndexerRequest(builder.Build());
}
}
}

View File

@@ -1,4 +1,5 @@
using Newtonsoft.Json;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace NzbDrone.Core.Indexers.BroadcastheNet
{
@@ -13,15 +14,15 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Search { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Codec { get; set; }
public IEnumerable<string> Codec { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Container { get; set; }
public IEnumerable<string> Container { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Source { get; set; }
public IEnumerable<string> Source { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Resolution { get; set; }
public IEnumerable<string> Resolution { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Origin { get; set; }
public IEnumerable<string> Origin { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Hash { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]

View File

@@ -292,56 +292,60 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann
foreach (var setting in _definition.Settings)
{
var name = ".Config." + setting.Name;
var value = Settings.ExtraFieldData.GetValueOrDefault(setting.Name, setting.Default);
if ((setting.Type != "password" && setting.Name != "apikey" && setting.Name != "rsskey") && indexerLogging)
object defaultValue = setting.Type switch
{
"select" => setting.Options.OrderBy(x => x.Key).Select(x => x.Key).ToList().IndexOf(setting.Default).ToString().ParseInt64() ?? 0,
_ => setting.Default
};
var value = Settings.ExtraFieldData.GetValueOrDefault(setting.Name, defaultValue);
if (indexerLogging && setting.Type != "password" && setting.Name != "apikey" && setting.Name != "rsskey")
{
_logger.Trace($"{name} got value {value.ToJson()}");
}
if (setting.Type == "text" || setting.Type == "password")
switch (setting.Type)
{
variables[name] = value;
}
else if (setting.Type == "checkbox")
{
if (value is string stringValue && bool.TryParse(stringValue, out var result))
{
value = result;
}
case "text":
case "password":
variables[name] = value;
break;
case "checkbox":
if (value is string stringValue && bool.TryParse(stringValue, out var result))
{
value = result;
}
variables[name] = (bool)value ? ".True" : null;
}
else if (setting.Type == "select")
{
if (indexerLogging)
{
_logger.Trace($"Setting options: {setting.Options.ToJson()}");
}
variables[name] = (bool)value ? ".True" : null;
break;
case "select":
if (indexerLogging)
{
_logger.Trace($"Setting options: {setting.Options.ToJson()}");
}
var sorted = setting.Options.OrderBy(x => x.Key).ToList();
var selected = sorted[(int)(long)value];
var sorted = setting.Options.OrderBy(x => x.Key).ToList();
var selected = sorted[(int)(long)value];
if (indexerLogging)
{
_logger.Debug($"Selected option: {selected.ToJson()}");
}
if (indexerLogging)
{
_logger.Debug($"Selected option: {selected.ToJson()}");
}
variables[name] = selected.Key;
}
else if (setting.Type == "info")
{
variables[name] = value;
}
else if (setting.Type == "cardigannCaptcha")
{
}
else
{
throw new NotSupportedException($"Type {setting.Type} is not supported.");
variables[name] = selected.Key;
break;
case "info":
variables[name] = value;
break;
case "cardigannCaptcha":
break;
default:
throw new NotSupportedException($"Type {setting.Type} is not supported.");
}
if (setting.Type != "password" && setting.Name != "apikey" && setting.Name != "rsskey" && indexerLogging && variables.ContainsKey(name))
if (indexerLogging && setting.Type != "password" && setting.Name != "apikey" && setting.Name != "rsskey" && variables.ContainsKey(name))
{
_logger.Debug($"Setting {setting.Name} to {variables[name].ToJson()}");
}

View File

@@ -9,7 +9,8 @@ namespace NzbDrone.Core.Indexers.Definitions;
public class DICMusic : GazelleBase<DICMusicSettings>
{
public override string Name => "DICMusic";
public override string[] IndexerUrls => new[] { "https://dicmusic.club/" };
public override string[] IndexerUrls => new[] { "https://dicmusic.com/" };
public override string[] LegacyUrls => new[] { "https://dicmusic.club/" };
public override string Description => "DICMusic is a CHINESE Private Torrent Tracker for MUSIC";
public override string Language => "zh-CN";
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;

View File

@@ -54,6 +54,8 @@ namespace NzbDrone.Core.Indexers.Definitions
{
private readonly IndexerCapabilitiesCategories _categories;
protected override string TimezoneOffset => "+01:00";
public ExoticaZParser(IndexerCapabilitiesCategories categories)
{
_categories = categories;

View File

@@ -1,19 +1,22 @@
using System.Collections.Generic;
using System.Linq;
using NLog;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Indexers.Definitions.FileList;
public class FileList : TorrentIndexerBase<FileListSettings>
{
public override string Name => "FileList.io";
public override string[] IndexerUrls => new[]
public override string[] IndexerUrls => new[] { "https://filelist.io/" };
public override string[] LegacyUrls => new[]
{
"https://filelist.io/",
"https://filelist.io",
"https://flro.org/"
};
public override string[] LegacyUrls => new[] { "https://filelist.io" };
public override string Description => "FileList (FL) is a ROMANIAN Private Torrent Tracker for 0DAY / GENERAL";
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override bool SupportsRss => true;
@@ -40,6 +43,13 @@ public class FileList : TorrentIndexerBase<FileListSettings>
return new FileListParser(Settings, Capabilities.Categories);
}
protected override IList<ReleaseInfo> CleanupReleases(IEnumerable<ReleaseInfo> releases, SearchCriteriaBase searchCriteria)
{
var cleanReleases = base.CleanupReleases(releases, searchCriteria);
return FilterReleasesByQuery(cleanReleases, searchCriteria).ToList();
}
private IndexerCapabilities SetCapabilities()
{
var caps = new IndexerCapabilities

View File

@@ -72,7 +72,7 @@ public class FileListParser : IParseIndexerResponse
InfoUrl = GetInfoUrl(id),
Seeders = row.Seeders,
Peers = row.Leechers + row.Seeders,
PublishDate = DateTime.Parse(row.UploadDate + " +0300", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal),
PublishDate = DateTime.Parse(row.UploadDate + " +0200", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal),
Description = row.SmallDescription,
Genres = row.SmallDescription.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).ToList(),
ImdbId = imdbId,

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization;
using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
@@ -25,6 +26,31 @@ public class FileListRequestGenerator : IIndexerRequestGenerator
{
parameters.Set("action", "search-torrents");
var searchQuery = searchCriteria.SanitizedSearchTerm.Trim();
if (DateTime.TryParseExact($"{searchCriteria.Season} {searchCriteria.Episode}", "yyyy MM/dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var showDate))
{
if (searchCriteria.ImdbId.IsNotNullOrWhiteSpace())
{
// Skip ID searches for daily episodes
return pageableRequests;
}
searchQuery = $"{searchQuery} {showDate:yyyy.MM.dd}".Trim();
}
else
{
if (searchCriteria.Season.HasValue)
{
parameters.Set("season", searchCriteria.Season.ToString());
}
if (searchCriteria.Episode.IsNotNullOrWhiteSpace())
{
parameters.Set("episode", searchCriteria.Episode);
}
}
if (searchCriteria.ImdbId.IsNotNullOrWhiteSpace())
{
parameters.Set("type", "imdb");
@@ -33,17 +59,7 @@ public class FileListRequestGenerator : IIndexerRequestGenerator
else if (searchCriteria.SearchTerm.IsNotNullOrWhiteSpace())
{
parameters.Set("type", "name");
parameters.Set("query", searchCriteria.SanitizedSearchTerm.Trim());
}
if (searchCriteria.Season.HasValue)
{
parameters.Set("season", searchCriteria.Season.ToString());
}
if (searchCriteria.Episode.IsNotNullOrWhiteSpace())
{
parameters.Set("episode", searchCriteria.Episode);
parameters.Set("query", searchQuery);
}
}

View File

@@ -35,6 +35,7 @@ public class GazelleTorrent
public string Category { get; set; }
public bool IsFreeLeech { get; set; }
public bool IsNeutralLeech { get; set; }
public bool IsFreeload { get; set; }
public bool IsPersonalFreeLeech { get; set; }
public bool CanUseToken { get; set; }
}
@@ -63,6 +64,7 @@ public class GazelleRelease
public List<GazelleTorrent> Torrents { get; set; }
public bool IsFreeLeech { get; set; }
public bool IsNeutralLeech { get; set; }
public bool IsFreeload { get; set; }
public bool IsPersonalFreeLeech { get; set; }
public bool CanUseToken { get; set; }
}

View File

@@ -12,9 +12,10 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits
public override string[] LegacyUrls => new[] { "https://hdbits.org" };
public override string Description => "Best HD Tracker";
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override IndexerCapabilities Capabilities => SetCapabilities();
public override bool SupportsRedirect => true;
public override int PageSize => 30;
public override bool SupportsPagination => true;
public override int PageSize => 100;
public override IndexerCapabilities Capabilities => SetCapabilities();
public HDBits(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
@@ -45,14 +46,14 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits
}
};
caps.Categories.AddCategoryMapping(6, NewznabStandardCategory.Audio, "Audio Track");
caps.Categories.AddCategoryMapping(3, NewznabStandardCategory.TVDocumentary, "Documentary");
caps.Categories.AddCategoryMapping(8, NewznabStandardCategory.Other, "Misc/Demo");
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.Movies, "Movie");
caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.TV, "TV");
caps.Categories.AddCategoryMapping(3, NewznabStandardCategory.TVDocumentary, "Documentary");
caps.Categories.AddCategoryMapping(4, NewznabStandardCategory.Audio, "Music");
caps.Categories.AddCategoryMapping(5, NewznabStandardCategory.TVSport, "Sport");
caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.TV, "TV");
caps.Categories.AddCategoryMapping(6, NewznabStandardCategory.Audio, "Audio Track");
caps.Categories.AddCategoryMapping(7, NewznabStandardCategory.XXX, "XXX");
caps.Categories.AddCategoryMapping(8, NewznabStandardCategory.Other, "Misc/Demo");
return caps;
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace NzbDrone.Core.Indexers.Definitions.HDBits
@@ -7,19 +8,15 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits
{
[JsonProperty(Required = Required.Always)]
public string Username { get; set; }
[JsonProperty(Required = Required.Always)]
public string Passkey { get; set; }
public string Hash { get; set; }
public string Search { get; set; }
public int[] Category { get; set; }
public int[] Codec { get; set; }
public int[] Medium { get; set; }
public IEnumerable<int> Category { get; set; }
public IEnumerable<int> Codec { get; set; }
public IEnumerable<int> Medium { get; set; }
public int? Origin { get; set; }
[JsonProperty(PropertyName = "imdb")]
@@ -33,13 +30,9 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits
[JsonProperty(PropertyName = "snatched_only")]
public bool? SnatchedOnly { get; set; }
public int? Limit { get; set; }
public int? Page { get; set; }
public TorrentQuery Clone()
{
return MemberwiseClone() as TorrentQuery;
}
}
public class HDBitsResponse

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Net;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.Parser.Model;
@@ -14,6 +15,8 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits
private readonly HDBitsSettings _settings;
private readonly IndexerCapabilitiesCategories _categories;
private readonly List<int> _halfLeechMediums = new () { (int)HdBitsMedium.Bluray, (int)HdBitsMedium.Remux, (int)HdBitsMedium.Capture };
public HDBitsParser(HDBitsSettings settings, IndexerCapabilitiesCategories categories)
{
_settings = settings;
@@ -22,7 +25,6 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
{
var torrentInfos = new List<ReleaseInfo>();
var indexerHttpResponse = indexerResponse.HttpResponse;
if (indexerHttpResponse.StatusCode == HttpStatusCode.Forbidden)
@@ -42,18 +44,25 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits
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)
if (jsonResponse.Data is not JArray responseData)
{
throw new IndexerException(indexerResponse, "Indexer API call response missing result data");
}
var releaseInfos = new List<ReleaseInfo>();
var queryResults = responseData.ToObject<TorrentQueryResponse[]>();
foreach (var result in queryResults)
{
// Skip non-freeleech results when freeleech only is set
if (_settings.FreeleechOnly && result.FreeLeech != "yes")
{
continue;
}
var id = result.Id;
var internalRelease = result.TypeOrigin == 1 ? true : false;
var internalRelease = result.TypeOrigin == 1;
var flags = new HashSet<IndexerFlag>();
@@ -62,10 +71,10 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits
flags.Add(IndexerFlag.Internal);
}
torrentInfos.Add(new HDBitsInfo()
releaseInfos.Add(new HDBitsInfo
{
Guid = string.Format("HDBits-{0}", id),
Title = result.Name,
Guid = $"HDBits-{id}",
Title = GetTitle(result),
Size = result.Size,
Categories = _categories.MapTrackerCatToNewznab(result.TypeCategory.ToString()),
InfoHash = result.Hash,
@@ -77,19 +86,55 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits
Peers = result.Leechers + result.Seeders,
PublishDate = result.Added.ToUniversalTime(),
Internal = internalRelease,
Year = result.ImdbInfo?.Year ?? 0,
ImdbId = result.ImdbInfo?.Id ?? 0,
TvdbId = result.TvdbInfo?.Id ?? 0,
DownloadVolumeFactor = result.FreeLeech == "yes" ? 0 : 1,
UploadVolumeFactor = 1,
DownloadVolumeFactor = GetDownloadVolumeFactor(result),
UploadVolumeFactor = GetUploadVolumeFactor(result),
IndexerFlags = flags
});
}
return torrentInfos.ToArray();
return releaseInfos.ToArray();
}
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
private string GetTitle(TorrentQueryResponse item)
{
return _settings.UseFilenames && item.FileName.IsNotNullOrWhiteSpace()
? item.FileName.Replace(".torrent", "", StringComparison.InvariantCultureIgnoreCase)
: item.Name;
}
private double GetDownloadVolumeFactor(TorrentQueryResponse item)
{
if (item.FreeLeech == "yes")
{
return 0;
}
// 100% Neutral Leech: all XXX content.
if (item.TypeCategory == 7)
{
return 0;
}
// 50% Free Leech: all full discs, remuxes, captures and all internal encodes, also all TV and Documentary content.
if (_halfLeechMediums.Contains(item.TypeMedium) || item.TypeOrigin == 1 || item.TypeCategory is 2 or 3)
{
return 0.5;
}
return 1;
}
private static double GetUploadVolumeFactor(TorrentQueryResponse item)
{
// 100% Neutral Leech: all XXX content.
return item.TypeCategory == 7 ? 0 : 1;
}
private string GetDownloadUrl(string torrentId)
{
var url = new HttpUri(_settings.BaseUrl)

View File

@@ -21,12 +21,8 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits
{
var pageableRequests = new IndexerPageableRequestChain();
var query = new TorrentQuery();
var imdbId = ParseUtil.GetImdbId(searchCriteria.ImdbId).GetValueOrDefault(0);
if (searchCriteria.Categories?.Length > 0)
{
query.Category = Capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories).Select(int.Parse).ToArray();
}
var imdbId = ParseUtil.GetImdbId(searchCriteria.ImdbId).GetValueOrDefault(0);
if (imdbId == 0 && searchCriteria.SearchTerm.IsNotNullOrWhiteSpace())
{
@@ -39,37 +35,11 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits
query.ImdbInfo.Id = imdbId;
}
pageableRequests.Add(GetRequest(query));
pageableRequests.Add(GetRequest(query, searchCriteria));
return pageableRequests;
}
public Func<IDictionary<string, string>> GetCookies { get; set; }
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
private IEnumerable<IndexerRequest> GetRequest(TorrentQuery query)
{
var request = new HttpRequestBuilder(Settings.BaseUrl)
.Resource("/api/torrents")
.Build();
request.Method = HttpMethod.Post;
const string appJson = "application/json";
request.Headers.Accept = appJson;
request.Headers.ContentType = appJson;
query.Username = Settings.Username;
query.Passkey = Settings.ApiKey;
query.Codec = Settings.Codecs.ToArray();
query.Medium = Settings.Mediums.ToArray();
request.SetContent(query.ToJson());
request.ContentSummary = query.ToJson(Formatting.None);
yield return new IndexerRequest(request);
}
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
{
return new IndexerPageableRequestChain();
@@ -79,14 +49,10 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits
{
var pageableRequests = new IndexerPageableRequestChain();
var query = new TorrentQuery();
var tvdbId = searchCriteria.TvdbId.GetValueOrDefault(0);
var imdbId = ParseUtil.GetImdbId(searchCriteria.ImdbId).GetValueOrDefault(0);
if (searchCriteria.Categories?.Length > 0)
{
query.Category = Capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories).Select(int.Parse).ToArray();
}
if (tvdbId == 0 && imdbId == 0 && searchCriteria.SearchTerm.IsNotNullOrWhiteSpace())
{
query.Search = searchCriteria.SanitizedTvSearchString;
@@ -114,7 +80,7 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits
query.ImdbInfo.Id = imdbId;
}
pageableRequests.Add(GetRequest(query));
pageableRequests.Add(GetRequest(query, searchCriteria));
return pageableRequests;
}
@@ -129,19 +95,56 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits
var pageableRequests = new IndexerPageableRequestChain();
var query = new TorrentQuery();
if (searchCriteria.Categories?.Length > 0)
{
query.Category = Capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories).Select(int.Parse).ToArray();
}
if (searchCriteria.SearchTerm.IsNotNullOrWhiteSpace())
{
query.Search = searchCriteria.SanitizedSearchTerm;
}
pageableRequests.Add(GetRequest(query));
pageableRequests.Add(GetRequest(query, searchCriteria));
return pageableRequests;
}
public Func<IDictionary<string, string>> GetCookies { get; set; }
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
private IEnumerable<IndexerRequest> GetRequest(TorrentQuery query, SearchCriteriaBase searchCriteria)
{
var request = new HttpRequestBuilder(Settings.BaseUrl)
.Resource("/api/torrents")
.Build();
request.Method = HttpMethod.Post;
const string appJson = "application/json";
request.Headers.Accept = appJson;
request.Headers.ContentType = appJson;
query.Username = Settings.Username;
query.Passkey = Settings.ApiKey;
query.Codec = Settings.Codecs.ToArray();
query.Medium = Settings.Mediums.ToArray();
if (searchCriteria.Categories?.Length > 0)
{
query.Category = Capabilities.Categories
.MapTorznabCapsToTrackers(searchCriteria.Categories)
.Distinct()
.Select(int.Parse)
.ToArray();
}
query.Limit = 100;
if (searchCriteria.Limit is > 0 && searchCriteria.Offset is > 0)
{
query.Page = (int)(searchCriteria.Offset / searchCriteria.Limit);
}
request.SetContent(query.ToJson());
request.ContentSummary = query.ToJson(Formatting.None);
yield return new IndexerRequest(request);
}
}
}

View File

@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using FluentValidation;
using NzbDrone.Core.Annotations;
@@ -10,6 +11,7 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits
{
public HDBitsSettingsValidator()
{
RuleFor(c => c.Username).NotEmpty();
RuleFor(c => c.ApiKey).NotEmpty();
}
}
@@ -20,8 +22,10 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits
public HDBitsSettings()
{
Codecs = System.Array.Empty<int>();
Mediums = System.Array.Empty<int>();
Codecs = Array.Empty<int>();
Mediums = Array.Empty<int>();
FreeleechOnly = false;
UseFilenames = true;
}
[FieldDefinition(2, Label = "Username", HelpText = "Site Username", Privacy = PrivacyLevel.UserName)]
@@ -30,45 +34,49 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits
[FieldDefinition(3, Label = "API Key", HelpText = "Site API Key", Privacy = PrivacyLevel.ApiKey)]
public string ApiKey { get; set; }
[FieldDefinition(4, Label = "Codecs", Type = FieldType.TagSelect, SelectOptions = typeof(HdBitsCodec), Advanced = true, HelpText = "Options: h264, Mpeg2, VC1, Xvid. If unspecified, all options are used.")]
[FieldDefinition(4, Label = "Codecs", Type = FieldType.Select, SelectOptions = typeof(HdBitsCodec), Advanced = true, HelpText = "If unspecified, all options are used.")]
public IEnumerable<int> Codecs { get; set; }
[FieldDefinition(5, Label = "Mediums", Type = FieldType.TagSelect, SelectOptions = typeof(HdBitsMedium), Advanced = true, HelpText = "Options: BluRay, Encode, Capture, Remux, WebDL. If unspecified, all options are used.")]
[FieldDefinition(5, Label = "Mediums", Type = FieldType.Select, SelectOptions = typeof(HdBitsMedium), Advanced = true, HelpText = "If unspecified, all options are used.")]
public IEnumerable<int> Mediums { get; set; }
[FieldDefinition(6, Label = "Freeleech Only", Type = FieldType.Checkbox, Advanced = true, HelpText = "Show freeleech releases only")]
public bool FreeleechOnly { get; set; }
[FieldDefinition(7, Label = "Use Filenames", Type = FieldType.Checkbox, HelpText = "Check this option if you want to use torrent filenames as release titles")]
public bool UseFilenames { get; set; }
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
public enum HdBitsCategory
{
Movie = 1,
Tv = 2,
Documentary = 3,
Music = 4,
Sport = 5,
Audio = 6,
Xxx = 7,
MiscDemo = 8
}
public enum HdBitsCodec
{
[FieldOption("H.264")]
H264 = 1,
[FieldOption("MPEG-2")]
Mpeg2 = 2,
[FieldOption("VC-1")]
Vc1 = 3,
[FieldOption("XviD")]
Xvid = 4,
[FieldOption("HEVC")]
HEVC = 5
}
public enum HdBitsMedium
{
[FieldOption("Blu-ray/HD DVD")]
Bluray = 1,
[FieldOption("Encode")]
Encode = 3,
[FieldOption("Capture")]
Capture = 4,
[FieldOption("Remux")]
Remux = 5,
[FieldOption("WEB-DL")]
WebDl = 6
}
}

View File

@@ -9,6 +9,7 @@ using AngleSharp.Html.Parser;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.Indexers.Settings;
@@ -19,7 +20,7 @@ using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Indexers.Definitions
{
public class HDSpace : TorrentIndexerBase<UserPassTorrentBaseSettings>
public class HDSpace : TorrentIndexerBase<HDSpaceSettings>
{
public override string Name => "HD-Space";
public override string[] IndexerUrls => new[] { "https://hd-space.org/" };
@@ -225,10 +226,10 @@ namespace NzbDrone.Core.Indexers.Definitions
public class HDSpaceParser : IParseIndexerResponse
{
private readonly UserPassTorrentBaseSettings _settings;
private readonly HDSpaceSettings _settings;
private readonly IndexerCapabilitiesCategories _categories;
public HDSpaceParser(UserPassTorrentBaseSettings settings, IndexerCapabilitiesCategories categories)
public HDSpaceParser(HDSpaceSettings settings, IndexerCapabilitiesCategories categories)
{
_settings = settings;
_categories = categories;
@@ -251,9 +252,34 @@ namespace NzbDrone.Core.Indexers.Definitions
continue;
}
var release = new TorrentInfo();
release.MinimumRatio = 1;
release.MinimumSeedTime = 86400; // 24 hours
var downloadVolumeFactor = 1.0;
if (row.QuerySelector("img[title=\"FreeLeech\"]") != null)
{
downloadVolumeFactor = 0;
}
else if (row.QuerySelector("img[src=\"images/sf.png\"]") != null)
{
downloadVolumeFactor = 0;
}
else if (row.QuerySelector("img[title=\"Half FreeLeech\"]") != null)
{
downloadVolumeFactor = 0.5;
}
// Skip non-freeleech results when freeleech only is set
if (_settings.FreeleechOnly && downloadVolumeFactor != 0.0)
{
continue;
}
var release = new TorrentInfo
{
DownloadVolumeFactor = downloadVolumeFactor,
UploadVolumeFactor = 1,
MinimumRatio = 1,
MinimumSeedTime = 86400 // 24 hours
};
var qLink = row.QuerySelector("td:nth-child(2) a[href^=\"index.php?page=torrent-details&id=\"]");
release.Title = qLink?.TextContent.Trim();
@@ -292,25 +318,6 @@ namespace NzbDrone.Core.Indexers.Definitions
var grabs = row.QuerySelector("td:nth-child(10)")?.TextContent.Trim().Replace("---", "0");
release.Grabs = ParseUtil.CoerceInt(grabs);
if (row.QuerySelector("img[title=\"FreeLeech\"]") != null)
{
release.DownloadVolumeFactor = 0;
}
else if (row.QuerySelector("img[src=\"images/sf.png\"]") != null)
{
release.DownloadVolumeFactor = 0;
}
else if (row.QuerySelector("img[title=\"Half FreeLeech\"]") != null)
{
release.DownloadVolumeFactor = 0.5;
}
else
{
release.DownloadVolumeFactor = 1;
}
release.UploadVolumeFactor = 1;
var categoryLink = row.QuerySelector("a[href^=\"index.php?page=torrents&category=\"]").GetAttribute("href");
var cat = ParseUtil.GetArgumentFromQueryString(categoryLink, "category");
release.Categories = _categories.MapTrackerCatToNewznab(cat);
@@ -323,4 +330,10 @@ namespace NzbDrone.Core.Indexers.Definitions
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}
public class HDSpaceSettings : UserPassTorrentBaseSettings
{
[FieldDefinition(4, Label = "Freeleech Only", Type = FieldType.Checkbox, HelpText = "Show freeleech releases only")]
public bool FreeleechOnly { get; set; } = false;
}
}

View File

@@ -73,6 +73,13 @@ namespace NzbDrone.Core.Indexers.Definitions
return CookieUtil.CookieHeaderToDictionary(Settings.Cookie);
}
protected override IList<ReleaseInfo> CleanupReleases(IEnumerable<ReleaseInfo> releases, SearchCriteriaBase searchCriteria)
{
var cleanReleases = base.CleanupReleases(releases, searchCriteria);
return FilterReleasesByQuery(cleanReleases, searchCriteria).ToList();
}
private IndexerCapabilities SetCapabilities()
{
var caps = new IndexerCapabilities
@@ -293,6 +300,10 @@ namespace NzbDrone.Core.Indexers.Definitions
var parser = new HtmlParser();
using var doc = parser.ParseDocument(indexerResponse.Content);
var headerColumns = doc.QuerySelectorAll("table[id=\"torrents\"] > thead > tr > th").Select(x => x.TextContent.Trim()).ToList();
var sizeIndex = FindColumnIndexOrDefault(headerColumns, "Sort by size", 5);
var filesIndex = FindColumnIndexOrDefault(headerColumns, "Sort by files");
var rows = doc.QuerySelectorAll("table[id=\"torrents\"] > tbody > tr");
foreach (var row in rows)
{
@@ -314,7 +325,6 @@ namespace NzbDrone.Core.Indexers.Definitions
var dateSplit = descrSplit.Last().Split(new[] { " by " }, StringSplitOptions.None);
var publishDate = DateTimeUtil.FromTimeAgo(dateSplit.First());
var description = descrSplit.Length > 1 ? "Tags: " + descrSplit.First().Trim() : "";
description += dateSplit.Length > 1 ? " Uploaded by: " + dateSplit.Last().Trim() : "";
var catIcon = row.QuerySelector("td:nth-of-type(1) a");
if (catIcon == null)
@@ -327,36 +337,40 @@ namespace NzbDrone.Core.Indexers.Definitions
// Torrents - Category column == Icons
var cat = _categories.MapTrackerCatToNewznab(catIcon.GetAttribute("href").Substring(1));
var size = ParseUtil.GetBytes(row.Children[5].TextContent);
var size = ParseUtil.GetBytes(row.Children[sizeIndex].TextContent);
var colIndex = 6;
int? files = null;
if (row.Children.Length == 10)
if (filesIndex != -1)
{
files = ParseUtil.CoerceInt(row.Children[colIndex].TextContent.Replace("Go to files", ""));
colIndex++;
files = ParseUtil.CoerceInt(row.Children[filesIndex].TextContent.Replace("Go to files", ""));
}
var grabs = ParseUtil.CoerceInt(row.Children[colIndex++].TextContent);
var seeders = ParseUtil.CoerceInt(row.Children[colIndex++].TextContent);
var leechers = ParseUtil.CoerceInt(row.Children[colIndex].TextContent);
var dlVolumeFactor = row.QuerySelector("span.free") != null ? 0 : 1;
var colIndex = row.Children.Length == 10 ? 7 : 6;
var grabsIndex = FindColumnIndexOrDefault(headerColumns, "Sort by snatches", colIndex++);
var seedersIndex = FindColumnIndexOrDefault(headerColumns, "Sort by seeders", colIndex++);
var leechersIndex = FindColumnIndexOrDefault(headerColumns, "Sort by leechers", colIndex);
var grabs = ParseUtil.CoerceInt(row.Children[grabsIndex].TextContent);
var seeders = ParseUtil.CoerceInt(row.Children[seedersIndex].TextContent);
var leechers = ParseUtil.CoerceInt(row.Children[leechersIndex].TextContent);
var release = new TorrentInfo
{
Title = title,
Guid = details.AbsoluteUri,
DownloadUrl = link.AbsoluteUri,
InfoUrl = details.AbsoluteUri,
PublishDate = publishDate,
Title = title,
Description = description,
Categories = cat,
Size = size,
Files = files,
Grabs = grabs,
Seeders = seeders,
Peers = seeders + leechers,
DownloadVolumeFactor = dlVolumeFactor,
PublishDate = publishDate,
DownloadVolumeFactor = row.QuerySelector("span.free") != null ? 0 : 1,
UploadVolumeFactor = 1,
MinimumRatio = 1,
MinimumSeedTime = 1209600 // 336 hours
@@ -368,6 +382,13 @@ namespace NzbDrone.Core.Indexers.Definitions
return torrentInfos.ToArray();
}
private static int FindColumnIndexOrDefault(List<string> columns, string name, int defaultIndex = -1)
{
var index = columns.FindIndex(x => x.Equals(name, StringComparison.Ordinal));
return index != -1 ? index : defaultIndex;
}
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
private static string CleanTitle(string title)

View File

@@ -10,6 +10,7 @@ using AngleSharp.Html.Parser;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.Indexers.Settings;
@@ -20,7 +21,7 @@ using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Indexers.Definitions
{
public class ImmortalSeed : TorrentIndexerBase<UserPassTorrentBaseSettings>
public class ImmortalSeed : TorrentIndexerBase<ImmortalSeedSettings>
{
public override string Name => "ImmortalSeed";
public override string[] IndexerUrls => new[] { "https://immortalseed.me/" };
@@ -46,7 +47,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public override IParseIndexerResponse GetParser()
{
return new ImmortalSeedParser(Capabilities.Categories);
return new ImmortalSeedParser(Settings, Capabilities.Categories);
}
protected override async Task DoLogin()
@@ -253,11 +254,14 @@ namespace NzbDrone.Core.Indexers.Definitions
public class ImmortalSeedParser : IParseIndexerResponse
{
private readonly ImmortalSeedSettings _settings;
private readonly IndexerCapabilitiesCategories _categories;
private readonly Regex _dateAddedRegex = new (@"\d{4}-\d{2}-\d{2} \d{2}:\d{2} [AaPp][Mm]", RegexOptions.Compiled);
public ImmortalSeedParser(IndexerCapabilitiesCategories categories)
public ImmortalSeedParser(ImmortalSeedSettings settings, IndexerCapabilitiesCategories categories)
{
_settings = settings;
_categories = categories;
}
@@ -271,6 +275,23 @@ namespace NzbDrone.Core.Indexers.Definitions
var rows = dom.QuerySelectorAll("table#sortabletable > tbody > tr:has(a[href*=\"details.php?id=\"])");
foreach (var row in rows)
{
var downloadVolumeFactor = 1.0;
if (row.QuerySelector("img[title^=\"Free Torrent\"], img[title^=\"Sitewide Free Torrent\"]") != null)
{
downloadVolumeFactor = 0.0;
}
else if (row.QuerySelector("img[title^=\"Silver Torrent\"]") != null)
{
downloadVolumeFactor = 0.5;
}
// Skip non-freeleech results when freeleech only is set
if (_settings.FreeleechOnly && downloadVolumeFactor != 0.0)
{
continue;
}
// details link, release name gets shortened if it's to long
var qDetails = row.QuerySelector("div > a[href*=\"details.php?id=\"]");
@@ -300,6 +321,7 @@ namespace NzbDrone.Core.Indexers.Definitions
Peers = peers,
Size = ParseUtil.GetBytes(row.QuerySelector("td:nth-of-type(5)")?.TextContent.Trim()),
Grabs = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(6)")?.TextContent),
DownloadVolumeFactor = downloadVolumeFactor,
UploadVolumeFactor = row.QuerySelector("img[title^=\"x2 Torrent\"]") != null ? 2 : 1,
MinimumRatio = 1,
MinimumSeedTime = 86400 // 24 hours
@@ -311,19 +333,6 @@ namespace NzbDrone.Core.Indexers.Definitions
release.PublishDate = DateTime.ParseExact(dateAddedMatch.Value, "yyyy-MM-dd hh:mm tt", CultureInfo.InvariantCulture);
}
if (row.QuerySelector("img[title^=\"Free Torrent\"], img[title^=\"Sitewide Free Torrent\"]") != null)
{
release.DownloadVolumeFactor = 0;
}
else if (row.QuerySelector("img[title^=\"Silver Torrent\"]") != null)
{
release.DownloadVolumeFactor = 0.5;
}
else
{
release.DownloadVolumeFactor = 1;
}
releaseInfos.Add(release);
}
@@ -332,4 +341,10 @@ namespace NzbDrone.Core.Indexers.Definitions
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}
public class ImmortalSeedSettings : UserPassTorrentBaseSettings
{
[FieldDefinition(4, Label = "Freeleech Only", Type = FieldType.Checkbox, HelpText = "Show freeleech releases only")]
public bool FreeleechOnly { get; set; } = false;
}
}

View File

@@ -265,12 +265,18 @@ namespace NzbDrone.Core.Indexers.Definitions
Grabs = ParseUtil.CoerceInt(row.Snatch),
Seeders = ParseUtil.CoerceInt(row.Seed),
Peers = ParseUtil.CoerceInt(row.Seed) + ParseUtil.CoerceInt(row.Leech),
Scene = row.Tags?.ContainsIgnoreCase("scene"),
MinimumRatio = 0, // ratioless
MinimumSeedTime = row.Category.ToLower() == "season" ? 432000 : 86400, // 120 hours for seasons and 24 hours for episodes
DownloadVolumeFactor = 0, // ratioless tracker
UploadVolumeFactor = 1
};
if (row.TvMazeId.IsNotNullOrWhiteSpace())
{
release.TvMazeId = ParseUtil.CoerceInt(row.TvMazeId);
}
torrentInfos.Add(release);
}
@@ -339,8 +345,11 @@ namespace NzbDrone.Core.Indexers.Definitions
public string Banner { get; set; }
[JsonPropertyName("group_id")]
public string TorrentId { get; set; }
[JsonPropertyName("series_id")]
public string TvMazeId { get; set; }
[JsonPropertyName("rls_utc")]
public string PublishDateUtc { get; set; }
public IEnumerable<string> Tags { get; set; }
}
public class NebulanceTorrents

View File

@@ -31,6 +31,7 @@ namespace NzbDrone.Core.Indexers.Newznab
{
return new NewznabRequestGenerator(_capabilitiesProvider)
{
Definition = Definition,
PageSize = PageSize,
Settings = Settings
};
@@ -101,11 +102,7 @@ namespace NzbDrone.Core.Indexers.Newznab
yield return GetDefinition("NZBNDX", GetSettings("https://www.nzbndx.com"));
yield return GetDefinition("NzbPlanet", GetSettings("https://api.nzbplanet.net"));
yield return GetDefinition("NZBStars", GetSettings("https://nzbstars.com"));
yield return GetDefinition("OZnzb", GetSettings("https://api.oznzb.com"));
yield return GetDefinition("SimplyNZBs", GetSettings("https://simplynzbs.com"));
yield return GetDefinition("SpotNZB", GetSettings("https://spotnzb.xyz"));
yield return GetDefinition("Tabula Rasa", GetSettings("https://www.tabula-rasa.pw", apiPath: @"/api/v1/api"));
yield return GetDefinition("VeryCouch LazyMuch", GetSettings("https://api.verycouch.com"));
yield return GetDefinition("Generic Newznab", GetSettings(""));
}
}

View File

@@ -129,6 +129,8 @@ namespace NzbDrone.Core.Indexers.Newznab
capabilities.SearchParams.AddIfNotNull(searchParam);
}
}
capabilities.SupportsRawSearch = xmlBasicSearch.Attribute("searchEngine")?.Value == "raw";
}
else
{

View File

@@ -13,10 +13,11 @@ namespace NzbDrone.Core.Indexers.Newznab
public class NewznabRequestGenerator : IIndexerRequestGenerator
{
private readonly INewznabCapabilitiesProvider _capabilitiesProvider;
public ProviderDefinition Definition { get; set; }
public int MaxPages { get; set; }
public int PageSize { get; set; }
public NewznabSettings Settings { get; set; }
public ProviderDefinition Definition { get; set; }
public NewznabRequestGenerator(INewznabCapabilitiesProvider capabilitiesProvider)
{

View File

@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using AngleSharp.Dom;
@@ -76,12 +75,16 @@ public class NorBits : TorrentIndexerBase<NorBitsSettings>
{
LogResponseContent = true,
AllowAutoRedirect = true,
Method = HttpMethod.Post
SuppressHttpError = true
};
var authLoginCheckRequest = requestBuilder3
.Post()
.AddFormParameter("username", Settings.Username)
.AddFormParameter("password", Settings.Password)
.AddFormParameter("code", Settings.TwoFactorAuthCode ?? string.Empty)
.AddFormParameter("logout", "no")
.AddFormParameter("returnto", "/")
.SetCookies(indexPage.GetCookies())
.SetHeader("Referer", loginUrl)
.Build();
@@ -174,6 +177,11 @@ public class NorBitsRequestGenerator : IIndexerRequestGenerator
{ "scenerelease", "0" }
};
if (_settings.FreeLeechOnly)
{
parameters.Set("FL", "1");
}
var searchTerm = "search=";
if (!string.IsNullOrWhiteSpace(imdbId))
@@ -343,6 +351,12 @@ public class NorBitsSettings : UserPassTorrentBaseSettings
UseFullSearch = false;
}
[FieldDefinition(4, Label = "Use Full Search", HelpText = "Use Full Search from Site", Type = FieldType.Checkbox)]
[FieldDefinition(4, Label = "2FA code", Type = FieldType.Textbox, HelpText = "Only fill in the <b>2FA code</b> box if you have enabled <b>2FA</b> on the NorBits Web Site. Otherwise just leave it empty.")]
public string TwoFactorAuthCode { get; set; }
[FieldDefinition(5, Label = "Use Full Search", Type = FieldType.Checkbox, HelpText = "Use Full Search from Site")]
public bool UseFullSearch { get; set; }
[FieldDefinition(6, Label = "FreeLeech Only", Type = FieldType.Checkbox, HelpText = "Search FreeLeech torrents only")]
public bool FreeLeechOnly { get; set; }
}

View File

@@ -29,6 +29,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override IndexerCapabilities Capabilities => SetCapabilities();
public override bool SupportsRedirect => true;
public override TimeSpan RateLimit => TimeSpan.FromSeconds(3);
public Orpheus(IIndexerHttpClient httpClient,
IEventAggregator eventAggregator,
@@ -49,10 +50,24 @@ namespace NzbDrone.Core.Indexers.Definitions
return new OrpheusParser(Settings, Capabilities.Categories);
}
protected override IList<ReleaseInfo> CleanupReleases(IEnumerable<ReleaseInfo> releases, SearchCriteriaBase searchCriteria)
{
var cleanReleases = base.CleanupReleases(releases, searchCriteria);
if (searchCriteria.IsRssSearch)
{
cleanReleases = cleanReleases.Take(50).ToList();
}
return cleanReleases;
}
private IndexerCapabilities SetCapabilities()
{
var caps = new IndexerCapabilities
{
LimitsDefault = 50,
LimitsMax = 50,
MusicSearchParams = new List<MusicSearchParam>
{
MusicSearchParam.Q, MusicSearchParam.Artist, MusicSearchParam.Album, MusicSearchParam.Year
@@ -200,6 +215,7 @@ namespace NzbDrone.Core.Indexers.Definitions
}
var queryCats = _capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories);
if (queryCats.Any())
{
queryCats.ForEach(cat => parameters.Set($"filter_cat[{cat}]", "1"));
@@ -273,6 +289,7 @@ namespace NzbDrone.Core.Indexers.Definitions
Title = WebUtility.HtmlDecode(title),
Artist = WebUtility.HtmlDecode(result.Artist),
Album = WebUtility.HtmlDecode(result.GroupName),
Year = int.Parse(result.GroupYear),
Container = torrent.Encoding,
Codec = torrent.Format,
Size = long.Parse(torrent.Size),
@@ -345,7 +362,7 @@ namespace NzbDrone.Core.Indexers.Definitions
private string GetTitle(GazelleRelease result, GazelleTorrent torrent)
{
var title = $"{result.Artist} - {result.GroupName} [{result.GroupYear}]";
var title = $"{result.Artist} - {result.GroupName} ({result.GroupYear})";
if (result.ReleaseType.IsNotNullOrWhiteSpace() && result.ReleaseType != "Unknown")
{
@@ -357,14 +374,23 @@ namespace NzbDrone.Core.Indexers.Definitions
title += $" [{$"{torrent.RemasterTitle} {torrent.RemasterYear}".Trim()}]";
}
title += $" [{torrent.Format} {torrent.Encoding}] [{torrent.Media}]";
var flags = new List<string>
{
$"{torrent.Format} {torrent.Encoding}",
$"{torrent.Media}"
};
if (torrent.HasLog)
{
flags.Add("Log (" + torrent.LogScore + "%)");
}
if (torrent.HasCue)
{
title += " [Cue]";
flags.Add("Cue");
}
return title;
return $"{title} [{string.Join(" / ", flags)}]";
}
private string GetDownloadUrl(int torrentId, bool canUseToken)
@@ -375,7 +401,7 @@ namespace NzbDrone.Core.Indexers.Definitions
.AddQueryParam("id", torrentId);
// Orpheus fails to download if usetoken=0 so we need to only add if we will use one
if (_settings.UseFreeleechToken)
if (_settings.UseFreeleechToken && canUseToken)
{
url = url.AddQueryParam("usetoken", "1");
}

View File

@@ -29,12 +29,12 @@ namespace NzbDrone.Core.Indexers.Definitions.PassThePopcorn
public override IIndexerRequestGenerator GetRequestGenerator()
{
return new PassThePopcornRequestGenerator(Settings);
return new PassThePopcornRequestGenerator(Settings, Capabilities);
}
public override IParseIndexerResponse GetParser()
{
return new PassThePopcornParser(Settings, _logger);
return new PassThePopcornParser(Settings);
}
private IndexerCapabilities SetCapabilities()
@@ -43,6 +43,10 @@ namespace NzbDrone.Core.Indexers.Definitions.PassThePopcorn
{
LimitsDefault = PageSize,
LimitsMax = PageSize,
TvSearchParams = new List<TvSearchParam>
{
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep, TvSearchParam.ImdbId
},
MovieSearchParams = new List<MovieSearchParam>
{
MovieSearchParam.Q, MovieSearchParam.ImdbId
@@ -56,18 +60,11 @@ namespace NzbDrone.Core.Indexers.Definitions.PassThePopcorn
};
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.Movies, "Feature Film");
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.MoviesForeign);
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.MoviesOther);
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.MoviesSD);
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.MoviesHD);
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.Movies3D);
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.MoviesBluRay);
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.MoviesDVD);
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.MoviesWEBDL);
caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.Movies, "Short Film");
caps.Categories.AddCategoryMapping(3, NewznabStandardCategory.TV, "Miniseries");
caps.Categories.AddCategoryMapping(4, NewznabStandardCategory.TV, "Stand-up Comedy");
caps.Categories.AddCategoryMapping(5, NewznabStandardCategory.TV, "Live Performance");
caps.Categories.AddCategoryMapping(4, NewznabStandardCategory.Movies, "Stand-up Comedy");
caps.Categories.AddCategoryMapping(5, NewznabStandardCategory.Movies, "Live Performance");
caps.Categories.AddCategoryMapping(6, NewznabStandardCategory.Movies, "Movie Collection");
return caps;
}

View File

@@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Net;
using NLog;
using System.Text.RegularExpressions;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer;
@@ -14,12 +14,12 @@ namespace NzbDrone.Core.Indexers.Definitions.PassThePopcorn
public class PassThePopcornParser : IParseIndexerResponse
{
private readonly PassThePopcornSettings _settings;
private readonly Logger _logger;
public PassThePopcornParser(PassThePopcornSettings settings, Logger logger)
private static Regex SeasonRegex => new (@"\bS\d{2,3}(E\d{2,3})?\b", RegexOptions.Compiled);
public PassThePopcornParser(PassThePopcornSettings settings)
{
_settings = settings;
_logger = logger;
}
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
@@ -57,6 +57,7 @@ namespace NzbDrone.Core.Indexers.Definitions.PassThePopcorn
foreach (var torrent in result.Torrents)
{
var id = torrent.Id;
var title = torrent.ReleaseName;
var flags = new HashSet<IndexerFlag>();
@@ -70,14 +71,21 @@ namespace NzbDrone.Core.Indexers.Definitions.PassThePopcorn
flags.Add(PassThePopcornFlag.Approved);
}
var categories = new List<IndexerCategory> { NewznabStandardCategory.Movies };
if (title != null && SeasonRegex.Match(title).Success)
{
categories.Add(NewznabStandardCategory.TV);
}
torrentInfos.Add(new TorrentInfo
{
Guid = $"PassThePopcorn-{id}",
Title = torrent.ReleaseName,
Title = title,
Year = int.Parse(result.Year),
InfoUrl = GetInfoUrl(result.GroupId, id),
DownloadUrl = GetDownloadUrl(id, jsonResponse.AuthKey, jsonResponse.PassKey),
Categories = new List<IndexerCategory> { NewznabStandardCategory.Movies },
Categories = categories,
Size = long.Parse(torrent.Size),
Grabs = int.Parse(torrent.Snatched),
Seeders = int.Parse(torrent.Seeders),
@@ -86,7 +94,12 @@ namespace NzbDrone.Core.Indexers.Definitions.PassThePopcorn
ImdbId = result.ImdbId.IsNotNullOrWhiteSpace() ? int.Parse(result.ImdbId) : 0,
Scene = torrent.Scene,
IndexerFlags = flags,
DownloadVolumeFactor = torrent.FreeleechType is "Freeleech" ? 0 : 1,
DownloadVolumeFactor = torrent.FreeleechType?.ToUpperInvariant() switch
{
"FREELEECH" => 0,
"HALF LEECH" => 0.5,
_ => 1
},
UploadVolumeFactor = 1,
MinimumRatio = 1,
MinimumSeedTime = 345600,

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.IndexerSearch.Definitions;
@@ -11,10 +12,12 @@ namespace NzbDrone.Core.Indexers.Definitions.PassThePopcorn
public class PassThePopcornRequestGenerator : IIndexerRequestGenerator
{
private readonly PassThePopcornSettings _settings;
private readonly IndexerCapabilities _capabilities;
public PassThePopcornRequestGenerator(PassThePopcornSettings settings)
public PassThePopcornRequestGenerator(PassThePopcornSettings settings, IndexerCapabilities capabilities)
{
_settings = settings;
_capabilities = capabilities;
}
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
@@ -27,7 +30,7 @@ namespace NzbDrone.Core.Indexers.Definitions.PassThePopcorn
}
else
{
pageableRequests.Add(GetRequest($"{searchCriteria.SearchTerm}", searchCriteria));
pageableRequests.Add(GetRequest($"{searchCriteria.SanitizedSearchTerm}", searchCriteria));
}
return pageableRequests;
@@ -40,7 +43,18 @@ namespace NzbDrone.Core.Indexers.Definitions.PassThePopcorn
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
{
return new IndexerPageableRequestChain();
var pageableRequests = new IndexerPageableRequestChain();
if (searchCriteria.ImdbId.IsNotNullOrWhiteSpace())
{
pageableRequests.Add(GetRequest(searchCriteria.FullImdbId, searchCriteria));
}
else
{
pageableRequests.Add(GetRequest($"{searchCriteria.SanitizedTvSearchString}", searchCriteria));
}
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
@@ -52,32 +66,43 @@ namespace NzbDrone.Core.Indexers.Definitions.PassThePopcorn
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetRequest($"{searchCriteria.SearchTerm}", searchCriteria));
pageableRequests.Add(GetRequest($"{searchCriteria.SanitizedSearchTerm}", searchCriteria));
return pageableRequests;
}
private IEnumerable<IndexerRequest> GetRequest(string searchParameters, SearchCriteriaBase searchCriteria)
private IEnumerable<IndexerRequest> GetRequest(string searchTerm, SearchCriteriaBase searchCriteria)
{
var parameters = new NameValueCollection
{
{ "action", "advanced" },
{ "json", "noredirect" },
{ "grouping", "0" },
{ "searchstr", searchParameters }
{ "searchstr", searchTerm }
};
if (_settings.FreeleechOnly)
{
parameters.Set("freetorrent", "1");
}
var queryCats = _capabilities.Categories
.MapTorznabCapsToTrackers(searchCriteria.Categories)
.Select(int.Parse)
.Distinct()
.ToList();
if (searchCriteria.IsRssSearch && queryCats.Any())
{
queryCats.ForEach(cat => parameters.Set($"filter_cat[{cat}]", "1"));
}
if (searchCriteria.Limit is > 0 && searchCriteria.Offset is > 0)
{
var page = (int)(searchCriteria.Offset / searchCriteria.Limit) + 1;
parameters.Set("page", page.ToString());
}
if (_settings.FreeleechOnly)
{
parameters.Set("freetorrent", "1");
}
var searchUrl = $"{_settings.BaseUrl.Trim().TrimEnd('/')}/torrents.php?{parameters.GetQueryString()}";
var request = new IndexerRequest(searchUrl, HttpAccept.Json);

View File

@@ -19,7 +19,7 @@ using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Indexers.Definitions;
[Obsolete("PirateTheNet has shutdown 2023-10-14")]
public class PirateTheNet : TorrentIndexerBase<UserPassTorrentBaseSettings>
{
public override string Name => "PirateTheNet";

View File

@@ -10,7 +10,7 @@ namespace NzbDrone.Core.Indexers.Definitions
{
public override string Name => "PrivateHD";
public override string[] IndexerUrls => new[] { "https://privatehd.to/" };
public override string Description => "PrivateHD is a Private Torrent Tracker for HD MOVIES / TV and the sister-site of AvistaZ, CinemaZ, ExoticaZ, and AnimeTorrents";
public override string Description => "PrivateHD (PHD) is a Private Torrent Tracker for HD MOVIES / TV and the sister-site of AvistaZ, CinemaZ, ExoticaZ, and AnimeTorrents";
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public PrivateHD(IIndexerRepository indexerRepository,

View File

@@ -29,6 +29,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override IndexerCapabilities Capabilities => SetCapabilities();
public override bool SupportsRedirect => true;
public override TimeSpan RateLimit => TimeSpan.FromSeconds(3);
public Redacted(IIndexerHttpClient httpClient,
IEventAggregator eventAggregator,
@@ -63,10 +64,24 @@ namespace NzbDrone.Core.Indexers.Definitions
return Task.FromResult(request);
}
protected override IList<ReleaseInfo> CleanupReleases(IEnumerable<ReleaseInfo> releases, SearchCriteriaBase searchCriteria)
{
var cleanReleases = base.CleanupReleases(releases, searchCriteria);
if (searchCriteria.IsRssSearch)
{
cleanReleases = cleanReleases.Take(50).ToList();
}
return cleanReleases;
}
private IndexerCapabilities SetCapabilities()
{
var caps = new IndexerCapabilities
{
LimitsDefault = 50,
LimitsMax = 50,
MusicSearchParams = new List<MusicSearchParam>
{
MusicSearchParam.Q, MusicSearchParam.Artist, MusicSearchParam.Album, MusicSearchParam.Year
@@ -172,11 +187,17 @@ namespace NzbDrone.Core.Indexers.Definitions
}
var queryCats = _capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories);
if (queryCats.Any())
{
queryCats.ForEach(cat => parameters.Set($"filter_cat[{cat}]", "1"));
}
if (_settings.FreeloadOnly)
{
parameters.Set("freetorrent", "4");
}
var searchUrl = _settings.BaseUrl.TrimEnd('/') + $"/ajax.php?{parameters.GetQueryString()}";
var request = new IndexerRequest(searchUrl, HttpAccept.Json);
@@ -226,6 +247,12 @@ namespace NzbDrone.Core.Indexers.Definitions
{
foreach (var torrent in result.Torrents)
{
// skip non-freeload results when freeload only is set
if (_settings.FreeloadOnly && !torrent.IsFreeload)
{
continue;
}
var id = torrent.TorrentId;
var title = GetTitle(result, torrent);
@@ -235,10 +262,11 @@ namespace NzbDrone.Core.Indexers.Definitions
{
Guid = infoUrl,
InfoUrl = infoUrl,
DownloadUrl = GetDownloadUrl(id, torrent.CanUseToken),
DownloadUrl = GetDownloadUrl(id, torrent.CanUseToken && !torrent.IsFreeload),
Title = WebUtility.HtmlDecode(title),
Artist = WebUtility.HtmlDecode(result.Artist),
Album = WebUtility.HtmlDecode(result.GroupName),
Year = int.Parse(result.GroupYear),
Container = torrent.Encoding,
Codec = torrent.Format,
Size = long.Parse(torrent.Size),
@@ -248,8 +276,8 @@ namespace NzbDrone.Core.Indexers.Definitions
Scene = torrent.Scene,
Files = torrent.FileCount,
Grabs = torrent.Snatches,
DownloadVolumeFactor = torrent.IsFreeLeech || torrent.IsNeutralLeech || torrent.IsPersonalFreeLeech ? 0 : 1,
UploadVolumeFactor = torrent.IsNeutralLeech ? 0 : 1
DownloadVolumeFactor = torrent.IsFreeLeech || torrent.IsNeutralLeech || torrent.IsFreeload || torrent.IsPersonalFreeLeech ? 0 : 1,
UploadVolumeFactor = torrent.IsNeutralLeech || torrent.IsFreeload ? 0 : 1
};
var category = torrent.Category;
@@ -269,6 +297,12 @@ namespace NzbDrone.Core.Indexers.Definitions
// Non-Audio files are formatted a little differently (1:1 for group and torrents)
else
{
// skip non-freeload results when freeload only is set
if (_settings.FreeloadOnly && !result.IsFreeload)
{
continue;
}
var id = result.TorrentId;
var infoUrl = GetInfoUrl(result.GroupId, id);
@@ -277,15 +311,15 @@ namespace NzbDrone.Core.Indexers.Definitions
Guid = infoUrl,
Title = WebUtility.HtmlDecode(result.GroupName),
Size = long.Parse(result.Size),
DownloadUrl = GetDownloadUrl(id, result.CanUseToken),
DownloadUrl = GetDownloadUrl(id, result.CanUseToken && !result.IsFreeload),
InfoUrl = infoUrl,
Seeders = int.Parse(result.Seeders),
Peers = int.Parse(result.Leechers) + int.Parse(result.Seeders),
PublishDate = DateTimeOffset.FromUnixTimeSeconds(ParseUtil.CoerceLong(result.GroupTime)).UtcDateTime,
Files = result.FileCount,
Grabs = result.Snatches,
DownloadVolumeFactor = result.IsFreeLeech || result.IsNeutralLeech || result.IsPersonalFreeLeech ? 0 : 1,
UploadVolumeFactor = result.IsNeutralLeech ? 0 : 1
DownloadVolumeFactor = result.IsFreeLeech || result.IsNeutralLeech || result.IsFreeload || result.IsPersonalFreeLeech ? 0 : 1,
UploadVolumeFactor = result.IsNeutralLeech || result.IsFreeload ? 0 : 1
};
var category = result.Category;
@@ -311,7 +345,7 @@ namespace NzbDrone.Core.Indexers.Definitions
private string GetTitle(GazelleRelease result, GazelleTorrent torrent)
{
var title = $"{result.Artist} - {result.GroupName} [{result.GroupYear}]";
var title = $"{result.Artist} - {result.GroupName} ({result.GroupYear})";
if (result.ReleaseType.IsNotNullOrWhiteSpace() && result.ReleaseType != "Unknown")
{
@@ -323,14 +357,23 @@ namespace NzbDrone.Core.Indexers.Definitions
title += $" [{$"{torrent.RemasterTitle} {torrent.RemasterYear}".Trim()}]";
}
title += $" [{torrent.Format} {torrent.Encoding}] [{torrent.Media}]";
var flags = new List<string>
{
$"{torrent.Format} {torrent.Encoding}",
$"{torrent.Media}"
};
if (torrent.HasLog)
{
flags.Add("Log (" + torrent.LogScore + "%)");
}
if (torrent.HasCue)
{
title += " [Cue]";
flags.Add("Cue");
}
return title;
return $"{title} [{string.Join(" / ", flags)}]";
}
private string GetDownloadUrl(int torrentId, bool canUseToken)
@@ -377,12 +420,15 @@ namespace NzbDrone.Core.Indexers.Definitions
UseFreeleechToken = false;
}
[FieldDefinition(2, Label = "API Key", HelpText = "API Key from the Site (Found in Settings => Access Settings)", Privacy = PrivacyLevel.ApiKey)]
[FieldDefinition(2, Label = "API Key", Privacy = PrivacyLevel.ApiKey, HelpText = "API Key from the Site (Found in Settings => Access Settings)")]
public string Apikey { get; set; }
[FieldDefinition(3, Label = "Use Freeleech Tokens", HelpText = "Use freeleech tokens when available", Type = FieldType.Checkbox)]
[FieldDefinition(3, Label = "Use Freeleech Tokens", Type = FieldType.Checkbox, HelpText = "Use freeleech tokens when available")]
public bool UseFreeleechToken { get; set; }
[FieldDefinition(4, Label = "Freeload Only", Type = FieldType.Checkbox, Advanced = true, HelpTextWarning = "Search freeload torrents only. End date: 6 January 2024, 23:59 UTC.")]
public bool FreeloadOnly { get; set; }
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));

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