Compare commits

...

136 Commits

Author SHA1 Message Date
Bogdan
1bb1ec6106 Fixed: (IPTorrents) Include episodes in season search 2024-02-03 00:13:50 +02:00
Bogdan
2bfb838933 Bind shortcut for pending changes confirmation only when it's shown 2024-01-31 19:46:55 +02:00
Bogdan
9eb291f578 Fixed: Avoid trailing slash in Proxy Url sent to FlareSolverr 2024-01-31 15:26:26 +02:00
Servarr
8cf892124c Automated API Docs update 2024-01-28 10:08:24 +02:00
Mark McDowall
47fb886930 Don't clone indexer API Key
(cherry picked from commit d336aaf3f04136471970155b5a7cc876770c64ff)
2024-01-28 09:59:49 +02:00
Mark McDowall
5034a211cb New: Don't return API Keys and Passwords via the API
(cherry picked from commit 570be882154e73f8ad1de5b16b957bcb964697fd)

Don't replace private values that haven't been set

(cherry picked from commit 52760e0908fa9852ed8a770f1916bb582eb8c8b4)
2024-01-28 09:59:49 +02:00
Bogdan
ed1364b6ff Update magnet trackers 2024-01-28 09:08:55 +02:00
Bogdan
71e18b616d Bump version to 1.13.2 2024-01-28 09:07:13 +02:00
abcasada
f7bf21df68 Update help text for Tags usage in Applications 2024-01-26 19:13:12 +02:00
Bogdan
d764e3405d New: (Torznab) Internal and Scene tags 2024-01-26 09:58:22 +02:00
Bogdan
16baceb784 New: Option to disable Email encryption
* New: Option to disable Email encryption

(cherry picked from commit 7be5732a3a6679120b0f01bd1eb1207194f57f5e)

* Fix possible NullRef in Email Encryption migration

(cherry picked from commit 271266b10ac51ee6dd7a7024d346b631bd5397c2)
2024-01-24 11:37:01 +02:00
Bogdan
5d2b80d15a Fixed: (SubsPlease) Parse release size from magnet links 2024-01-23 10:59:59 +02:00
Bogdan
a20a81f424 New: Category filter for Indexers 2024-01-23 09:00:49 +02:00
Bogdan
ebb66e9086 Update database migration version translation token 2024-01-23 08:50:05 +02:00
Bogdan
cb8797693e Fixed: Sorting by name in Manage Applications and Download Client modals 2024-01-23 08:40:32 +02:00
Stevie Robinson
255c6335ae New: Add sorting to Manage Applications and Download Client modals
(cherry picked from commit 91053ca51ded804739f94ee936c1376a755dbe11)
2024-01-23 08:38:47 +02:00
Bogdan
155cd53dcd Fix translations for manage applications and download clients 2024-01-23 08:29:59 +02:00
Weblate
ae70a96c10 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dani Talens <databio@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Julian Baquero <julian-baquero@upc.edu.co>
Co-authored-by: MaddionMax <kovacs.tamas@ius.hu>
Co-authored-by: Magyar <kochnorbert@icloud.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: blabla <romcrack56@gmail.com>
Co-authored-by: brn <barantsenkul@gmail.com>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: horvi28 <horvi28@gmail.com>
Co-authored-by: wilfriedarma <wilfriedarma.collet@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ca/
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/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/tr/
Translation: Servarr/Prowlarr
2024-01-23 08:15:51 +02:00
Bogdan
16c0daf090 Fix value types for EnhancedSelectInputConnector 2024-01-22 13:24:00 +02:00
Bogdan
34c78c5a9d Fixed: (BTN) Disable ID-based searches for episodes with absolute episode number
Release name searches are not supported, so it's better to prevent an useless request.
2024-01-22 11:48:32 +02:00
Bogdan
dd5b108ffd Fixed: (BTN) Allow ID-based with text search query 2024-01-21 17:13:12 +02:00
Servarr
0b83986255 Automated API Docs update 2024-01-21 07:54:16 +02:00
Bogdan
2bd25fb6f3 Reorder sync fields for App Profiles 2024-01-21 07:47:48 +02:00
Bogdan
0f5eb5d3a3 Bump version to 1.13.1 2024-01-21 07:47:48 +02:00
Bogdan
c9434c61e3 Transpile logical assignment operators with babel 2024-01-21 03:58:55 +02:00
Mark McDowall
ee969b7a06 New: Log warning if less than 1 GB free space during update
(cherry picked from commit e66ba84fc0b5b120dd4e87f6b8ae1b3c038ee72b)
2024-01-21 03:58:28 +02:00
Bogdan
cfdf88a1e2 Add exceptions to logs for indexers and apps 2024-01-20 04:49:16 +02:00
bakerboy448
70a5dae293 Fixed: Don't lowercase UrlBase in ConfigFileProvider
UrlBase should honour the case it is given.

(cherry picked from commit e1de523c89f7649e64f520b090bbdb2f56cc4b85)
(cherry picked from commit 9ccefe00951d2959ef79bdaa5731d95f97162d46)

Co-authored-by: Marty Zalega <marty@zalega.me>
2024-01-20 01:34:20 +02:00
Bogdan
b53f8d4552 Bump version to 1.13.0 2024-01-18 05:23:29 +02:00
Weblate
9668e91b21 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dani Talens <databio@gmail.com>
Co-authored-by: Deleted User <noreply+2593@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pl/
Translation: Servarr/Prowlarr
2024-01-17 23:01:59 +02:00
Bogdan
49857693c0 Fix count in GGn fixture after allowing only torrents 2024-01-17 11:14:00 +02:00
Bogdan
850315ad1c Fixed: (GGn) Improve titles and allow only torrents 2024-01-17 10:45:02 +02:00
Mark McDowall
86124d4319 New: Optional directory setting for Aria2
(cherry picked from commit fd17df0dd03a5feb088c3241a247eac20f0e8c6c)
2024-01-17 07:49:51 +02:00
Bogdan
4f28d583d7 Throw download as failed for invalid magnet links 2024-01-17 07:40:17 +02:00
Stevie Robinson
01f3930211 Sort Custom Filters
(cherry picked from commit e4b5d559df2d5f3d55e16aae5922509e84f31e64)
2024-01-17 07:38:23 +02:00
Weblate
7c7114c87a Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dani Talens <databio@gmail.com>
Co-authored-by: Daniele Prevedello <dprevedello86@gmail.com>
Co-authored-by: DimitriDR <dimitridroeck@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Trooper57 <igorkcs@gmail.com>
Co-authored-by: Watashi <drazy24@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: crayon3shawn <crayon3shawn@gmail.com>
Co-authored-by: hansaudun <hans@n5.no>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_TW/
Translation: Servarr/Prowlarr
2024-01-16 05:11:12 +02:00
Bogdan
ef8e6d774b Fix typo 2024-01-16 05:10:20 +02:00
Bogdan
2960fc37d9 Fixed: (FL) Improve error message for service unavailable 2024-01-16 04:58:17 +02:00
Qstick
8bddf753bb Bump Inno version to 6.2.2
(cherry picked from commit c5992ed944b2d4f2f41a08ac407c0f013da61ea6)

Build report can get sent before installer finished

(cherry picked from commit 60d9aacac64b39fd22d43e8b1ca4641bd60a9b48)
2024-01-14 21:44:58 +02:00
Bogdan
cff24b3fd4 Bump version to 1.12.2 2024-01-14 07:13:34 +02:00
Bogdan
031d81330d Fixed: Filter history by multiple event types in PG 2024-01-12 22:11:45 +02:00
Bogdan
6201b42fbd New: Refresh button for indexer stats 2024-01-12 15:26:32 +02:00
Bogdan
7022054dd7 New: Indexer ID as column
Fixes #1948
2024-01-12 14:58:27 +02:00
ilike2burnthing
c9b663247c Empty Torznab ApiKeyWhiteList 2024-01-12 05:55:09 +02:00
Bogdan
0b0a0cfa5b New: (HDBits) Filter by origins 2024-01-10 22:39:43 +02:00
Weblate
3c0fea8b7c Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Aitzol Garmendia <aitzolgarmendia@gmail.com>
Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Bradley BARBIER <bradley.barbier@outlook.fr>
Co-authored-by: Dani Talens <databio@gmail.com>
Co-authored-by: DimitriDR <dimitridroeck@gmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: RicardoVelaC <ricardovelac@gmail.com>
Co-authored-by: reloxx <reloxx@interia.pl>
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/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/id/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/lv/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_TW/
Translation: Servarr/Prowlarr
2024-01-09 20:59:04 +02:00
Bogdan
ac97952fd7 Fixed: (HDBits) Strip non-word chars from search query
Co-authored-by: Kyle Sanderson <kyle.leet@gmail.com>
2024-01-08 22:24:35 +02:00
Bogdan
c3e40c0564 Log web exceptions in test connection 2024-01-08 21:31:08 +02:00
Servarr
ce615a77c2 Automated API Docs update 2024-01-08 01:29:38 +02:00
Bogdan
0f6dfe389c Fix CS in History 2024-01-08 01:21:44 +02:00
Bogdan
25d94a9286 New: History custom filters
Co-authored-by: Mark McDowall <mark@mcdowall.ca>
2024-01-08 00:53:40 +02:00
Bogdan
52a690b41a Fixed: Filter history by multiple event types 2024-01-07 19:04:22 +02:00
Bogdan
56c8c3d6c6 Bump version to 1.12.1 2024-01-07 11:10:04 +02:00
Weblate
5cee8990b3 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Aitzol Garmendia <aitzolgarmendia@gmail.com>
Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Dani Talens <databio@gmail.com>
Co-authored-by: HuaBing <admin@hbcraft.cn>
Co-authored-by: JJonttuu <oikeaihminen@protonmail.com>
Co-authored-by: Juan Lores <juan.lores@gmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Piotr Komborski <piotr+github@kombor.ski>
Co-authored-by: RicardoVelaC <ricardovelac@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: boan51204 <je.991707@gmail.com>
Co-authored-by: 饶志华 <879467666@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ca/
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/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_TW/
Translation: Servarr/Prowlarr
2024-01-05 18:38:00 -06:00
Bogdan
9679e88717 Update end date for RED freeload event 2024-01-04 23:32:37 +02:00
Bogdan
f4203993ba New: (MAM) Book title and author returned in response 2024-01-03 11:29:06 +02:00
Bogdan
4be0715fe3 New: (MAM) Filter releases by Min/Max size 2024-01-03 11:21:40 +02:00
Bogdan
f9c9d4a0e0 Fixed: (GGn) Improve title, timezone, MST, min/max size filters
Hiding torrents of type link
And some minor refactoring around the passkey fetching and parsing.
2024-01-03 10:49:25 +02:00
Weblate
bf47380f7b Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Mario Rodriguez <mario2423@gmail.com>
Co-authored-by: Norbi <kovinor123@gmail.com>
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/pt_BR/
Translation: Servarr/Prowlarr
2024-01-01 14:10:25 +02:00
Mark McDowall
e27a46f578 Fixed: Disable SSL on start if certificate path is not set
(cherry picked from commit 4e19fec123900b8ba1252b640f26f2a4983683ff)
2024-01-01 08:07:46 +02:00
Weblate
af4e69f8fb Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dimitri <dimitridroeck@gmail.com>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Koch Norbert <kochnorbert@icloud.com>
Co-authored-by: Pietro Ribeiro <xxb1exuv6@mozmail.com>
Co-authored-by: SunStorm <me@sunstorm.rocks>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: chiral-lab <jan.eltner@googlemail.com>
Co-authored-by: chrizl <chrizl@gmail.com>
Co-authored-by: resi23 <x-resistant-x@gmx.de>
Co-authored-by: slammingdeath <sebastianbrudny97@gmail.com>
Co-authored-by: ube <ube@alienautopsy.net>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/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/sv/
Translation: Servarr/Prowlarr
2023-12-31 10:48:13 +02:00
Weblate
1b2106d4f0 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dimitri <dimitridroeck@gmail.com>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Koch Norbert <kochnorbert@icloud.com>
Co-authored-by: Pietro Ribeiro <xxb1exuv6@mozmail.com>
Co-authored-by: SunStorm <me@sunstorm.rocks>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: chiral-lab <jan.eltner@googlemail.com>
Co-authored-by: chrizl <chrizl@gmail.com>
Co-authored-by: resi23 <x-resistant-x@gmx.de>
Co-authored-by: slammingdeath <sebastianbrudny97@gmail.com>
Co-authored-by: ube <ube@alienautopsy.net>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/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/sv/
Translation: Servarr/Prowlarr
2023-12-31 10:39:03 +02:00
Weblate
bdfbda3805 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dimitri <dimitridroeck@gmail.com>
Co-authored-by: Koch Norbert <kochnorbert@icloud.com>
Co-authored-by: Pietro Ribeiro <xxb1exuv6@mozmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: chrizl <chrizl@gmail.com>
Co-authored-by: resi23 <x-resistant-x@gmx.de>
Co-authored-by: slammingdeath <sebastianbrudny97@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt/
Translation: Servarr/Prowlarr
2023-12-31 07:26:54 +02:00
Stevie Robinson
cb98b10468 Translate fields on the backend
(cherry picked from commit 48b12f5b00429a7cd218d23f0544641b0da62a06)
2023-12-31 07:16:38 +02:00
Mark McDowall
ae1bc8366c New: Add qBittorrent option for Content Layout
(cherry picked from commit 4b22200708ca120cfdcf9cb796be92183adb95d1)
2023-12-31 06:46:16 +02:00
Bogdan
67eeb4373c Fixed: Ignore empty tags when adding items to Flood
(cherry picked from commit 0a5200766ea80fc1c97bfa497cdfed31b9af687f)
2023-12-31 06:39:20 +02:00
Bogdan
9d40a64be4 Bump version to 1.12.0 2023-12-31 06:36:27 +02:00
Bogdan
20cc146d82 Fixed: Don't die when grabbing releases in bulk 2023-12-30 14:40:04 +02:00
Bogdan
8beff32117 Minor cleanup in AudioBookBay 2023-12-25 14:14:22 +02:00
Bogdan
8b7c488173 New: (Discord) Remove size grab field 2023-12-25 14:14:15 +02:00
Bogdan
1dabbc94f8 Fixed: BinSearch removed 2023-12-24 22:44:34 +02:00
Bogdan
52667b979b Bump version to 1.11.4 2023-12-24 09:13:43 +02:00
Weblate
14f9a75a73 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Aitzol Garmendia <aitzolgarmendia@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translation: Servarr/Prowlarr
2023-12-21 02:34:36 +02:00
Weblate
ed17d91a7b Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: ηg <jonas.konrath@icloud.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translation: Servarr/Prowlarr
2023-12-19 20:02:09 +02:00
Bogdan
f54280b888 Bump version to 1.11.3 2023-12-17 15:53:28 +02:00
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
204 changed files with 3874 additions and 2084 deletions

View File

@@ -9,15 +9,15 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '1.10.4'
majorVersion: '1.13.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'
innoVersion: '6.2.2'
windowsImage: 'windows-2022'
linuxImage: 'ubuntu-20.04'
macImage: 'macOS-11'
@@ -1206,6 +1206,7 @@ stages:
- stage: Report_Out
dependsOn:
- Analyze
- Installer
- Unit_Test
- Integration
- Automation

View File

@@ -254,7 +254,7 @@ InstallInno()
ProgressStart "Installing portable Inno Setup"
rm -rf _inno
curl -s --output innosetup.exe "https://files.jrsoftware.org/is/6/innosetup-${INNOVERSION:-6.2.0}.exe"
curl -s --output innosetup.exe "https://files.jrsoftware.org/is/6/innosetup-${INNOVERSION:-6.2.2}.exe"
mkdir _inno
./innosetup.exe //portable=1 //silent //currentuser //dir=.\\_inno
rm innosetup.exe

View File

@@ -2,6 +2,8 @@ const loose = true;
module.exports = {
plugins: [
'@babel/plugin-transform-logical-assignment-operators',
// Stage 1
'@babel/plugin-proposal-export-default-from',
['@babel/plugin-transform-optional-chaining', { loose }],

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

@@ -1,4 +1,5 @@
import SortDirection from 'Helpers/Props/SortDirection';
import { FilterBuilderProp } from './AppState';
export interface Error {
responseJSON: {
@@ -20,6 +21,10 @@ export interface PagedAppSectionState {
pageSize: number;
}
export interface AppSectionFilterState<T> {
filterBuilderProps: FilterBuilderProp<T>[];
}
export interface AppSectionSchemaState<T> {
isSchemaFetching: boolean;
isSchemaPopulated: boolean;

View File

@@ -1,8 +1,12 @@
import AppSectionState from 'App/State/AppSectionState';
import AppSectionState, {
AppSectionFilterState,
} from 'App/State/AppSectionState';
import Column from 'Components/Table/Column';
import History from 'typings/History';
interface HistoryAppState extends AppSectionState<History> {
interface HistoryAppState
extends AppSectionState<History>,
AppSectionFilterState<History> {
pageSize: number;
columns: Column[];
}

View File

@@ -3,6 +3,7 @@ import AppSectionState, {
AppSectionItemState,
AppSectionSaveState,
} from 'App/State/AppSectionState';
import { IndexerCategory } from 'Indexer/Indexer';
import Application from 'typings/Application';
import DownloadClient from 'typings/DownloadClient';
import Notification from 'typings/Notification';
@@ -25,6 +26,11 @@ export interface DownloadClientAppState
AppSectionDeleteState,
AppSectionSaveState {}
export interface IndexerCategoryAppState
extends AppSectionState<IndexerCategory>,
AppSectionDeleteState,
AppSectionSaveState {}
export interface NotificationAppState
extends AppSectionState<Notification>,
AppSectionDeleteState {}
@@ -35,6 +41,7 @@ interface SettingsAppState {
appProfiles: AppProfileAppState;
applications: ApplicationAppState;
downloadClients: DownloadClientAppState;
indexerCategories: IndexerCategoryAppState;
notifications: NotificationAppState;
ui: UiSettingsAppState;
}

View File

@@ -0,0 +1,41 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import { IndexerCategory } from 'Indexer/Indexer';
import FilterBuilderRowValue from './FilterBuilderRowValue';
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
const indexerCategoriesSelector = createSelector(
(state: AppState) => state.settings.indexerCategories,
(categories) => categories.items
);
function CategoryFilterBuilderRowValue(props: FilterBuilderRowValueProps) {
const categories: IndexerCategory[] = useSelector(indexerCategoriesSelector);
const tagList = categories.reduce(
(acc: { id: number; name: string }[], element) => {
acc.push({
id: element.id,
name: `${element.name} (${element.id})`,
});
if (element.subCategories && element.subCategories.length > 0) {
element.subCategories.forEach((subCat) => {
acc.push({
id: subCat.id,
name: `${subCat.name} (${subCat.id})`,
});
});
}
return acc;
},
[]
);
return <FilterBuilderRowValue {...props} tagList={tagList} />;
}
export default CategoryFilterBuilderRowValue;

View File

@@ -5,8 +5,10 @@ import IconButton from 'Components/Link/IconButton';
import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Props';
import AppProfileFilterBuilderRowValueConnector from './AppProfileFilterBuilderRowValueConnector';
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
import CategoryFilterBuilderRowValue from './CategoryFilterBuilderRowValue';
import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
import HistoryEventTypeFilterBuilderRowValue from './HistoryEventTypeFilterBuilderRowValue';
import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector';
import PrivacyFilterBuilderRowValue from './PrivacyFilterBuilderRowValue';
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
@@ -55,9 +57,15 @@ function getRowValueConnector(selectedFilterBuilderProp) {
case filterBuilderValueTypes.BOOL:
return BoolFilterBuilderRowValue;
case filterBuilderValueTypes.CATEGORY:
return CategoryFilterBuilderRowValue;
case filterBuilderValueTypes.DATE:
return DateFilterBuilderRowValue;
case filterBuilderValueTypes.HISTORY_EVENT_TYPE:
return HistoryEventTypeFilterBuilderRowValue;
case filterBuilderValueTypes.INDEXER:
return IndexerFilterBuilderRowValueConnector;

View File

@@ -0,0 +1,16 @@
import { FilterBuilderProp } from 'App/State/AppState';
interface FilterBuilderRowOnChangeProps {
name: string;
value: unknown[];
}
interface FilterBuilderRowValueProps {
filterType?: string;
filterValue: string | number | object | string[] | number[] | object[];
selectedFilterBuilderProp: FilterBuilderProp<unknown>;
sectionItem: unknown[];
onChange: (payload: FilterBuilderRowOnChangeProps) => void;
}
export default FilterBuilderRowValueProps;

View File

@@ -0,0 +1,39 @@
import React from 'react';
import translate from 'Utilities/String/translate';
import FilterBuilderRowValue from './FilterBuilderRowValue';
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
const EVENT_TYPE_OPTIONS = [
{
id: 1,
get name() {
return translate('Grabbed');
},
},
{
id: 3,
get name() {
return translate('IndexerRss');
},
},
{
id: 2,
get name() {
return translate('IndexerQuery');
},
},
{
id: 4,
get name() {
return translate('IndexerAuth');
},
},
];
function HistoryEventTypeFilterBuilderRowValue(
props: FilterBuilderRowValueProps
) {
return <FilterBuilderRowValue {...props} tagList={EVENT_TYPE_OPTIONS} />;
}
export default HistoryEventTypeFilterBuilderRowValue;

View File

@@ -30,22 +30,24 @@ function CustomFiltersModalContent(props) {
<ModalBody>
{
customFilters.map((customFilter) => {
return (
<CustomFilter
key={customFilter.id}
id={customFilter.id}
label={customFilter.label}
filters={customFilter.filters}
selectedFilterKey={selectedFilterKey}
isDeleting={isDeleting}
deleteError={deleteError}
dispatchSetFilter={dispatchSetFilter}
dispatchDeleteCustomFilter={dispatchDeleteCustomFilter}
onEditPress={onEditCustomFilter}
/>
);
})
customFilters
.sort((a, b) => a.label.localeCompare(b.label))
.map((customFilter) => {
return (
<CustomFilter
key={customFilter.id}
id={customFilter.id}
label={customFilter.label}
filters={customFilter.filters}
selectedFilterKey={selectedFilterKey}
isDeleting={isDeleting}
deleteError={deleteError}
dispatchSetFilter={dispatchSetFilter}
dispatchDeleteCustomFilter={dispatchDeleteCustomFilter}
onEditPress={onEditCustomFilter}
/>
);
})
}
<div className={styles.addButtonContainer}>

View File

@@ -147,7 +147,7 @@ EnhancedSelectInputConnector.propTypes = {
provider: PropTypes.string.isRequired,
providerData: PropTypes.object.isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])).isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.arrayOf(PropTypes.string), PropTypes.arrayOf(PropTypes.number)]).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
selectOptionsProviderAction: PropTypes.string,
onChange: PropTypes.func.isRequired,

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

@@ -2,6 +2,7 @@ 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 {
@@ -12,7 +13,10 @@ class InfoInput extends Component {
const { value } = this.props;
return (
<Alert kind={kinds.INFO}>
<Alert
kind={kinds.INFO}
className={styles.message}
>
<span dangerouslySetInnerHTML={{ __html: value }} />
</Alert>
);

View File

@@ -40,18 +40,26 @@ class FilterMenuContent extends Component {
}
{
customFilters.map((filter) => {
return (
<FilterMenuItem
key={filter.id}
filterKey={filter.id}
selectedFilterKey={selectedFilterKey}
onPress={onFilterSelect}
>
{filter.label}
</FilterMenuItem>
);
})
customFilters.length > 0 ?
<MenuItemSeparator /> :
null
}
{
customFilters
.sort((a, b) => a.label.localeCompare(b.label))
.map((filter) => {
return (
<FilterMenuItem
key={filter.id}
filterKey={filter.id}
selectedFilterKey={selectedFilterKey}
onPress={onFilterSelect}
>
{filter.label}
</FilterMenuItem>
);
})
}
{

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

@@ -2,9 +2,10 @@ export const BOOL = 'bool';
export const BYTES = 'bytes';
export const DATE = 'date';
export const DEFAULT = 'default';
export const HISTORY_EVENT_TYPE = 'historyEventType';
export const INDEXER = 'indexer';
export const PROTOCOL = 'protocol';
export const PRIVACY = 'privacy';
export const APP_PROFILE = 'appProfile';
export const MOVIE_STATUS = 'movieStatus';
export const CATEGORY = 'category';
export const TAG = 'tag';

View File

@@ -15,6 +15,7 @@ import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptions
import TablePager from 'Components/Table/TablePager';
import { align, icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import HistoryFilterModal from './HistoryFilterModal';
import HistoryOptionsConnector from './HistoryOptionsConnector';
import HistoryRowConnector from './HistoryRowConnector';
@@ -63,6 +64,7 @@ class History extends Component {
columns,
selectedFilterKey,
filters,
customFilters,
totalRecords,
onFilterSelect,
onFirstPagePress,
@@ -108,7 +110,8 @@ class History extends Component {
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={[]}
customFilters={customFilters}
filterModalConnectorComponent={HistoryFilterModal}
onFilterSelect={onFilterSelect}
/>
</PageToolbarSection>
@@ -193,8 +196,9 @@ History.propTypes = {
indexersError: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
selectedFilterKey: PropTypes.string.isRequired,
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
totalRecords: PropTypes.number,
onFilterSelect: PropTypes.func.isRequired,
onFirstPagePress: PropTypes.func.isRequired,

View File

@@ -6,6 +6,7 @@ import * as commandNames from 'Commands/commandNames';
import withCurrentPage from 'Components/withCurrentPage';
import { executeCommand } from 'Store/Actions/commandActions';
import * as historyActions from 'Store/Actions/historyActions';
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import History from './History';
@@ -14,13 +15,15 @@ function createMapStateToProps() {
return createSelector(
(state) => state.history,
(state) => state.indexers,
createCustomFiltersSelector('history'),
createCommandExecutingSelector(commandNames.CLEAR_HISTORY),
(history, indexers, isHistoryClearing) => {
(history, indexers, customFilters, isHistoryClearing) => {
return {
isIndexersFetching: indexers.isFetching,
isIndexersPopulated: indexers.isPopulated,
indexersError: indexers.error,
isHistoryClearing,
customFilters,
...history
};
}

View File

@@ -0,0 +1,54 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import FilterModal from 'Components/Filter/FilterModal';
import { setHistoryFilter } from 'Store/Actions/historyActions';
function createHistorySelector() {
return createSelector(
(state: AppState) => state.history.items,
(queueItems) => {
return queueItems;
}
);
}
function createFilterBuilderPropsSelector() {
return createSelector(
(state: AppState) => state.history.filterBuilderProps,
(filterBuilderProps) => {
return filterBuilderProps;
}
);
}
interface HistoryFilterModalProps {
isOpen: boolean;
}
export default function HistoryFilterModal(props: HistoryFilterModalProps) {
const sectionItems = useSelector(createHistorySelector());
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
const customFilterType = 'history';
const dispatch = useDispatch();
const dispatchSetFilter = useCallback(
(payload: unknown) => {
dispatch(setHistoryFilter(payload));
},
[dispatch]
);
return (
<FilterModal
// TODO: Don't spread all the props
{...props}
sectionItems={sectionItems}
filterBuilderProps={filterBuilderProps}
customFilterType={customFilterType}
dispatchSetFilter={dispatchSetFilter}
/>
);
}

View File

@@ -11,6 +11,12 @@
flex: 0 0 60px;
}
.id {
composes: cell;
flex: 0 0 60px;
}
.sortName {
composes: cell;

View File

@@ -8,6 +8,7 @@ interface CssExports {
'cell': string;
'checkInput': string;
'externalLink': string;
'id': string;
'minimumSeeders': string;
'packSeedTime': string;
'priority': string;

View File

@@ -34,7 +34,7 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
const { indexerId, columns, isSelectMode, onCloneIndexerPress } = props;
const { indexer, appProfile, status, longDateFormat, timeFormat } =
useSelector(createIndexerIndexItemSelector(props.indexerId));
useSelector(createIndexerIndexItemSelector(indexerId));
const {
id,
@@ -148,12 +148,24 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
);
}
if (name === 'id') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
<IndexerTitleLink
indexerId={indexerId}
title={`${indexerId}`}
onCloneIndexerPress={onCloneIndexerPress}
/>
</VirtualTableRowCell>
);
}
if (name === 'sortName') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
<IndexerTitleLink
indexerId={indexerId}
indexerName={indexerName}
title={indexerName}
onCloneIndexerPress={onCloneIndexerPress}
/>
</VirtualTableRowCell>

View File

@@ -4,6 +4,12 @@
flex: 0 0 60px;
}
.id {
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
flex: 0 0 60px;
}
.sortName {
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';

View File

@@ -5,6 +5,7 @@ interface CssExports {
'added': string;
'appProfileId': string;
'capabilities': string;
'id': string;
'minimumSeeders': string;
'packSeedTime': string;
'priority': string;

View File

@@ -1,17 +1,16 @@
import PropTypes from 'prop-types';
import React, { useCallback, useState } from 'react';
import Link from 'Components/Link/Link';
import IndexerInfoModal from './Info/IndexerInfoModal';
import styles from './IndexerTitleLink.css';
interface IndexerTitleLinkProps {
indexerName: string;
indexerId: number;
title: string;
onCloneIndexerPress(id: number): void;
}
function IndexerTitleLink(props: IndexerTitleLinkProps) {
const { indexerName, indexerId, onCloneIndexerPress } = props;
const { title, indexerId, onCloneIndexerPress } = props;
const [isIndexerInfoModalOpen, setIsIndexerInfoModalOpen] = useState(false);
@@ -26,7 +25,7 @@ function IndexerTitleLink(props: IndexerTitleLinkProps) {
return (
<div>
<Link className={styles.link} onPress={onIndexerInfoPress}>
{indexerName}
{title}
</Link>
<IndexerInfoModal
@@ -39,8 +38,4 @@ function IndexerTitleLink(props: IndexerTitleLinkProps) {
);
}
IndexerTitleLink.propTypes = {
indexerName: PropTypes.string.isRequired,
};
export default IndexerTitleLink;

View File

@@ -12,8 +12,9 @@ import FilterMenu from 'Components/Menu/FilterMenu';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import { align, kinds } from 'Helpers/Props';
import { align, icons, kinds } from 'Helpers/Props';
import {
fetchIndexerStats,
setIndexerStatsFilter,
@@ -194,6 +195,10 @@ function IndexerStats() {
dispatch(fetchIndexerStats());
}, [dispatch]);
const onRefreshPress = useCallback(() => {
dispatch(fetchIndexerStats());
}, [dispatch]);
const onFilterSelect = useCallback(
(value: string) => {
dispatch(setIndexerStatsFilter({ selectedFilterKey: value }));
@@ -219,8 +224,17 @@ function IndexerStats() {
}, 0) ?? 0;
return (
<PageContent>
<PageContent title={translate('Stats')}>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={translate('Refresh')}
iconName={icons.REFRESH}
isSpinning={isFetching}
onPress={onRefreshPress}
/>
</PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT} collapseButtons={false}>
<FilterMenu
alignMenu={align.RIGHT}

View File

@@ -285,7 +285,7 @@ class SearchIndex extends Component {
const hasNoIndexer = !totalItems;
return (
<PageContent>
<PageContent title={translate('Search')}>
<PageToolbar>
<PageToolbarSection
alignContent={align.RIGHT}

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

@@ -62,7 +62,7 @@ class Applications extends Component {
return (
<FieldSet legend={translate('Applications')}>
<PageSectionContent
errorMessage={translate('UnableToLoadApplicationList')}
errorMessage={translate('ApplicationsLoadError')}
{...otherProps}
>
<div className={styles.applications}>

View File

@@ -14,9 +14,11 @@ import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import useSelectState from 'Helpers/Hooks/useSelectState';
import { kinds } from 'Helpers/Props';
import SortDirection from 'Helpers/Props/SortDirection';
import {
bulkDeleteApplications,
bulkEditApplications,
setManageApplicationsSort,
} from 'Store/Actions/settingsActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import { SelectStateInputProps } from 'typings/props';
@@ -62,6 +64,8 @@ const COLUMNS = [
interface ManageApplicationsModalContentProps {
onModalClose(): void;
sortKey?: string;
sortDirection?: SortDirection;
}
function ManageApplicationsModalContent(
@@ -76,6 +80,8 @@ function ManageApplicationsModalContent(
isSaving,
error,
items,
sortKey,
sortDirection,
}: ApplicationAppState = useSelector(
createClientSideCollectionSelector('settings.applications')
);
@@ -96,6 +102,13 @@ function ManageApplicationsModalContent(
const selectedCount = selectedIds.length;
const onSortPress = useCallback(
(value: string) => {
dispatch(setManageApplicationsSort({ sortKey: value }));
},
[dispatch]
);
const onDeletePress = useCallback(() => {
setIsDeleteModalOpen(true);
}, [setIsDeleteModalOpen]);
@@ -201,6 +214,9 @@ function ManageApplicationsModalContent(
allSelected={allSelected}
allUnselected={allUnselected}
onSelectAllChange={onSelectAllChange}
sortKey={sortKey}
sortDirection={sortDirection}
onSortPress={onSortPress}
>
<TableBody>
{items.map((item) => {

View File

@@ -84,7 +84,7 @@ class DownloadClientSettings extends Component {
/>
<PageToolbarButton
label={translate('ManageDownloadClients')}
label={translate('ManageClients')}
iconName={icons.MANAGE}
onPress={this.onManageDownloadClientsPress}
/>

View File

@@ -61,7 +61,7 @@ class DownloadClients extends Component {
return (
<FieldSet legend={translate('DownloadClients')}>
<PageSectionContent
errorMessage={translate('UnableToLoadDownloadClients')}
errorMessage={translate('DownloadClientsLoadError')}
{...otherProps}
>
<div className={styles.downloadClients}>

View File

@@ -14,9 +14,11 @@ import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import useSelectState from 'Helpers/Hooks/useSelectState';
import { kinds } from 'Helpers/Props';
import SortDirection from 'Helpers/Props/SortDirection';
import {
bulkDeleteDownloadClients,
bulkEditDownloadClients,
setManageDownloadClientsSort,
} from 'Store/Actions/settingsActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import { SelectStateInputProps } from 'typings/props';
@@ -61,6 +63,8 @@ const COLUMNS = [
interface ManageDownloadClientsModalContentProps {
onModalClose(): void;
sortKey?: string;
sortDirection?: SortDirection;
}
function ManageDownloadClientsModalContent(
@@ -75,6 +79,8 @@ function ManageDownloadClientsModalContent(
isSaving,
error,
items,
sortKey,
sortDirection,
}: DownloadClientAppState = useSelector(
createClientSideCollectionSelector('settings.downloadClients')
);
@@ -93,6 +99,13 @@ function ManageDownloadClientsModalContent(
const selectedCount = selectedIds.length;
const onSortPress = useCallback(
(value: string) => {
dispatch(setManageDownloadClientsSort({ sortKey: value }));
},
[dispatch]
);
const onDeletePress = useCallback(() => {
setIsDeleteModalOpen(true);
}, [setIsDeleteModalOpen]);
@@ -174,6 +187,9 @@ function ManageDownloadClientsModalContent(
allSelected={allSelected}
allUnselected={allUnselected}
onSelectAllChange={onSelectAllChange}
sortKey={sortKey}
sortDirection={sortDirection}
onSortPress={onSortPress}
>
<TableBody>
{items.map((item) => {

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

@@ -15,12 +15,17 @@ function PendingChangesModal(props) {
isOpen,
onConfirm,
onCancel,
bindShortcut
bindShortcut,
unbindShortcut
} = props;
useEffect(() => {
bindShortcut('enter', onConfirm);
}, [bindShortcut, onConfirm]);
if (isOpen) {
bindShortcut('enter', onConfirm);
return () => unbindShortcut('enter', onConfirm);
}
}, [bindShortcut, unbindShortcut, isOpen, onConfirm]);
return (
<Modal
@@ -61,7 +66,8 @@ PendingChangesModal.propTypes = {
kind: PropTypes.oneOf(kinds.all),
onConfirm: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
bindShortcut: PropTypes.func.isRequired
bindShortcut: PropTypes.func.isRequired,
unbindShortcut: PropTypes.func.isRequired
};
PendingChangesModal.defaultProps = {

View File

@@ -97,20 +97,6 @@ class EditAppProfileModalContent extends Component {
/>
</FormGroup>
<FormGroup>
<FormLabel>
{translate('EnableInteractiveSearch')}
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enableInteractiveSearch"
{...enableInteractiveSearch}
helpText={translate('EnableInteractiveSearchHelpText')}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>
{translate('EnableAutomaticSearch')}
@@ -125,6 +111,20 @@ class EditAppProfileModalContent extends Component {
/>
</FormGroup>
<FormGroup>
<FormLabel>
{translate('EnableInteractiveSearch')}
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enableInteractiveSearch"
{...enableInteractiveSearch}
helpText={translate('EnableInteractiveSearchHelpText')}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>
{translate('MinimumSeeders')}

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

@@ -6,6 +6,8 @@ import getSectionState from 'Utilities/State/getSectionState';
import { set, updateServerSideCollection } from '../baseActions';
function createFetchServerSideCollectionHandler(section, url, fetchDataAugmenter) {
const [baseSection] = section.split('.');
return function(getState, payload, dispatch) {
dispatch(set({ section, isFetching: true }));
@@ -25,10 +27,13 @@ function createFetchServerSideCollectionHandler(section, url, fetchDataAugmenter
const {
selectedFilterKey,
filters,
customFilters
filters
} = sectionState;
const customFilters = getState().customFilters.items.filter((customFilter) => {
return customFilter.type === section || customFilter.type === baseSection;
});
const selectedFilters = findSelectedFilters(selectedFilterKey, filters, customFilters);
selectedFilters.forEach((filter) => {
@@ -37,7 +42,8 @@ function createFetchServerSideCollectionHandler(section, url, fetchDataAugmenter
const promise = createAjaxRequest({
url,
data
data,
traditional: true
}).request;
promise.done((response) => {

View File

@@ -1,4 +1,5 @@
import { createAction } from 'redux-actions';
import { sortDirections } from 'Helpers/Props';
import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler';
import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler';
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
@@ -7,6 +8,7 @@ import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHand
import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler';
import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler';
import createSetClientSideCollectionSortReducer from 'Store/Actions/Creators/Reducers/createSetClientSideCollectionSortReducer';
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import { createThunk } from 'Store/thunks';
@@ -30,9 +32,10 @@ export const CANCEL_SAVE_APPLICATION = 'settings/applications/cancelSaveApplicat
export const DELETE_APPLICATION = 'settings/applications/deleteApplication';
export const TEST_APPLICATION = 'settings/applications/testApplication';
export const CANCEL_TEST_APPLICATION = 'settings/applications/cancelTestApplication';
export const TEST_ALL_APPLICATIONS = 'indexers/testAllApplications';
export const TEST_ALL_APPLICATIONS = 'settings/applications/testAllApplications';
export const BULK_EDIT_APPLICATIONS = 'settings/applications/bulkEditApplications';
export const BULK_DELETE_APPLICATIONS = 'settings/applications/bulkDeleteApplications';
export const SET_MANAGE_APPLICATIONS_SORT = 'settings/applications/setManageApplicationsSort';
//
// Action Creators
@@ -49,6 +52,7 @@ export const cancelTestApplication = createThunk(CANCEL_TEST_APPLICATION);
export const testAllApplications = createThunk(TEST_ALL_APPLICATIONS);
export const bulkEditApplications = createThunk(BULK_EDIT_APPLICATIONS);
export const bulkDeleteApplications = createThunk(BULK_DELETE_APPLICATIONS);
export const setManageApplicationsSort = createAction(SET_MANAGE_APPLICATIONS_SORT);
export const setApplicationValue = createAction(SET_APPLICATION_VALUE, (payload) => {
return {
@@ -88,7 +92,14 @@ export default {
isTesting: false,
isTestingAll: false,
items: [],
pendingChanges: {}
pendingChanges: {},
sortKey: 'name',
sortDirection: sortDirections.ASCENDING,
sortPredicates: {
name: function(item) {
return item.name.toLowerCase();
}
}
},
//
@@ -121,7 +132,10 @@ export default {
return selectedSchema;
});
}
},
[SET_MANAGE_APPLICATIONS_SORT]: createSetClientSideCollectionSortReducer(section)
}
};

View File

@@ -1,4 +1,5 @@
import { createAction } from 'redux-actions';
import { sortDirections } from 'Helpers/Props';
import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler';
import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler';
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
@@ -7,6 +8,7 @@ import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHand
import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler';
import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler';
import createSetClientSideCollectionSortReducer from 'Store/Actions/Creators/Reducers/createSetClientSideCollectionSortReducer';
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import { createThunk } from 'Store/thunks';
@@ -34,6 +36,7 @@ export const CANCEL_TEST_DOWNLOAD_CLIENT = 'settings/downloadClients/cancelTestD
export const TEST_ALL_DOWNLOAD_CLIENTS = 'settings/downloadClients/testAllDownloadClients';
export const BULK_EDIT_DOWNLOAD_CLIENTS = 'settings/downloadClients/bulkEditDownloadClients';
export const BULK_DELETE_DOWNLOAD_CLIENTS = 'settings/downloadClients/bulkDeleteDownloadClients';
export const SET_MANAGE_DOWNLOAD_CLIENTS_SORT = 'settings/downloadClients/setManageDownloadClientsSort';
//
// Action Creators
@@ -50,6 +53,7 @@ export const cancelTestDownloadClient = createThunk(CANCEL_TEST_DOWNLOAD_CLIENT)
export const testAllDownloadClients = createThunk(TEST_ALL_DOWNLOAD_CLIENTS);
export const bulkEditDownloadClients = createThunk(BULK_EDIT_DOWNLOAD_CLIENTS);
export const bulkDeleteDownloadClients = createThunk(BULK_DELETE_DOWNLOAD_CLIENTS);
export const setManageDownloadClientsSort = createAction(SET_MANAGE_DOWNLOAD_CLIENTS_SORT);
export const setDownloadClientValue = createAction(SET_DOWNLOAD_CLIENT_VALUE, (payload) => {
return {
@@ -89,7 +93,14 @@ export default {
isTesting: false,
isTestingAll: false,
items: [],
pendingChanges: {}
pendingChanges: {},
sortKey: 'name',
sortDirection: sortDirections.ASCENDING,
sortPredicates: {
name: function(item) {
return item.name.toLowerCase();
}
}
},
//
@@ -147,7 +158,10 @@ export default {
return selectedSchema;
});
}
},
[SET_MANAGE_DOWNLOAD_CLIENTS_SORT]: createSetClientSideCollectionSortReducer(section)
}
};

View File

@@ -1,5 +1,5 @@
import { createAction } from 'redux-actions';
import { filterTypes, sortDirections } from 'Helpers/Props';
import { filterBuilderTypes, filterBuilderValueTypes, filterTypes, sortDirections } from 'Helpers/Props';
import { createThunk, handleThunks } from 'Store/thunks';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers';
@@ -159,6 +159,27 @@ export const defaultState = {
}
]
}
],
filterBuilderProps: [
{
name: 'eventType',
label: () => translate('EventType'),
type: filterBuilderTypes.EQUAL,
valueType: filterBuilderValueTypes.HISTORY_EVENT_TYPE
},
{
name: 'indexerIds',
label: () => translate('Indexer'),
type: filterBuilderTypes.EQUAL,
valueType: filterBuilderValueTypes.INDEXER
},
{
name: 'successful',
label: () => translate('Successful'),
type: filterBuilderTypes.EQUAL,
valueType: filterBuilderValueTypes.BOOL
}
]
};

View File

@@ -1,6 +1,6 @@
import _ from 'lodash';
import { createAction } from 'redux-actions';
import { sortDirections } from 'Helpers/Props';
import { filterTypePredicates, sortDirections } from 'Helpers/Props';
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler';
import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
@@ -69,6 +69,28 @@ export const filterPredicates = {
item.fields.find((field) => field.name === 'vipExpiration')?.value ?? null;
return dateFilterPredicate(vipExpiration, filterValue, type);
},
categories: function(item, filterValue, type) {
const predicate = filterTypePredicates[type];
const { categories = [] } = item.capabilities || {};
const categoryList = categories
.filter((category) => category.id < 100000)
.reduce((acc, element) => {
acc.push(element.id);
if (element.subCategories && element.subCategories.length > 0) {
element.subCategories.forEach((subCat) => {
acc.push(subCat.id);
});
}
return acc;
}, []);
return predicate(categoryList, filterValue);
}
};
@@ -203,7 +225,13 @@ export const reducers = createHandleActions({
delete selectedSchema.name;
selectedSchema.fields = selectedSchema.fields.map((field) => {
return { ...field };
const newField = { ...field };
if (newField.privacy === 'apiKey' || newField.privacy === 'password') {
newField.value = '';
}
return newField;
});
newState.selectedSchema = selectedSchema;

View File

@@ -37,12 +37,18 @@ export const defaultState = {
isVisible: true,
isModifiable: false
},
{
name: 'id',
columnLabel: () => translate('IndexerId'),
label: () => translate('Id'),
isSortable: true,
isVisible: false
},
{
name: 'sortName',
label: () => translate('IndexerName'),
isSortable: true,
isVisible: true,
isModifiable: false
isVisible: true
},
{
name: 'protocol',
@@ -180,6 +186,12 @@ export const defaultState = {
type: filterBuilderTypes.EXACT,
valueType: filterBuilderValueTypes.APP_PROFILE
},
{
name: 'categories',
label: () => translate('Categories'),
type: filterBuilderTypes.ARRAY,
valueType: filterBuilderValueTypes.CATEGORY
},
{
name: 'tags',
label: () => translate('Tags'),

View File

@@ -74,8 +74,9 @@ export const defaultState = {
valueType: filterBuilderValueTypes.TAG
}
],
selectedFilterKey: 'all',
customFilters: []
selectedFilterKey: 'all'
};
export const persistState = [

View File

@@ -369,8 +369,9 @@ export const actionHandlers = handleThunks({
promise.done((data) => {
dispatch(batchActions([
...data.map((release) => {
...data.map(({ guid }) => {
return updateRelease({
guid,
isGrabbing: false,
isGrabbed: true,
grabError: null

View File

@@ -22,9 +22,9 @@ class About extends Component {
isNetCore,
isDocker,
runtimeVersion,
migrationVersion,
databaseVersion,
databaseType,
migrationVersion,
appData,
startupPath,
mode,
@@ -66,13 +66,13 @@ class About extends Component {
}
<DescriptionListItem
title={translate('DBMigration')}
data={migrationVersion}
title={translate('Database')}
data={`${titleCase(databaseType)} ${databaseVersion}`}
/>
<DescriptionListItem
title={translate('Database')}
data={`${titleCase(databaseType)} ${databaseVersion}`}
title={translate('DatabaseMigration')}
data={migrationVersion}
/>
<DescriptionListItem

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",

View File

@@ -92,6 +92,10 @@ namespace NzbDrone.Common.Http
{
data = new XElement("base64", Convert.ToBase64String(bytes));
}
else if (value is Dictionary<string, string> d)
{
data = new XElement("struct", d.Select(p => new XElement("member", new XElement("name", p.Key), new XElement("value", p.Value))));
}
else
{
throw new InvalidOperationException($"Unhandled argument type {value.GetType().Name}");

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,151 @@
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Datastore.Migration;
using NzbDrone.Core.Notifications.Email;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Datastore.Migration
{
[TestFixture]
public class email_encryptionFixture : MigrationTest<email_encryption>
{
[Test]
public void should_convert_do_not_require_encryption_to_auto()
{
var db = WithMigrationTestDb(c =>
{
c.Insert.IntoTable("Notifications").Row(new
{
OnGrab = true,
OnHealthIssue = true,
IncludeHealthWarnings = true,
Name = "Mail Prowlarr",
Implementation = "Email",
Tags = "[]",
Settings = new EmailSettings38
{
Server = "smtp.gmail.com",
Port = 563,
To = new List<string> { "dont@email.me" },
RequireEncryption = false
}.ToJson(),
ConfigContract = "EmailSettings"
});
});
var items = db.Query<NotificationDefinition39>("SELECT * FROM \"Notifications\"");
items.Should().HaveCount(1);
items.First().Implementation.Should().Be("Email");
items.First().ConfigContract.Should().Be("EmailSettings");
items.First().Settings.UseEncryption.Should().Be((int)EmailEncryptionType.Preferred);
}
[Test]
public void should_convert_require_encryption_to_always()
{
var db = WithMigrationTestDb(c =>
{
c.Insert.IntoTable("Notifications").Row(new
{
OnGrab = true,
OnHealthIssue = true,
IncludeHealthWarnings = true,
Name = "Mail Prowlarr",
Implementation = "Email",
Tags = "[]",
Settings = new EmailSettings38
{
Server = "smtp.gmail.com",
Port = 563,
To = new List<string> { "dont@email.me" },
RequireEncryption = true
}.ToJson(),
ConfigContract = "EmailSettings"
});
});
var items = db.Query<NotificationDefinition39>("SELECT * FROM \"Notifications\"");
items.Should().HaveCount(1);
items.First().Implementation.Should().Be("Email");
items.First().ConfigContract.Should().Be("EmailSettings");
items.First().Settings.UseEncryption.Should().Be((int)EmailEncryptionType.Always);
}
[Test]
public void should_use_defaults_when_settings_are_empty()
{
var db = WithMigrationTestDb(c =>
{
c.Insert.IntoTable("Notifications").Row(new
{
OnGrab = true,
OnHealthIssue = true,
IncludeHealthWarnings = true,
Name = "Mail Prowlarr",
Implementation = "Email",
Tags = "[]",
Settings = new { }.ToJson(),
ConfigContract = "EmailSettings"
});
});
var items = db.Query<NotificationDefinition39>("SELECT * FROM \"Notifications\"");
items.Should().HaveCount(1);
items.First().Implementation.Should().Be("Email");
items.First().ConfigContract.Should().Be("EmailSettings");
items.First().Settings.UseEncryption.Should().Be((int)EmailEncryptionType.Preferred);
}
}
public class NotificationDefinition39
{
public int Id { get; set; }
public string Implementation { get; set; }
public string ConfigContract { get; set; }
public EmailSettings39 Settings { get; set; }
public string Name { get; set; }
public bool OnGrab { get; set; }
public bool OnHealthIssue { get; set; }
public bool OnHealthRestored { get; set; }
public bool OnApplicationUpdate { get; set; }
public bool SupportsOnGrab { get; set; }
public bool IncludeManualGrabs { get; set; }
public bool SupportsOnHealthIssue { get; set; }
public bool SupportsOnHealthRestored { get; set; }
public bool IncludeHealthWarnings { get; set; }
public bool SupportsOnApplicationUpdate { get; set; }
public List<int> Tags { get; set; }
}
public class EmailSettings38
{
public string Server { get; set; }
public int Port { get; set; }
public bool RequireEncryption { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public string From { get; set; }
public IEnumerable<string> To { get; set; }
public IEnumerable<string> Cc { get; set; }
public IEnumerable<string> Bcc { get; set; }
}
public class EmailSettings39
{
public string Server { get; set; }
public int Port { get; set; }
public int UseEncryption { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public string From { get; set; }
public IEnumerable<string> To { get; set; }
public IEnumerable<string> Cc { get; set; }
public IEnumerable<string> Bcc { get; set; }
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http;
@@ -21,10 +22,10 @@ namespace NzbDrone.Core.Test.IndexerTests.GazelleGamesTests
[SetUp]
public void Setup()
{
Subject.Definition = new IndexerDefinition()
Subject.Definition = new IndexerDefinition
{
Name = "GazelleGames",
Settings = new GazelleGamesSettings() { Apikey = "somekey" }
Settings = new GazelleGamesSettings { Apikey = "somekey" }
};
}
@@ -37,20 +38,20 @@ namespace NzbDrone.Core.Test.IndexerTests.GazelleGamesTests
.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 int[] { 2000 } })).Releases;
var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new[] { 2000 } })).Releases;
releases.Should().HaveCount(1464);
releases.Should().HaveCount(1462);
releases.First().Should().BeOfType<TorrentInfo>();
var torrentInfo = releases.First() as TorrentInfo;
torrentInfo.Title.Should().Be("Microsoft_Flight_Simulator-HOODLUM");
torrentInfo.Title.Should().Be("Microsoft_Flight_Simulator-HOODLUM (2020) [Windows / Multi-Language / Full ISO]");
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
torrentInfo.DownloadUrl.Should().Be("https://gazellegames.net/torrents.php?action=download&id=303216&authkey=prowlarr&torrent_pass=");
torrentInfo.InfoUrl.Should().Be("https://gazellegames.net/torrents.php?id=84781&torrentid=303216");
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-07-25 6:39:11").ToUniversalTime());
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-07-25 06:39:11", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal));
torrentInfo.Size.Should().Be(80077617780);
torrentInfo.InfoHash.Should().Be(null);
torrentInfo.MagnetUrl.Should().Be(null);
@@ -74,7 +75,7 @@ namespace NzbDrone.Core.Test.IndexerTests.GazelleGamesTests
.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 int[] { 2000 } })).Releases;
var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new[] { 2000 } })).Releases;
releases.Should().HaveCount(0);
}

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

@@ -44,7 +44,7 @@ namespace NzbDrone.Core.Test.IndexerTests.OrpheusTests
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

@@ -0,0 +1,111 @@
using System;
using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Notifications.Email;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.NotificationTests.EmailTests
{
[TestFixture]
public class EmailSettingsValidatorFixture : CoreTest<EmailSettingsValidator>
{
private EmailSettings _emailSettings;
private TestValidator<EmailSettings> _validator;
[SetUp]
public void Setup()
{
_validator = new TestValidator<EmailSettings>
{
v => v.RuleFor(s => s).SetValidator(Subject)
};
_emailSettings = Builder<EmailSettings>.CreateNew()
.With(s => s.Server = "someserver")
.With(s => s.Port = 567)
.With(s => s.UseEncryption = (int)EmailEncryptionType.Always)
.With(s => s.From = "dont@email.me")
.With(s => s.To = new string[] { "dont@email.me" })
.Build();
}
[Test]
public void should_be_valid_if_all_settings_valid()
{
_validator.Validate(_emailSettings).IsValid.Should().BeTrue();
}
[Test]
public void should_not_be_valid_if_port_is_out_of_range()
{
_emailSettings.Port = 900000;
_validator.Validate(_emailSettings).IsValid.Should().BeFalse();
}
[Test]
public void should_not_be_valid_if_server_is_empty()
{
_emailSettings.Server = "";
_validator.Validate(_emailSettings).IsValid.Should().BeFalse();
}
[Test]
public void should_not_be_valid_if_from_is_empty()
{
_emailSettings.From = "";
_validator.Validate(_emailSettings).IsValid.Should().BeFalse();
}
[TestCase("prowlarr")]
[TestCase("email.me")]
[Ignore("Allowed coz some email servers allow arbitrary source, we probably need to support 'Name <email>' syntax")]
public void should_not_be_valid_if_from_is_invalid(string email)
{
_emailSettings.From = email;
_validator.Validate(_emailSettings).IsValid.Should().BeFalse();
}
[TestCase("prowlarr")]
[TestCase("email.me")]
public void should_not_be_valid_if_to_is_invalid(string email)
{
_emailSettings.To = new string[] { email };
_validator.Validate(_emailSettings).IsValid.Should().BeFalse();
}
[TestCase("prowlarr")]
[TestCase("email.me")]
public void should_not_be_valid_if_cc_is_invalid(string email)
{
_emailSettings.Cc = new string[] { email };
_validator.Validate(_emailSettings).IsValid.Should().BeFalse();
}
[TestCase("prowlarr")]
[TestCase("email.me")]
public void should_not_be_valid_if_bcc_is_invalid(string email)
{
_emailSettings.Bcc = new string[] { email };
_validator.Validate(_emailSettings).IsValid.Should().BeFalse();
}
[Test]
public void should_not_be_valid_if_to_bcc_cc_are_all_empty()
{
_emailSettings.To = Array.Empty<string>();
_emailSettings.Cc = Array.Empty<string>();
_emailSettings.Bcc = Array.Empty<string>();
_validator.Validate(_emailSettings).IsValid.Should().BeFalse();
}
}
}

View File

@@ -41,6 +41,23 @@ namespace NzbDrone.Core.Annotations
public string Hint { get; set; }
}
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public class FieldTokenAttribute : Attribute
{
public FieldTokenAttribute(TokenField field, string label = "", string token = "", object value = null)
{
Label = label;
Field = field;
Token = token;
Value = value?.ToString();
}
public string Label { get; set; }
public TokenField Field { get; set; }
public string Token { get; set; }
public string Value { get; set; }
}
public class FieldSelectOption
{
public int Value { get; set; }
@@ -82,4 +99,11 @@ namespace NzbDrone.Core.Annotations
ApiKey,
UserName
}
public enum TokenField
{
Label,
HelpText,
HelpTextWarning
}
}

View File

@@ -252,11 +252,11 @@ namespace NzbDrone.Core.Applications
if (webException.Message.Contains("502") || webException.Message.Contains("503") ||
webException.Message.Contains("timed out"))
{
_logger.Warn("{0} server is currently unavailable. {1}", this, webException.Message);
_logger.Warn(webException, "{0} server is currently unavailable. {1}", this, webException.Message);
}
else
{
_logger.Warn("{0} {1}", this, webException.Message);
_logger.Warn(webException, "{0} {1}", this, webException.Message);
}
}
catch (TooManyRequestsException ex)
@@ -264,12 +264,12 @@ namespace NzbDrone.Core.Applications
var minimumBackOff = ex.RetryAfter != TimeSpan.Zero ? ex.RetryAfter : TimeSpan.FromHours(1);
_applicationStatusService.RecordFailure(application.Definition.Id, minimumBackOff);
_logger.Warn("API Request Limit reached for {0}", this);
_logger.Warn(ex, "API Request Limit reached for {0}", this);
}
catch (HttpException ex)
{
_applicationStatusService.RecordFailure(application.Definition.Id);
_logger.Warn("{0} {1}", this, ex.Message);
_logger.Warn(ex, "{0} {1}", this, ex.Message);
}
catch (Exception ex)
{
@@ -301,11 +301,11 @@ namespace NzbDrone.Core.Applications
if (webException.Message.Contains("502") || webException.Message.Contains("503") ||
webException.Message.Contains("timed out"))
{
_logger.Warn("{0} server is currently unavailable. {1}", this, webException.Message);
_logger.Warn(webException, "{0} server is currently unavailable. {1}", this, webException.Message);
}
else
{
_logger.Warn("{0} {1}", this, webException.Message);
_logger.Warn(webException, "{0} {1}", this, webException.Message);
}
}
catch (TooManyRequestsException ex)
@@ -313,12 +313,12 @@ namespace NzbDrone.Core.Applications
var minimumBackOff = ex.RetryAfter != TimeSpan.Zero ? ex.RetryAfter : TimeSpan.FromHours(1);
_applicationStatusService.RecordFailure(application.Definition.Id, minimumBackOff);
_logger.Warn("API Request Limit reached for {0}", this);
_logger.Warn(ex, "API Request Limit reached for {0}", this);
}
catch (HttpException ex)
{
_applicationStatusService.RecordFailure(application.Definition.Id);
_logger.Warn("{0} {1}", this, ex.Message);
_logger.Warn(ex, "{0} {1}", this, ex.Message);
}
catch (Exception ex)
{

View File

@@ -227,7 +227,7 @@ namespace NzbDrone.Core.Configuration
return urlBase;
}
return "/" + urlBase.Trim('/').ToLower();
return "/" + urlBase;
}
}
@@ -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 or cert path is empty disable SSL
if (EnableSsl && (GetValue("SslCertHash", null).IsNotNullOrWhiteSpace() || SslCertPath.IsNullOrWhiteSpace()))
{
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,7 +39,7 @@ namespace NzbDrone.Core.Datastore
return connectionBuilder.DataSource;
}
private static string GetConnectionString(string dbPath)
private static DatabaseConnectionInfo GetConnectionString(string dbPath)
{
var connectionBuilder = new SQLiteConnectionStringBuilder
{
@@ -57,21 +57,22 @@ namespace NzbDrone.Core.Datastore
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

@@ -0,0 +1,50 @@
using System.Collections.Generic;
using System.Data;
using Dapper;
using FluentMigrator;
using Newtonsoft.Json.Linq;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(039)]
public class email_encryption : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Execute.WithConnection(ChangeEncryption);
}
private void ChangeEncryption(IDbConnection conn, IDbTransaction tran)
{
var updated = new List<object>();
using (var getEmailCmd = conn.CreateCommand())
{
getEmailCmd.Transaction = tran;
getEmailCmd.CommandText = "SELECT \"Id\", \"Settings\" FROM \"Notifications\" WHERE \"Implementation\" = 'Email'";
using (var reader = getEmailCmd.ExecuteReader())
{
while (reader.Read())
{
var id = reader.GetInt32(0);
var settings = Json.Deserialize<JObject>(reader.GetString(1));
settings["useEncryption"] = settings.Value<bool?>("requireEncryption") ?? false ? 1 : 0;
settings["requireEncryption"] = null;
updated.Add(new
{
Settings = settings.ToJson(),
Id = id
});
}
}
}
var updateSql = "UPDATE \"Notifications\" SET \"Settings\" = @Settings WHERE \"Id\" = @Id";
conn.Execute(updateSql, updated, transaction: tran);
}
}
}

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

@@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using System.Xml.XPath;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Download.Extensions;
@@ -95,8 +96,14 @@ namespace NzbDrone.Core.Download.Clients.Aria2
public string AddUri(Aria2Settings settings, string magnet)
{
var response = ExecuteRequest(settings, "aria2.addUri", GetToken(settings), new List<string> { magnet });
var options = new Dictionary<string, string>();
if (settings.Directory.IsNotNullOrWhiteSpace())
{
options.Add("dir", settings.Directory);
}
var response = ExecuteRequest(settings, "aria2.addUri", GetToken(settings), new List<string> { magnet }, options);
var gid = response.GetStringResponse();
return gid;
@@ -104,8 +111,16 @@ namespace NzbDrone.Core.Download.Clients.Aria2
public string AddTorrent(Aria2Settings settings, byte[] torrent)
{
var response = ExecuteRequest(settings, "aria2.addTorrent", GetToken(settings), torrent);
// Aria2's second parameter is an array of URIs and needs to be sent if options are provided, this satisfies that requirement.
var emptyListOfUris = new List<string>();
var options = new Dictionary<string, string>();
if (settings.Directory.IsNotNullOrWhiteSpace())
{
options.Add("dir", settings.Directory);
}
var response = ExecuteRequest(settings, "aria2.addTorrent", GetToken(settings), torrent, emptyListOfUris, options);
var gid = response.GetStringResponse();
return gid;

View File

@@ -41,6 +41,9 @@ namespace NzbDrone.Core.Download.Clients.Aria2
[FieldDefinition(4, Label = "Secret token", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
public string SecretToken { get; set; }
[FieldDefinition(5, Label = "Directory", Type = FieldType.Textbox, HelpText = "DownloadClientAriaSettingsDirectoryHelpText")]
public string Directory { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));

View File

@@ -4,6 +4,7 @@ using System.Linq;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download.Clients.Flood.Models;
using NzbDrone.Core.Indexers;
@@ -56,7 +57,7 @@ namespace NzbDrone.Core.Download.Clients.Flood
}
}
return result;
return result.Where(t => t.IsNotNullOrWhiteSpace());
}
public override string Name => "Flood";

View File

@@ -0,0 +1,9 @@
namespace NzbDrone.Core.Download.Clients.QBittorrent
{
public enum QBittorrentContentLayout
{
Default = 0,
Original = 1,
Subfolder = 2
}
}

View File

@@ -265,6 +265,15 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
{
request.AddFormParameter("firstLastPiecePrio", true);
}
if ((QBittorrentContentLayout)settings.ContentLayout == QBittorrentContentLayout.Original)
{
request.AddFormParameter("contentLayout", "Original");
}
else if ((QBittorrentContentLayout)settings.ContentLayout == QBittorrentContentLayout.Subfolder)
{
request.AddFormParameter("contentLayout", "Subfolder");
}
}
public void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings)

View File

@@ -62,6 +62,9 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
[FieldDefinition(10, Label = "First and Last First", Type = FieldType.Checkbox, HelpText = "Download first and last pieces first (qBittorrent 4.1.0+)")]
public bool FirstAndLast { get; set; }
[FieldDefinition(13, Label = "DownloadClientQbittorrentSettingsContentLayout", Type = FieldType.Select, SelectOptions = typeof(QBittorrentContentLayout), HelpText = "DownloadClientQbittorrentSettingsContentLayoutHelpText")]
public int ContentLayout { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));

View File

@@ -171,9 +171,7 @@ namespace NzbDrone.Core.Download
}
catch (FormatException ex)
{
_logger.Error(ex, "Failed to parse magnetlink for release '{0}': '{1}'", release.Title, magnetUrl);
return null;
throw new ReleaseDownloadException("Failed to parse magnetlink for release '{0}': '{1}'", ex, release.Title, magnetUrl);
}
if (hash != null)

View File

@@ -115,7 +115,7 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
MaxTimeout = maxTimeout,
Proxy = new FlareSolverrProxy
{
Url = proxyUrl?.AbsoluteUri
Url = proxyUrl?.OriginalString
}
};
}
@@ -141,7 +141,7 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
MaxTimeout = maxTimeout,
Proxy = new FlareSolverrProxy
{
Url = proxyUrl?.AbsoluteUri
Url = proxyUrl?.OriginalString
}
};
}
@@ -206,17 +206,13 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
private Uri GetProxyUri(HttpProxySettings proxySettings)
{
switch (proxySettings.Type)
return proxySettings.Type switch
{
case ProxyType.Http:
return new Uri("http://" + proxySettings.Host + ":" + proxySettings.Port);
case ProxyType.Socks4:
return new Uri("socks4://" + proxySettings.Host + ":" + proxySettings.Port);
case ProxyType.Socks5:
return new Uri("socks5://" + proxySettings.Host + ":" + proxySettings.Port);
default:
return null;
}
ProxyType.Http => new Uri("http://" + proxySettings.Host + ":" + proxySettings.Port),
ProxyType.Socks4 => new Uri("socks4://" + proxySettings.Host + ":" + proxySettings.Port),
ProxyType.Socks5 => new Uri("socks5://" + proxySettings.Host + ":" + proxySettings.Port),
_ => null
};
}
private class FlareSolverrRequest

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

@@ -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

@@ -26,8 +26,7 @@ public class AudioBookBay : TorrentIndexerBase<NoAuthTorrentBaseSettings>
public override string Name => "AudioBook Bay";
public override string[] IndexerUrls => new[]
{
"https://audiobookbay.is/",
"https://audiobookbay.se/"
"https://audiobookbay.is/"
};
public override string[] LegacyUrls => new[]
{
@@ -55,12 +54,14 @@ public class AudioBookBay : TorrentIndexerBase<NoAuthTorrentBaseSettings>
"https://audiobookbay.unblockit.pet/",
"https://audiobookbay.unblockit.ink/",
"https://audiobookbay.unblockit.bio/", // error 502
"https://audiobookbay.li/"
"https://audiobookbay.li/",
"https://audiobookbay.se/" // redirects to .is but has invalid CA
};
public override string Description => "AudioBook Bay (ABB) is a public Torrent Tracker for AUDIOBOOKS";
public override string Language => "en-US";
public override IndexerPrivacy Privacy => IndexerPrivacy.Public;
public override int PageSize => 15;
public override TimeSpan RateLimit => TimeSpan.FromSeconds(5);
public override IndexerCapabilities Capabilities => SetCapabilities();
public AudioBookBay(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
@@ -70,7 +71,7 @@ public class AudioBookBay : TorrentIndexerBase<NoAuthTorrentBaseSettings>
public override IIndexerRequestGenerator GetRequestGenerator()
{
return new AudioBookBayRequestGenerator(Settings, Capabilities);
return new AudioBookBayRequestGenerator(Settings);
}
public override IParseIndexerResponse GetParser()
@@ -119,64 +120,7 @@ public class AudioBookBay : TorrentIndexerBase<NoAuthTorrentBaseSettings>
}
};
// Age
caps.Categories.AddCategoryMapping("children", NewznabStandardCategory.AudioAudiobook, "Children");
caps.Categories.AddCategoryMapping("teen-young-adult", NewznabStandardCategory.AudioAudiobook, "Teen & Young Adult");
caps.Categories.AddCategoryMapping("adults", NewznabStandardCategory.AudioAudiobook, "Adults");
// Category
caps.Categories.AddCategoryMapping("postapocalyptic", NewznabStandardCategory.AudioAudiobook, "(Post)apocalyptic");
caps.Categories.AddCategoryMapping("action", NewznabStandardCategory.AudioAudiobook, "Action");
caps.Categories.AddCategoryMapping("adventure", NewznabStandardCategory.AudioAudiobook, "Adventure");
caps.Categories.AddCategoryMapping("art", NewznabStandardCategory.AudioAudiobook, "Art");
caps.Categories.AddCategoryMapping("autobiography-biographies", NewznabStandardCategory.AudioAudiobook, "Autobiography & Biographies");
caps.Categories.AddCategoryMapping("business", NewznabStandardCategory.AudioAudiobook, "Business");
caps.Categories.AddCategoryMapping("computer", NewznabStandardCategory.AudioAudiobook, "Computer");
caps.Categories.AddCategoryMapping("contemporary", NewznabStandardCategory.AudioAudiobook, "Contemporary");
caps.Categories.AddCategoryMapping("crime", NewznabStandardCategory.AudioAudiobook, "Crime");
caps.Categories.AddCategoryMapping("detective", NewznabStandardCategory.AudioAudiobook, "Detective");
caps.Categories.AddCategoryMapping("doctor-who-sci-fi", NewznabStandardCategory.AudioAudiobook, "Doctor Who");
caps.Categories.AddCategoryMapping("education", NewznabStandardCategory.AudioAudiobook, "Education");
caps.Categories.AddCategoryMapping("fantasy", NewznabStandardCategory.AudioAudiobook, "Fantasy");
caps.Categories.AddCategoryMapping("general-fiction", NewznabStandardCategory.AudioAudiobook, "General Fiction");
caps.Categories.AddCategoryMapping("historical-fiction", NewznabStandardCategory.AudioAudiobook, "Historical Fiction");
caps.Categories.AddCategoryMapping("history", NewznabStandardCategory.AudioAudiobook, "History");
caps.Categories.AddCategoryMapping("horror", NewznabStandardCategory.AudioAudiobook, "Horror");
caps.Categories.AddCategoryMapping("humor", NewznabStandardCategory.AudioAudiobook, "Humor");
caps.Categories.AddCategoryMapping("lecture", NewznabStandardCategory.AudioAudiobook, "Lecture");
caps.Categories.AddCategoryMapping("lgbt", NewznabStandardCategory.AudioAudiobook, "LGBT");
caps.Categories.AddCategoryMapping("literature", NewznabStandardCategory.AudioAudiobook, "Literature");
caps.Categories.AddCategoryMapping("litrpg", NewznabStandardCategory.AudioAudiobook, "LitRPG");
caps.Categories.AddCategoryMapping("general-non-fiction", NewznabStandardCategory.AudioAudiobook, "Misc. Non-fiction");
caps.Categories.AddCategoryMapping("mystery", NewznabStandardCategory.AudioAudiobook, "Mystery");
caps.Categories.AddCategoryMapping("paranormal", NewznabStandardCategory.AudioAudiobook, "Paranormal");
caps.Categories.AddCategoryMapping("plays-theater", NewznabStandardCategory.AudioAudiobook, "Plays & Theater");
caps.Categories.AddCategoryMapping("poetry", NewznabStandardCategory.AudioAudiobook, "Poetry");
caps.Categories.AddCategoryMapping("political", NewznabStandardCategory.AudioAudiobook, "Political");
caps.Categories.AddCategoryMapping("radio-productions", NewznabStandardCategory.AudioAudiobook, "Radio Productions");
caps.Categories.AddCategoryMapping("romance", NewznabStandardCategory.AudioAudiobook, "Romance");
caps.Categories.AddCategoryMapping("sci-fi", NewznabStandardCategory.AudioAudiobook, "Sci-Fi");
caps.Categories.AddCategoryMapping("science", NewznabStandardCategory.AudioAudiobook, "Science");
caps.Categories.AddCategoryMapping("self-help", NewznabStandardCategory.AudioAudiobook, "Self-help");
caps.Categories.AddCategoryMapping("spiritual", NewznabStandardCategory.AudioAudiobook, "Spiritual & Religious");
caps.Categories.AddCategoryMapping("sports", NewznabStandardCategory.AudioAudiobook, "Sport & Recreation");
caps.Categories.AddCategoryMapping("suspense", NewznabStandardCategory.AudioAudiobook, "Suspense");
caps.Categories.AddCategoryMapping("thriller", NewznabStandardCategory.AudioAudiobook, "Thriller");
caps.Categories.AddCategoryMapping("true-crime", NewznabStandardCategory.AudioAudiobook, "True Crime");
caps.Categories.AddCategoryMapping("tutorial", NewznabStandardCategory.AudioAudiobook, "Tutorial");
caps.Categories.AddCategoryMapping("westerns", NewznabStandardCategory.AudioAudiobook, "Westerns");
caps.Categories.AddCategoryMapping("zombies", NewznabStandardCategory.AudioAudiobook, "Zombies");
// Category Modifiers
caps.Categories.AddCategoryMapping("anthology", NewznabStandardCategory.AudioAudiobook, "Anthology");
caps.Categories.AddCategoryMapping("bestsellers", NewznabStandardCategory.AudioAudiobook, "Bestsellers");
caps.Categories.AddCategoryMapping("classic", NewznabStandardCategory.AudioAudiobook, "Classic");
caps.Categories.AddCategoryMapping("documentary", NewznabStandardCategory.AudioAudiobook, "Documentary");
caps.Categories.AddCategoryMapping("full-cast", NewznabStandardCategory.AudioAudiobook, "Full Cast");
caps.Categories.AddCategoryMapping("libertarian", NewznabStandardCategory.AudioAudiobook, "Libertarian");
caps.Categories.AddCategoryMapping("military", NewznabStandardCategory.AudioAudiobook, "Military");
caps.Categories.AddCategoryMapping("novel", NewznabStandardCategory.AudioAudiobook, "Novel");
caps.Categories.AddCategoryMapping("short-story", NewznabStandardCategory.AudioAudiobook, "Short Story");
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.AudioAudiobook);
return caps;
}
@@ -185,12 +129,10 @@ public class AudioBookBay : TorrentIndexerBase<NoAuthTorrentBaseSettings>
public class AudioBookBayRequestGenerator : IIndexerRequestGenerator
{
private readonly NoAuthTorrentBaseSettings _settings;
private readonly IndexerCapabilities _capabilities;
public AudioBookBayRequestGenerator(NoAuthTorrentBaseSettings settings, IndexerCapabilities capabilities)
public AudioBookBayRequestGenerator(NoAuthTorrentBaseSettings settings)
{
_settings = settings;
_capabilities = capabilities;
}
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
@@ -246,8 +188,6 @@ public class AudioBookBayRequestGenerator : IIndexerRequestGenerator
}
yield return new IndexerRequest(new UriBuilder(searchUrl) { Path = "/" }.Uri.AbsoluteUri, HttpAccept.Html);
yield return new IndexerRequest(new UriBuilder(searchUrl) { Path = "/page/2/" }.Uri.AbsoluteUri, HttpAccept.Html);
yield return new IndexerRequest(new UriBuilder(searchUrl) { Path = "/page/3/" }.Uri.AbsoluteUri, HttpAccept.Html);
}
public Func<IDictionary<string, string>> GetCookies { get; set; }
@@ -300,8 +240,7 @@ public class AudioBookBayParser : IParseIndexerResponse
var postInfo = row.QuerySelector("div.postInfo")?.FirstChild?.TextContent.Trim().Replace("\xA0", ";") ?? string.Empty;
var matchCategory = Regex.Match(postInfo, @"Category: (.+)$", RegexOptions.IgnoreCase);
var category = matchCategory.Groups[1].Success ? matchCategory.Groups[1].Value.Split(';', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).ToList() : new List<string>();
var categories = category.SelectMany(_categories.MapTrackerCatDescToNewznab).Distinct().ToList();
var genres = matchCategory.Groups[1].Success ? matchCategory.Groups[1].Value.Split(';', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).ToList() : new List<string>();
var release = new TorrentInfo
{
@@ -309,13 +248,14 @@ public class AudioBookBayParser : IParseIndexerResponse
InfoUrl = infoUrl,
DownloadUrl = infoUrl,
Title = CleanTitle(title),
Categories = categories,
Categories = new List<IndexerCategory> { NewznabStandardCategory.AudioAudiobook },
Size = size,
Seeders = 1,
Peers = 1,
PublishDate = publishDate,
DownloadVolumeFactor = 0,
UploadVolumeFactor = 1
UploadVolumeFactor = 1,
Genres = genres
};
var cover = row.QuerySelector("img[src]")?.GetAttribute("src")?.Trim();

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(5);
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

@@ -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

View File

@@ -19,6 +19,7 @@ using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Definitions
{
[Obsolete("Site has shutdown")]
public class BinSearch : UsenetIndexerBase<BinSearchSettings>
{
public override string Name => "BinSearch";

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();
@@ -50,7 +32,7 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
var parameters = new BroadcastheNetTorrentQuery();
var searchString = searchCriteria.SearchTerm ?? string.Empty;
var searchTerm = searchCriteria.SearchTerm ?? string.Empty;
var btnResults = searchCriteria.Limit.GetValueOrDefault();
if (btnResults == 0)
@@ -68,9 +50,10 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
{
parameters.Tvrage = $"{searchCriteria.RId}";
}
else if (searchString.IsNotNullOrWhiteSpace())
if (searchTerm.IsNotNullOrWhiteSpace())
{
parameters.Search = searchString.Replace(" ", "%");
parameters.Search = searchTerm.Replace(" ", "%");
}
// If only the season/episode is searched for then change format to match expected format
@@ -102,6 +85,11 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
parameters.Category = "Episode";
pageableRequests.Add(GetPagedRequests(parameters, btnResults, btnOffset));
}
else if (searchTerm.IsNotNullOrWhiteSpace() && int.TryParse(searchTerm, out _) && (searchCriteria.TvdbId > 0 || searchCriteria.RId > 0))
{
// Disable ID-based searches for episodes with absolute episode number
return new IndexerPageableRequestChain();
}
else
{
// Neither a season only search nor daily nor standard, fall back to query
@@ -122,7 +110,7 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
var parameters = new BroadcastheNetTorrentQuery();
var searchString = searchCriteria.SearchTerm ?? "";
var searchTerm = searchCriteria.SearchTerm ?? string.Empty;
var btnResults = searchCriteria.Limit.GetValueOrDefault();
if (btnResults == 0)
@@ -132,11 +120,24 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
var btnOffset = searchCriteria.Offset.GetValueOrDefault(0);
parameters.Search = searchString.Replace(" ", "%");
if (searchTerm.IsNotNullOrWhiteSpace())
{
parameters.Search = searchTerm.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

@@ -1,7 +1,10 @@
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;
@@ -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

@@ -38,3 +38,8 @@ public class FileListTorrent
[JsonPropertyName("small_description")]
public string SmallDescription { get; set; }
}
public class FileListErrorResponse
{
public string Error { get; set; }
}

View File

@@ -28,9 +28,14 @@ public class FileListParser : IParseIndexerResponse
throw new IndexerException(indexerResponse, "Unexpected response status {0} code from indexer request", indexerResponse.HttpResponse.StatusCode);
}
if (indexerResponse.Content.StartsWith("{\"error\"") && STJson.TryDeserialize<FileListErrorResponse>(indexerResponse.Content, out var errorResponse))
{
throw new IndexerException(indexerResponse, "Unexpected response from indexer request: {0}", errorResponse.Error);
}
if (!indexerResponse.HttpResponse.Headers.ContentType.Contains(HttpAccept.Json.Value))
{
throw new IndexerException(indexerResponse, $"Unexpected response header {indexerResponse.HttpResponse.Headers.ContentType} from indexer request, expected {HttpAccept.Json.Value}");
throw new IndexerException(indexerResponse, "Unexpected response header {0} from indexer request, expected {1}", indexerResponse.HttpResponse.Headers.ContentType, HttpAccept.Json.Value);
}
var releaseInfos = new List<ReleaseInfo>();

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