Compare commits

...

214 Commits

Author SHA1 Message Date
Weblate 5375cbe1c2 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Crocmou <slaanesh8854@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: diaverso <alexito_perez.95@hotmail.com>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translation: Servarr/Readarr
2024-01-27 17:59:45 +02:00
Mark McDowall d0b797ea61 Fixed: History retention for Newsbin
(cherry picked from commit 0ea189d03c8c5e02c00b96a4281dd9e668d6a9ae)
2024-01-27 10:26:00 +02:00
Servarr e76f160695 Automated API Docs update 2024-01-25 08:16:50 +02:00
Weblate ef71fc1b41 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Alexander <a.burdun@gmail.com>
Co-authored-by: Dani Talens <databio@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Magyar <kochnorbert@icloud.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: horvi28 <horvi28@gmail.com>
Co-authored-by: reloxx <reloxx@interia.pl>
Co-authored-by: wilfriedarma <wilfriedarma.collet@gmail.com>
Co-authored-by: zichichi <sollami@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/uk/
Translation: Servarr/Readarr
2024-01-25 08:13:18 +02:00
Mark McDowall 14f14e5da4 New: Optionally remove from queue by changing category to 'Post-Import Category' when configured
(cherry picked from commit 345854d0fe9b65a561fdab12aac688782a420aa5)

Closes #3260
2024-01-25 08:10:18 +02:00
Mark McDowall bd265e47fa Fixed: Don't try to remove the same item from queue multiple times
(cherry picked from commit 2491da067815e129df3a3a79c0cc7221a9d87094)

Closes #2087
2024-01-25 08:01:16 +02:00
Mark McDowall 333d344c0b New: Add FileId to History data for import events
(cherry picked from commit 952a7248c962908fc5da92762507421923a06e17)

Closes #788
2024-01-25 07:49:21 +02:00
Mark McDowall db6712f030 New: Add size to more history events
(cherry picked from commit 0d064181941fc6d149fc2f891661e059758d5428)

Closes #3250
2024-01-25 07:46:52 +02:00
Bogdan 1065a6283c Update database migration version translation token
(cherry picked from commit 7d0d503a5e132cda3c03d6f7cd7b51c9c80740de)

Closes #3257
2024-01-25 07:42:27 +02:00
Stevie Robinson 1b40c5c7ce Add Regular Expression Custom Format translation
(cherry picked from commit 9f50166fa62a71d0a23e2c2d331651792285dc0e)

Closes #3256
2024-01-25 07:38:34 +02:00
Mark McDowall a8de87300e New: Add download client name to pending items waiting for a specific client
(cherry picked from commit 3cd4c67ba12cd5e8cc00d3df8929555fc0ad5918)

Closes #3254
2024-01-25 07:32:46 +02:00
Qstick f260078ac8 Fixed: Allow restore to process backups up to ~1000MB 2024-01-25 07:19:51 +02:00
Mark McDowall 5a6486be21 Don't use TestCase for single test
(cherry picked from commit 541d3307e1466b0353dc4149f502a4b62b4de616)
2024-01-24 20:36:47 -06:00
Bogdan 2e9de3cb86 Fixed: Sorting by name in Manage Indexer and Download Client modals
(cherry picked from commit 31baed4b2c2406e48b8defa51352a13adb6d470f)
2024-01-23 07:35:49 +02:00
bakerboy448 a259684916 Improve Release Title Custom Format debugging
(cherry picked from commit ec40bc6eea1eb282cb804b8dd5461bf5ade332e9)

Closes #3235
2024-01-21 08:17:34 +02:00
Qstick 5704adfbc5 New: Improve All Authors call by using dictionary for stats iteration
(cherry picked from commit e792db4d3355fedd3ea9e35b3f5e1e30394d9ee3)

Closes #3230
2024-01-21 08:13:22 +02:00
Bogdan 6cfaab07ba Wrap values in log messages in FileListParser
Closes #3229
2024-01-21 08:08:21 +02:00
Stevie Robinson b36085a3cc New: Drop commands table content before postgres migration
Signed-off-by: Stevie Robinson <stevie.robinson@gmail.com>
(cherry picked from commit 8dd3b45c90209136c0bd0a861061c6d20837d62f)

Closes #3225
2024-01-21 08:06:54 +02:00
Bogdan 0afa0977b0 Bump version to 0.3.17 2024-01-21 08:01:27 +02:00
Bogdan 4a174e559f Transpile logical assignment operators with babel
(cherry picked from commit 3cf4d2907e32e81050f35cda042dcc2b4641d40d)
2024-01-21 03:55:02 +02:00
servarr[bot] 0fb8ab2280 New: Log warning if less than 1 GB free space during update
* New: Log warning if less than 1 GB free space during update

(cherry picked from commit e66ba84fc0b5b120dd4e87f6b8ae1b3c038ee72b)

---------

Co-authored-by: Mark McDowall <mark@mcdowall.ca>
2024-01-21 03:54:39 +02:00
Mark McDowall 261b0f398b Fixed: Don't clone indexer API Key
(cherry picked from commit d336aaf3f04136471970155b5a7cc876770c64ff)
2024-01-20 07:43:57 +02:00
Weblate d1fea384a7 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Julian Baquero <julian-baquero@upc.edu.co>
Co-authored-by: Koch Norbert <kochnorbert@icloud.com>
Co-authored-by: MaddionMax <kovacs.tamas@ius.hu>
Co-authored-by: brn <barantsenkul@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/tr/
Translation: Servarr/Readarr
2024-01-20 02:06:38 +02:00
Stevie Robinson 9542ea0d2e Round off the seeded ratio when checking for removal candidates
Signed-off-by: Stevie Robinson <stevie.robinson@gmail.com>

(cherry picked from commit c6bb6ad8788fb1c20ed466a495f2b47034947145)
2024-01-19 08:15:19 +02:00
Weblate e1d697c561 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dani Talens <databio@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ca/
Translation: Servarr/Readarr
2024-01-18 00:03:31 +02:00
Bogdan 22ed847849 Replace support-requests with label-actions 2024-01-16 23:55:43 +02:00
Stevie Robinson 2faef704b4 Fixed: Replacing 'appName' translation token
(cherry picked from commit 2e51b8792db0d3ec402672dc92c95f3cb886ef44)

Closes #3058
Fixes #3221
2024-01-16 23:50:14 +02:00
Bogdan a566c3e21f Check Content-Type in FileList parser 2024-01-16 21:52:40 +02:00
Stevie Robinson cc0d2a84ae Sort Custom Filters
(cherry picked from commit e4b5d559df2d5f3d55e16aae5922509e84f31e64)
2024-01-16 08:08:39 +02:00
Qstick 1c3d2ce4e5 Improved http timeout handling
(cherry picked from commit f87a66fcba6ca9ca972fa1c747a940b216e0e5e3)
2024-01-16 08:08:26 +02:00
Weblate 57f614f4cd Multiple Translations updated by Weblate
ignore-downstream

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: crayon3shawn <crayon3shawn@gmail.com>
Co-authored-by: hansaudun <hans@n5.no>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_TW/
Translation: Servarr/Readarr
2024-01-15 20:01:07 +02:00
Bogdan 9d2efe0944 Fixed: Cutoff unmet showing Unmonitored books 2024-01-15 16:43:12 +02:00
Bogdan e032be48e0 Fixed: Wanted Missing showing Unmonitored books 2024-01-15 16:41:03 +02:00
Bogdan cd66de1992 Bump version to 0.3.16 2024-01-14 07:12:55 +02:00
Bogdan 3066dd92d7 Ignore tests temporarily 2024-01-13 03:36:01 +02:00
Servarr 467a87baec Automated API Docs update 2024-01-13 02:04:59 +02:00
Bogdan 80fb077c94 Fix log typo in release/push 2024-01-13 01:28:08 +02:00
Bogdan 07433d69ca New: Resolve download client by name using 'downloadClient' for pushed releases
Closes #3053
2024-01-13 01:27:36 +02:00
Mark McDowall 3b3ebe463c Fixed: Pushed releases not being properly rejected
(cherry picked from commit 07f816ffb18ac34090c2f8ba25147737299b361d)

Closes #2943
2024-01-13 01:26:15 +02:00
Mark McDowall 03392ca635 New: Optional 'downloadClientId' for pushed releases
(cherry picked from commit fa5bfc3742c24c5730b77bf8178a423d98fdf50e)

Closes #2934
2024-01-13 01:23:11 +02:00
Bogdan d23ce9ecc2 Allow to override download client
Towards #2331
2024-01-13 01:18:11 +02:00
Bogdan e968fcaff6 Fixed: Filter history by multiple event types in PG 2024-01-12 22:10:48 +02:00
Gavin Mogan 31da559f89 Fixed: Database type when PG host contains ".db" (#3186)
Previously was looking for a ".db" in the connection string, which is
the typical sqlite filename. The problem is if your connection string has a
.db anywhere in it, such as postgres.db.internal it'll think its a
sqlite file

Solution borrowed from sonarr:

https://github.com/Sonarr/Sonarr/blob/develop/src/NzbDrone.Core/Datastore/Database.cs#L43
2024-01-12 13:29:06 +02:00
Servarr a093290792 Automated API Docs update 2024-01-12 04:02:47 +02:00
Mark McDowall 9e3dfc510d Paging params in API docs
(cherry picked from commit bfaa7291e14a8d3847ef2154a52c363944560803)

Closes #2975
Closes #2991
2024-01-12 03:52:11 +02:00
Bogdan 9d27c172ac Fixed: Improve torrent blocklist matching
Closes #3184
2024-01-12 03:28:57 +02:00
Bogdan 518dbe53eb Fixed: Release source for release/push
Closes #3182
2024-01-12 03:27:39 +02:00
ilike2burnthing f9ba00c9e7 Remove unsupported pagination for Nyaa
(cherry picked from commit fef525ddb8b5f91bb36b3c9e652663fccb098a00)

Closes #3180
2024-01-12 03:25:39 +02:00
ilike2burnthing 4aec7a0ea7 Remove dead Torznab API key whitelist
(cherry picked from commit 3454f1c9ed11fbb9aa66e16524a529e924e5a77e)

Closes #3179
2024-01-12 03:23:37 +02:00
Weblate fc4cf8e81e Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Watashi <drazy24@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fr/
Translation: Servarr/Readarr
2024-01-12 03:21:34 +02:00
Weblate 143de3b220 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Aleksandr <alyarmak@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: Havok Dan <havokdan@yahoo.com.br>
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: reloxx <reloxx@interia.pl>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_TW/
Translation: Servarr/Readarr
2024-01-10 21:05:13 +02:00
Servarr e1a07d01b2 Automated API Docs update 2024-01-10 19:13:31 +02:00
Bogdan 27e498bb14 Fixed: Add ForeignEditionId to books endpoint 2024-01-10 19:07:29 +02:00
Bogdan b9f1882a57 Fixed: Refresh book files after renaming 2024-01-09 19:27:16 +02:00
ta264 2392573c39 New: Freeleech tokens support for Gazelle 2024-01-09 16:14:27 +02:00
Mark McDowall 2351efd013 Fixed: Blocklisting torrents from indexers that do not provide torrent hash
(cherry picked from commit 3541cd7ba877fb785c7f97123745abf51162eb8e)

Closes #3082
2024-01-09 16:07:26 +02:00
Weblate 526429bde4 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dani Talens <databio@gmail.com>
Co-authored-by: DimitriDR <dimitridroeck@gmail.com>
Co-authored-by: JJonttuu <oikeaihminen@protonmail.com>
Co-authored-by: RicardoVelaC <ricardovelac@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fr/
Translation: Servarr/Readarr
2024-01-09 15:48:13 +02:00
Servarr abd44b59bc Automated API Docs update 2024-01-09 15:46:24 +02:00
Bogdan 9942457ffc Rename to books 2024-01-09 03:32:05 +02:00
Mark McDowall 073342ef39 New: Download client option for redownloading failed releases from Interactive Search
(cherry picked from commit 87e0a7983a437a4d166aa8b9c9eaf78ea5431969)

Closes #2987
2024-01-09 03:14:48 +02:00
Bogdan b455708f2e Add release source for releases
Towards #2130
2024-01-09 03:10:52 +02:00
Bogdan 622b02c478 Use last history item in FailedDownloadService 2024-01-09 03:05:52 +02:00
Bogdan 8effba383d Bump version to 0.3.15 2024-01-07 11:11:32 +02:00
Bogdan 2749479283 Fix possible enumerations in TrackGroupingService 2024-01-06 19:41:17 +02:00
Bogdan 4cbafa76d8 New: Custom formats in book history
(cherry picked from commit cd2ce34f10007efacd8631d3ce3ac4f4a6212966)

Closes #2134
Closes #3163
2024-01-06 19:41:17 +02:00
Bogdan 73782cc233 Remove duplicated source title in history 2024-01-06 19:41:17 +02:00
Weblate de396fe9be Multiple Translations updated by Weblate
ignore-downstream

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/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translation: Servarr/Readarr
2024-01-01 14:11:59 +02:00
Mark McDowall 71cb9e1dd7 Fixed: Disable SSL on start if certificate path is not set
(cherry picked from commit 4e19fec123900b8ba1252b640f26f2a4983683ff)
2024-01-01 06:58:14 +02:00
Mark McDowall ee5ed57fcc New: Add qBittorrent option for Content Layout
(cherry picked from commit 4b22200708ca120cfdcf9cb796be92183adb95d1)

Closes #3140
2023-12-31 11:12:25 +02:00
Stevie Robinson d20a049a5a Translate fields on the backend
(cherry picked from commit 48b12f5b00429a7cd218d23f0544641b0da62a06)
2023-12-31 11:10:45 +02:00
Stevie Robinson a9f77ace37 Fixed: Fallback to English translations if invalid UI language in config
(cherry picked from commit 4c7201741276eccaea2fb1f33daecc31e8b2d54e)

Closes #2882
2023-12-31 11:07:58 +02:00
Mark McDowall 0341a2ec26 Initial support to use named tokens for backend translations
Towards #3003

(cherry picked from commit 11f96c31048c2d1aafca0c91736d439f7f9a95a8)
2023-12-31 11:03:44 +02:00
Stevie Robinson d6796bbe1a New: Show Proper or Repack tag in interactive search
(cherry picked from commit efb000529b5dff42829df3ef151e4750a7b15cf6)

Closes #3141
2023-12-31 10:57:25 +02:00
Bogdan 9066f8558c New: Retry on failed downloads of torrent and nzb files
(cherry picked from commit bc20ef73bdd47b7cdad43d4c7d4b4bd534e49252)

Closes #3151
2023-12-31 10:52:37 +02:00
Weblate c4e37528ee Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dimitri <dimitridroeck@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Koch Norbert <kochnorbert@icloud.com>
Co-authored-by: Nicola <nicola.neri@gmail.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/readarr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/sv/
Translation: Servarr/Readarr
2023-12-31 06:25:13 +02:00
Bogdan 5937c952af Fixed: Ignore empty tags when adding items to Flood
(cherry picked from commit 0a5200766ea80fc1c97bfa497cdfed31b9af687f)
2023-12-31 06:24:43 +02:00
Stevie Robinson 0f4bd3c472 New: Add sorting to Manage Indexer and Download Client modals
(cherry picked from commit 91053ca51ded804739f94ee936c1376a755dbe11)
2023-12-31 06:24:21 +02:00
Qstick cf415e61de New: Bulk Delete for Unmapped Files
(cherry picked from commit 71c1edd47c5377bcdeeb68e9cededf122a6ce6b4)
2023-12-27 03:17:41 +02:00
Bogdan 9865e92cea Add error message for invalid Root Folder in Ebook Tag Service 2023-12-25 01:53:21 +02:00
Bogdan 1cf956a9d9 Don't use empty file path from Calibre 2023-12-25 01:53:21 +02:00
Bogdan 8989c55c8c Bump version to 0.3.14 2023-12-24 09:12:40 +02:00
Bogdan dc83e0127e Fixed: Minor UI improvements to author and book details 2023-12-24 09:05:46 +02:00
Bogdan 34eb312426 Fixed: File Count on Books page 2023-12-24 07:22:10 +02:00
Bogdan 9d5cdebdb2 Fixed: Displaying Import List Exclusion actions on mobile 2023-12-24 05:31:34 +02:00
Bogdan a0ab224acd Log payload for set fields request in Calibre 2023-12-24 02:08:31 +02:00
Weblate 05aa35a54d 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: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Menno Liefstingh <mennoliefstingh@gmail.com>
Co-authored-by: Pietro Ribeiro <xxb1exuv6@mozmail.com>
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: VisoTC <szlytlyt@outlook.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/lv/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translation: Servarr/Readarr
2023-12-23 03:22:45 +02:00
Mark McDowall ca7f8775f5 Fixed: Increase width and truncate long names on Import List Exclusions
(cherry picked from commit 2d0541c03b761a0ec5e10711d6bd577e07141517)
2023-12-23 03:20:23 +02:00
Mark McDowall 2a01e9b445 Fixed: Don't grab propers/repacks when item in queue meets cutoff and propers/repacks are not downloaded automatically
(cherry picked from commit cf00fecbe410caf1a57d561e458f2e58921eef05)

Closes #2210
2023-12-23 03:11:19 +02:00
Mark McDowall 7d30c7d1ea Fixed: Parsing similar author names with common words at end
(cherry picked from commit 0fe24539625f8397dfb63d4618611db99c3c137a)
2023-12-23 03:01:34 +02:00
Mark McDowall 50be87e5a4 Fixed: Error checking if files should be deleted after import won't leave import in limbo
(cherry picked from commit 88ad6f9544110a2e825ebe6b2cde17e9f05475cc)

Closes #824
2023-12-23 03:00:03 +02:00
Bakerboy448 0572d1ac80 QualityParser - Simplify new expression (IDE0090)
(cherry picked from commit 9ae647d9d23bcd53ef34ba6eeffd0cf17248404d)

Closes #2320
2023-12-23 02:56:30 +02:00
Stevie Robinson d2240514d7 Fixed: Reduce font size for title on author and book details
(cherry picked from commit 03f5174a4b2a005aab8d1a1540f4bcb272682f2e)

Closes #3009
2023-12-22 03:32:28 +02:00
Weblate ad47dc032d 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: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Menno Liefstingh <mennoliefstingh@gmail.com>
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: VisoTC <szlytlyt@outlook.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/lv/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translation: Servarr/Readarr
2023-12-22 03:19:00 +02:00
Bogdan 6c6df7d7d9 Fixed: Abort old fetch items requests in manual import 2023-12-22 02:40:41 +02:00
Bogdan 2121204064 New: Remember sorting for manual import 2023-12-22 02:39:31 +02:00
Mark McDowall 61004ea33f New: Natural Sorting Manual Import Paths
(cherry picked from commit bdd5865876796bc203c8117418a5389afc8b5f11)

Closes #1641
2023-12-22 02:38:14 +02:00
ta264 54c1c7862e New: Sort interactive import by path by default
(cherry picked from commit 4e41c3b237de596496523fcd3671d8d3c4192c27)
2023-12-22 02:37:10 +02:00
Mark McDowall 43dfdc8bf5 Fix tests for ImportListSyncService
(cherry picked from commit b271b3b694081a2889d75d39dc0296d53c734aaf)

Closes #2843
2023-12-19 00:27:21 +02:00
Mark McDowall 0d1ae0ca4e New: Less logging when no import lists are enabled
(cherry picked from commit 7be4840f028f24e3920bd395a4e15eb5e643e46f)

Closes #2836
2023-12-18 23:53:34 +02:00
Taloth Saldono 9902889a30 Fixed metadata images containing html content
(cherry picked from commit 87a64cdacbb0945c89b878d02a7eb2ac28427026)
2023-12-18 22:16:41 +02:00
Bogdan 04d7061030 Bump version to 0.3.13 2023-12-17 15:55:59 +02:00
Bogdan fd201912a9 Fix help text for Import Extra Files
Co-authored-by: zakary <zak@ary.dev>
2023-12-16 16:39:40 +02:00
Mark McDowall c412701a3d Fixed: Imported books updating on Calendar
(cherry picked from commit 5a3bc49392b700650a34536ff3794bce614f64a4)

Closes #3126
2023-12-15 19:55:00 +02:00
Agneev Mukherjee 7451a66365 Enable browser navigation buttons for PWA
(cherry picked from commit da9a60691f363323565a293ed9eaeb6349ceccb6)

Closes #3122
2023-12-15 19:52:06 +02:00
Bogdan a6431fdb0b OZnzb removed
Closes #3123
2023-12-15 19:51:56 +02:00
Qstick 060b133f6d Fixed: Correctly handle Migration when PG Host has ".db"
(cherry picked from commit 97ee24507f4306e3b62c3d00cd3ade6a09d1b957)

Closes #3116
2023-12-12 15:47:30 +02:00
Bogdan 5ed13b942b Implement DatabaseConnectionInfo
Co-authored-by: Qstick <qstick@gmail.com>
2023-12-12 15:46:13 +02:00
Bogdan 89f3d8167b Prevent NullRef on header assert 2023-12-10 16:31:30 +02:00
Bogdan 77b027374f Increase the wait timeout for integration tests init 2023-12-10 16:30:33 +02:00
Bogdan 650490abb2 Bump dotnet to 6.0.25 2023-12-10 15:43:22 +02:00
Bogdan 7d2e215d61 Bump version to 0.3.12 2023-12-10 13:45:43 +02:00
Bogdan 65ff890c74 Ignore tests temporarily 2023-12-09 14:12:18 +02:00
Mark McDowall 50c0b0dbaa Always validate Custom Script path
(cherry picked from commit c922cc5dc617dd776d4523cbf62376821c5a4ad9)
2023-12-09 13:12:49 +02:00
Weblate d5f36d0144 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Hajiroxx <luypanda@163.com>
Co-authored-by: Jurriaan Den Toonder <jur.den.toonder@gmail.com>
Co-authored-by: RicardoVelaC <ricardovelac@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translation: Servarr/Readarr
2023-12-09 13:11:48 +02:00
Bogdan fab7558bd4 Fixed: Don't write audio tags if there are no updates
(cherry picked from commit 1e147580729e24fbb6d8707d2a0ddfc8bd036d43)
2023-12-07 15:23:49 +02:00
Weblate 3dc86b3a01 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: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translation: Servarr/Readarr
2023-12-06 16:01:34 +02:00
Taloth Saldono 24ad6134e3 Small helper in UI to access Readarr API more easily
(cherry picked from commit 090cdc364ef335fbfea8cf540696af813f6ecea4)

Closes #677
2023-12-06 12:15:07 +02:00
dependabot[bot] 033f8c40af Bump @adobe/css-tools from 4.3.1 to 4.3.2
Bumps [@adobe/css-tools](https://github.com/adobe/css-tools) from 4.3.1 to 4.3.2.
- [Changelog](https://github.com/adobe/css-tools/blob/main/History.md)
- [Commits](https://github.com/adobe/css-tools/commits)

---
updated-dependencies:
- dependency-name: "@adobe/css-tools"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-03 13:51:48 +02:00
Weblate 4c73a619eb 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: Dimitri <dimitridroeck@gmail.com>
Co-authored-by: Patatra <patrice.chevreau@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
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/readarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/id/
Translation: Servarr/Readarr
2023-12-01 04:02:04 +02:00
bakerboy448 3ca798e983 Fixed: RootFolderWatchingService Logging 2023-12-01 04:01:32 +02:00
bmarinov d9827fd6a6 Fixed: Filter unchanged files using UTC timestamps
(cherry picked from commit 9fc66e9b985a1eabd05f324ac631dfac39d2aebc)
2023-11-26 09:35:42 +02:00
Stevie Robinson f4f03a853f New: Remove defunct Boxcar notifications
(cherry picked from commit c6ad2396bb98dc8eb1ad47bf5d066b227a47f8b5)

Closes #3103
2023-11-25 22:30:43 +02:00
servarr[bot] 4f4e4bf2ca Fixed: Disable SSL when using the removed SslCertHash configuration (#3088)
(cherry picked from commit d95660d3c78d1ee11a7966d58e78a82a8df01393)

Co-authored-by: Mark McDowall <mark@mcdowall.ca>
2023-11-20 08:13:17 +02:00
Bogdan 413a70a312 Wrap long lines in description lists
(cherry picked from commit 4e048bf4999230f9c75d98ef2d8a1201d5ed68ed)
2023-11-20 07:38:05 +02:00
Weblate a8f2b91010 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translation: Servarr/Readarr
2023-11-17 03:31:51 +02:00
Mark McDowall 68a4ee6000 Rename 'ReturnUrl' to 'returnUrl' for forms auth redirection
(cherry picked from commit 812712e2843a738054c065a6d5c1b7c81c5f8e7b)
2023-11-17 03:31:00 +02:00
Bogdan 5196ce311b Fixed: Enforce validation warnings when testing providers
(cherry picked from commit c3b4126d0c4f449a41e2cf7ea438b20e25370995)
2023-11-17 02:39:58 +02:00
Bogdan ae92b22727 Fixed: Record status for notifications on tests
(cherry picked from commit 3d05913534e40e1b9ff217798d806d0b7c170d2d)
2023-11-10 19:12:48 +02:00
Weblate 0bccffef01 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Francisco Cachado <franciscomcachado@gmail.com>
Co-authored-by: Javier Parada <jparada@gmail.com>
Co-authored-by: Nesego <nesego@gmail.com>
Co-authored-by: RicardoVelaC <ricardovelac@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt/
Translation: Servarr/Readarr
2023-11-10 03:17:40 +02:00
Mark McDowall bca899b9c0 Don't store successful results for invalid providers
(cherry picked from commit de23182d593e2284972103d505e66dd8d812dfdb)
2023-11-10 03:16:30 +02:00
Bogdan 2bb576a94b Bump version to 0.3.11 2023-11-05 11:15:33 +02:00
Mark McDowall bb49949853 Fixed: Blocking unknown indexers from pushed releases
(cherry picked from commit 44d8dbaac81706691124ae5f8317289f0a3e5d73)
2023-10-31 00:04:49 +02:00
Bogdan a093061b29 Use variable for App name in translations
Towards #2925
2023-10-30 23:07:03 +02:00
Weblate df876707c4 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Baptiste Mongin <baptiste.mongin@gmail.com>
Co-authored-by: RicardoVelaC <ricardovelac@gmail.com>
Co-authored-by: Ruben Lourenco <ruben.lourenco01@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt/
Translation: Servarr/Readarr
2023-10-30 23:03:37 +02:00
Bogdan 2af33143ba New: Add Download Client validation for indexers
(cherry picked from commit e53b7f8c945e3597ca1719961e82540f1f01f0e9)

Closes #3033
2023-10-29 01:24:56 +03:00
Bogdan c3c5a47776 New: Set busy timeout for SQLite
(cherry picked from commit 192eb7b62ae60f300a9371ce3ed2e0056b5a1f4d)

Closes #3039
2023-10-29 01:24:14 +03:00
Weblate a21abe0838 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: ID-86 <id86dev@gmail.com>
Co-authored-by: Jordy <prive@jordyhoebergen.nl>
Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: bai0012 <baicongrui@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/id/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translation: Servarr/Readarr
2023-10-29 01:22:50 +03:00
Bogdan a32f5f6639 Allow 0 as valid value in QualityProfileExistsValidator
(cherry picked from commit 36ca24e55a5eda859047d82855f65c401cc0b30f)
2023-10-29 01:22:30 +03:00
Bogdan 4cd45ecc21 Sort Custom Formats by name
(cherry picked from commit e9bb1d52a72b20a58d1a672ecfa3797eda6f081a)
2023-10-29 01:22:19 +03:00
Bogdan 2c8e0b1ca4 Add default value for Queue count to avoid failed prop type
(cherry picked from commit 43ed7730f08de7baddbdafcccd99370258593221)
2023-10-29 01:22:07 +03:00
Weblate bd25c9e3e0 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ro/
Translation: Servarr/Readarr
2023-10-22 10:04:08 +03:00
Bogdan ee64b8788b Bump version to 0.3.10 2023-10-22 09:35:38 +03:00
Weblate 7aeada2089 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dlgeri123 <bornemiszageri@gmail.com>
Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: jianl <jianjianfengyun@126.com>
Co-authored-by: 宿命 <331874545@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translation: Servarr/Readarr
2023-10-22 09:34:13 +03:00
Bogdan e188c9aac0 Don't die when trying to open file with nullable path
Closes #3012
2023-10-19 17:35:23 +03:00
Bogdan a3ae2359f5 Fixed: Ignore case when cleansing announce URLs
(cherry picked from commit 41ed300899e8d7de82b1113d13ac6f6cf28cec17)
2023-10-19 17:06:40 +03:00
Bogdan 5b92905dd4 Bump version to 0.3.9 2023-10-15 07:51:31 +03:00
Weblate fc402743aa Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: DavidHenryThoreau <sorau@protonmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fr/
Translation: Servarr/Readarr
2023-10-13 12:37:22 +03:00
Bogdan b9d53ed732 Add PostgreSQL specific query for cleaning multiple monitored editions
Fixes #2995
2023-10-12 02:20:28 +03:00
Bogdan d248747635 Fixed: Avoid logging evaluations when not using any Remote Path Mappings
(cherry picked from commit 44eb729ccc13237f4439006159bd616e8bdb5750)
2023-10-10 07:12:09 +03:00
Bogdan d70224c811 Add status test all button for IndexerLongTermStatusCheck
(cherry picked from commit 4ffa1816bd2305550abee20cea27e1296a99ddf6)
2023-10-10 07:11:58 +03:00
Bogdan acdf8c8aa8 Bump version to 0.3.8 2023-10-08 07:09:11 +03:00
Bogdan 3ed41554ce Log Notifiarr errors as warnings 2023-10-07 22:58:40 +03:00
Bogdan ce808c6d7b Prevent mapping null metadata responses
Fixes #2971
2023-10-07 01:40:49 +03:00
Weblate 63b1b56a4f Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Dimitri <dimitridroeck@gmail.com>
Co-authored-by: Garkus98 <ivan12061998@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: RicardoVelaC <ricardovelac@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: blankhang <blankhang@gmail.com>
Co-authored-by: 宿命 <331874545@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translation: Servarr/Readarr
2023-10-07 01:04:37 +03:00
Bogdan a5647bedc8 Remove reddit from support issues 2023-10-07 00:25:19 +03:00
Servarr fe659bb79d Automated API Docs update 2023-10-04 06:59:02 +03:00
MxMarx 9918535509 New: Author Added notification
(cherry picked from commit https://github.com/Radarr/Radarr/commit/f890aadffa5ae579bcf65abdcf3e3948837084a9)
2023-10-04 06:47:23 +03:00
William Brockhus f9a6db40b8 Fixed: Ignore timezone when comparing tag dates 2023-10-04 06:26:08 +03:00
Bogdan 6273d69ed6 Fix tests 2023-10-04 05:50:57 +03:00
dependabot[bot] 7012380e95 Bump postcss from 8.4.23 to 8.4.31
Bumps [postcss](https://github.com/postcss/postcss) from 8.4.23 to 8.4.31.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.4.23...8.4.31)

---
updated-dependencies:
- dependency-name: postcss
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-04 04:05:39 +03:00
Bogdan b001ecd698 Preserve the protocol for fanart images
Closes #2944
2023-10-01 17:29:46 +03:00
Bogdan e28becdda4 Preserve the protocol in Author Image
Closes #2942
2023-10-01 17:27:38 +03:00
Mark McDowall eae06695e8 Fixed: Completed downloads in Qbit missing import path
(cherry picked from commit 35365665cfd436ac276dd9591e23333bd26cf789)

Closes #2959
2023-10-01 17:23:21 +03:00
Weblate 54a9af2ced Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Stevie Robinson <stevie.robinson@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: mr cmuc <github@nextcos.de>
Co-authored-by: 宿命 <331874545@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translation: Servarr/Readarr
2023-10-01 17:20:42 +03:00
Stevie Robinson c9b55266fc Fixed: qBittorent history retention to allow at least 14 days seeding
(cherry picked from commit 33b87acabf2b4c71ee24cda1a466dec6f4f76996)
2023-10-01 17:20:16 +03:00
bakerboy448 05b64406a4 Fixed: Only apply remote path mappings for completed items in Qbit
(cherry picked from commit 583eb52ddc01b608ab6cb17e863a8830c17b7b75)
2023-10-01 17:20:06 +03:00
Bogdan 1f37c5387b Revert "Avoid returning null in static resource mapper Task"
This reverts commit d7305b9753.
2023-10-01 03:26:00 +03:00
Stevie Robinson 4a6c7042fe Fixed: SABnzbd history retention to allow at least 14 days
(cherry picked from commit a3938d8e0264b48b35f4715cbc15329fb489218a)
2023-09-27 19:47:13 +03:00
Bogdan d7305b9753 Avoid returning null in static resource mapper Task
(cherry picked from commit a1ea7accb32bc72f61ed4531d109f76fad843939)
2023-09-27 18:54:41 +03:00
Bogdan bd56643eaa Bump version to 0.3.7 2023-09-24 16:24:12 +03:00
Stevie Robinson 44e6de2e23 Add health check for dl clients removing completed downloads + enable for sab and qbit
(cherry picked from commit 7f2cd8a0e99b537a1c616998514bacdd8468a016)

Closes #2939
2023-09-19 21:44:45 +03:00
Mark McDowall b209d047fa Fixed: Don't try to create metadata images if source files doesn't exist
(cherry picked from commit 9a1022386a031c928fc0495d6ab990ebce605ec1)

Closes #2933
2023-09-19 21:37:09 +03:00
Mark McDowall fd5ab27df6 New: Don't treat 400 responses from Notifiarr as errors
(cherry picked from commit 5eb420bbe12f59d0a5392abf3d351be28ca210e6)

Closes #2938
2023-09-19 21:35:26 +03:00
Bogdan 4a89befd79 Log request failures in Notifiarr 2023-09-19 21:34:12 +03:00
Bogdan 1a30293c33 Check for empty description as well in ParseQuality 2023-09-19 21:33:26 +03:00
Bogdan f5c2a6bf51 Fix use of empty Author SortName in filename 2023-09-19 18:55:24 +03:00
Weblate f3d90fdaf1 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Akashi2020 <dieux02400@gmail.com>
Co-authored-by: Anthony Veaudry <anthonyveaudry@gmail.com>
Co-authored-by: Gyuyeop Kim <rlarbduq777@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Herve Lauwerier <hervelauwerier@gmail.com>
Co-authored-by: Richard de Souza Leite <rs9010482@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: mati300m <mateusz.smolec@gmail.com>
Co-authored-by: 宿命 <331874545@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translation: Servarr/Readarr
2023-09-19 14:32:26 +03:00
MxMarx 04c5671a0a Fixed: Release Push api broken when no indexer id is specified 2023-09-19 14:31:44 +03:00
Qstick 22cc88c5e7 Fixed: Show correct error on unauthorized caps call
(cherry picked from commit f2b0fc946e1fb1b4649f1b46a003bd2add09a461)
2023-09-19 14:28:41 +03:00
Bogdan ca0c95a2d2 Fixed: Skip parsing releases without title
(cherry picked from commit c7824bb593291634bf14a5f7aa689666969b03bf)
2023-09-19 14:28:14 +03:00
Mark McDowall 419f790d66 Fixed: Don't allow quality profile to be created without all qualities
(cherry picked from commit 32e1ae2f64827272d351991838200884876e52b4)
2023-09-19 14:28:01 +03:00
Bogdan 9fe08429bc Use await on reading the response content
(cherry picked from commit 82d586e7015d7ea06356ca436024a8af5a4fb677)
2023-09-18 03:24:30 +03:00
Bogdan 71f4a88ab3 Bump version to 0.3.6 2023-09-17 12:02:41 +03:00
Bogdan 30b283eda3 Fixed: Ignore inaccessible mount points
(cherry picked from commit 60f18249b05daa20523542beef54bc126d963d1e)
2023-09-14 06:40:05 +03:00
Bogdan e23d0bbfa1 Add housekeeping task to unmonitor multiple monitored editions 2023-09-10 20:30:55 +03:00
Weblate 765a2aa01b Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Qstick <qstick@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/da/
Translation: Servarr/Readarr
2023-09-10 20:30:40 +03:00
Bogdan 64895c3210 Bump version to 0.3.5 2023-09-10 09:04:03 +03:00
Weblate 03ab84a814 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: DavidJares <david.jares@me.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: He Zhu <zhuhe202@qq.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: 宿命 <331874545@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translation: Servarr/Readarr
2023-09-10 00:33:32 -05:00
Bogdan b77e5b14e1 Fixed: macOS version detection
(cherry picked from commit 060be6177a5477c94823e6a423c42064dedc1afb)

Closes #2908
2023-09-08 05:08:09 +03:00
Bogdan 75efbd45e1 Fixed: Calculating seed time for qBittorrent
(cherry picked from commit 1b3ff64cc521396f9f1623617052c497649325a8)
2023-09-08 04:17:10 +03:00
Weblate 00cac507ad Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: DavidJares <david.jares@me.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: 宿命 <331874545@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translation: Servarr/Readarr
2023-09-07 11:58:51 +03:00
Bogdan c4850505b0 New: Add Plex Media Server notifications 2023-09-07 10:18:24 +03:00
Bogdan 75213c86a1 Bump dotnet to 6.0.21 2023-09-05 16:30:21 +03:00
Bogdan b8c3a42643 Migrate to merged proposals now included in babel/present-env
Closes #2899
2023-09-05 03:03:33 +03:00
Bogdan 8acb034aa6 Use not allowed cursor for disabled select options
(cherry picked from commit 229a4bba05d1f42089aa92b1d938747e152590b2)
2023-09-05 02:58:09 +03:00
Bogdan 889d32552b Update UI dev packages 2023-09-01 14:29:20 +03:00
Bogdan adc5f4db97 Fixed: Increase timeout when downloading updates
(cherry picked from commit 467ce70291c793042ffb3ed8942c42e7bc1424d5)
2023-09-01 04:05:16 +03:00
Weblate 9d08050f96 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: He Zhu <zhuhe202@qq.com>
Co-authored-by: monopolo11 <bernardorn21@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translation: Servarr/Readarr
2023-08-31 20:59:43 +03:00
Weblate f8cffbb4cf Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: AlexR-sf <omg.portal.supp@gmail.com>
Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: DavidJares <david.jares@me.com>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: He Zhu <zhuhe202@qq.com>
Co-authored-by: Renan da Mota Ciciliato <renanciciliato@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: brokje1988 <brokje1988@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translation: Servarr/Readarr
2023-08-29 18:12:42 +03:00
Weblate 14aeb66142 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: He Zhu <zhuhe202@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translation: Servarr/Readarr
2023-08-27 19:22:06 +03:00
Bogdan 37e8e11e31 Ensure the correct icons are spinning when refreshing authors and books 2023-08-25 21:51:52 +03:00
Bogdan bdb2f14936 Prevent NullRef in GetChangedAuthors when metadata is down 2023-08-25 21:49:15 +03:00
Mark McDowall a97af657be Improved UI error messages (stack trace and version)
(cherry picked from commit 37c355da51b654cea7309678c32a83a5cbe43d1f)

Closes #2207
2023-08-24 21:15:47 +03:00
Servarr 301127e6dc Automated API Docs update 2023-08-24 00:44:35 +03:00
Bogdan 1f95bcae4e New: Async HttpClient
(cherry picked from commit 0feee191462dd3e5dde66e476e8b4b46a85ec4f0)
2023-08-24 00:38:31 +03:00
Bogdan 29118cda45 New: Use HTTP/2 in HttpClient
(cherry picked from commit 78593f428acc578785f9ecfdd41fbf2443d93d84)
2023-08-24 00:38:31 +03:00
Bogdan 09beaa939d Fixed: (FileList) Prevent double query escaping in search requests 2023-08-24 00:38:31 +03:00
Bogdan 2107624f1c Prevent health checks warnings for disabled notifications
(cherry picked from commit 5a7f42a63e25d6abdb187c37e92a908a6b85fb4d)
2023-08-23 04:58:22 +03:00
Weblate c1c2076e5c Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translation: Servarr/Readarr
2023-08-23 04:57:55 +03:00
Bogdan c31a797bd8 Revert "Switch to Parallel.ForEach for list processing with MaxParallelism"
This reverts commit ebb2b4eca3.
2023-08-22 06:03:14 +03:00
Qstick ebb2b4eca3 Switch to Parallel.ForEach for list processing with MaxParallelism
(cherry picked from commit 0f93e04186f24abdb0cf0b3ba6a3505fda834e06)
2023-08-21 04:44:59 +03:00
Qstick 3ec5d9b9fe Use default MemoryAllocator for ImageSharp resizing
(cherry picked from commit c1a3a8249befde0a1b68e7845d5d2346066457a1)
2023-08-21 04:42:59 +03:00
Qstick 1ad84a7c44 Fixed: Ignore case when comparing torrent infohash
(cherry picked from commit 7986488c6d1687b0810b3bcac2c1dae725e770ac)
2023-08-20 16:10:26 -05:00
Bogdan 9d67c18254 Bump version to 0.3.4 2023-08-20 12:24:38 +03:00
396 changed files with 8245 additions and 4286 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
name: Bug Report name: Bug Report
description: 'Report a new bug, if you are not 100% certain this is a bug please go to our Reddit or Discord first' description: 'Report a new bug, if you are not 100% certain this is a bug please go to our Discord first'
labels: ['Type: Bug', 'Status: Needs Triage'] labels: ['Type: Bug', 'Status: Needs Triage']
body: body:
- type: checkboxes - type: checkboxes
-3
View File
@@ -3,6 +3,3 @@ contact_links:
- name: Support via Discord - name: Support via Discord
url: https://readarr.com/discord url: https://readarr.com/discord
about: Chat with users and devs on support and setup related topics. about: Chat with users and devs on support and setup related topics.
- name: Support via Reddit
url: https://reddit.com/r/Readarr
about: Discuss and search thru support topics.
+16
View File
@@ -0,0 +1,16 @@
# Configuration for Label Actions - https://github.com/dessant/label-actions
'Type: Support':
comment: >
:wave: @{issue-author}, we use the issue tracker exclusively
for bug reports and feature requests. However, this issue appears
to be a support request. Please hop over onto our [Discord](https://readarr.com/discord).
close: true
close-reason: 'not planned'
'Status: Logs Needed':
comment: >
:wave: @{issue-author}, In order to help you further we'll need to see logs.
You'll need to enable trace logging and replicate the problem that you encountered.
Guidance on how to enable trace logging can be found in
our [troubleshooting guide](https://wiki.servarr.com/readarr/troubleshooting#logging-and-log-files).
+17
View File
@@ -0,0 +1,17 @@
name: 'Label Actions'
on:
issues:
types: [labeled, unlabeled]
permissions:
contents: read
issues: write
jobs:
action:
runs-on: ubuntu-latest
steps:
- uses: dessant/label-actions@v3
with:
process-only: 'issues'
-32
View File
@@ -1,32 +0,0 @@
name: 'Support requests'
on:
issues:
types: [labeled, unlabeled, reopened]
jobs:
support:
runs-on: ubuntu-latest
steps:
- uses: dessant/support-requests@v3
with:
github-token: ${{ github.token }}
support-label: 'Type: Support'
issue-comment: >
:wave: @{issue-author}, we use the issue tracker exclusively
for bug reports and feature requests. However, this issue appears
to be a support request. Please hop over onto our [Discord](https://readarr.com/discord)
or [Subreddit](https://reddit.com/r/readarr)
close-issue: true
lock-issue: false
- uses: dessant/support-requests@v3
with:
github-token: ${{ github.token }}
support-label: 'Status: Logs Needed'
issue-comment: >
:wave: @{issue-author}, In order to help you further we'll need to see logs.
You'll need to enable trace logging and replicate the problem that you encountered.
Guidance on how to enable trace logging can be found in
our [troubleshooting guide](https://wiki.servarr.com/readarr/troubleshooting#logging-and-log-files).
close-issue: false
lock-issue: false
+2 -2
View File
@@ -9,13 +9,13 @@ variables:
testsFolder: './_tests' testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '0.3.3' majorVersion: '0.3.17'
minorVersion: $[counter('minorVersion', 1)] minorVersion: $[counter('minorVersion', 1)]
readarrVersion: '$(majorVersion).$(minorVersion)' readarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(readarrVersion)' buildName: '$(Build.SourceBranchName).$(readarrVersion)'
sentryOrg: 'servarr' sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com' sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.408' dotnetVersion: '6.0.417'
nodeVersion: '16.X' nodeVersion: '16.X'
innoVersion: '6.2.0' innoVersion: '6.2.0'
windowsImage: 'windows-2022' windowsImage: 'windows-2022'
+6 -4
View File
@@ -2,16 +2,18 @@ const loose = true;
module.exports = { module.exports = {
plugins: [ plugins: [
'@babel/plugin-transform-logical-assignment-operators',
// Stage 1 // Stage 1
'@babel/plugin-proposal-export-default-from', '@babel/plugin-proposal-export-default-from',
['@babel/plugin-proposal-optional-chaining', { loose }], ['@babel/plugin-transform-optional-chaining', { loose }],
['@babel/plugin-proposal-nullish-coalescing-operator', { loose }], ['@babel/plugin-transform-nullish-coalescing-operator', { loose }],
// Stage 2 // Stage 2
'@babel/plugin-proposal-export-namespace-from', '@babel/plugin-transform-export-namespace-from',
// Stage 3 // Stage 3
['@babel/plugin-proposal-class-properties', { loose }], ['@babel/plugin-transform-class-properties', { loose }],
'@babel/plugin-syntax-dynamic-import' '@babel/plugin-syntax-dynamic-import'
], ],
env: { env: {
+14 -3
View File
@@ -23,7 +23,7 @@ import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected'; import toggleSelected from 'Utilities/Table/toggleSelected';
import QueueOptionsConnector from './QueueOptionsConnector'; import QueueOptionsConnector from './QueueOptionsConnector';
import QueueRowConnector from './QueueRowConnector'; import QueueRowConnector from './QueueRowConnector';
import RemoveQueueItemsModal from './RemoveQueueItemsModal'; import RemoveQueueItemModal from './RemoveQueueItemModal';
class Queue extends Component { class Queue extends Component {
@@ -289,9 +289,16 @@ class Queue extends Component {
} }
</PageContentBody> </PageContentBody>
<RemoveQueueItemsModal <RemoveQueueItemModal
isOpen={isConfirmRemoveModalOpen} isOpen={isConfirmRemoveModalOpen}
selectedCount={selectedCount} selectedCount={selectedCount}
canChangeCategory={isConfirmRemoveModalOpen && (
selectedIds.every((id) => {
const item = items.find((i) => i.id === id);
return !!(item && item.downloadClientHasPostImportCategory);
})
)}
canIgnore={isConfirmRemoveModalOpen && ( canIgnore={isConfirmRemoveModalOpen && (
selectedIds.every((id) => { selectedIds.every((id) => {
const item = items.find((i) => i.id === id); const item = items.find((i) => i.id === id);
@@ -299,7 +306,7 @@ class Queue extends Component {
return !!(item && item.authorId && item.bookId); return !!(item && item.authorId && item.bookId);
}) })
)} )}
allPending={isConfirmRemoveModalOpen && ( pending={isConfirmRemoveModalOpen && (
selectedIds.every((id) => { selectedIds.every((id) => {
const item = items.find((i) => i.id === id); const item = items.find((i) => i.id === id);
@@ -338,4 +345,8 @@ Queue.propTypes = {
onRemoveSelectedPress: PropTypes.func.isRequired onRemoveSelectedPress: PropTypes.func.isRequired
}; };
Queue.defaultProps = {
count: 0
};
export default Queue; export default Queue;
+3
View File
@@ -98,6 +98,7 @@ class QueueRow extends Component {
indexer, indexer,
outputPath, outputPath,
downloadClient, downloadClient,
downloadClientHasPostImportCategory,
downloadForced, downloadForced,
estimatedCompletionTime, estimatedCompletionTime,
timeleft, timeleft,
@@ -389,6 +390,7 @@ class QueueRow extends Component {
<RemoveQueueItemModal <RemoveQueueItemModal
isOpen={isRemoveQueueItemModalOpen} isOpen={isRemoveQueueItemModalOpen}
sourceTitle={title} sourceTitle={title}
canChangeCategory={!!downloadClientHasPostImportCategory}
canIgnore={!!author} canIgnore={!!author}
isPending={isPending} isPending={isPending}
onRemovePress={this.onRemoveQueueItemModalConfirmed} onRemovePress={this.onRemoveQueueItemModalConfirmed}
@@ -418,6 +420,7 @@ QueueRow.propTypes = {
indexer: PropTypes.string, indexer: PropTypes.string,
outputPath: PropTypes.string, outputPath: PropTypes.string,
downloadClient: PropTypes.string, downloadClient: PropTypes.string,
downloadClientHasPostImportCategory: PropTypes.bool,
downloadForced: PropTypes.bool.isRequired, downloadForced: PropTypes.bool.isRequired,
estimatedCompletionTime: PropTypes.string, estimatedCompletionTime: PropTypes.string,
timeleft: PropTypes.string, timeleft: PropTypes.string,
@@ -1,177 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button';
import Modal from 'Components/Modal/Modal';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
class RemoveQueueItemModal extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
remove: true,
blocklist: false,
skipRedownload: false
};
}
//
// Control
resetState = function() {
this.setState({
remove: true,
blocklist: false,
skipRedownload: false
});
};
//
// Listeners
onRemoveChange = ({ value }) => {
this.setState({ remove: value });
};
onBlocklistChange = ({ value }) => {
this.setState({ blocklist: value });
};
onSkipRedownloadChange = ({ value }) => {
this.setState({ skipRedownload: value });
};
onRemoveConfirmed = () => {
const state = this.state;
this.resetState();
this.props.onRemovePress(state);
};
onModalClose = () => {
this.resetState();
this.props.onModalClose();
};
//
// Render
render() {
const {
isOpen,
sourceTitle,
canIgnore,
isPending
} = this.props;
const { remove, blocklist, skipRedownload } = this.state;
return (
<Modal
isOpen={isOpen}
size={sizes.MEDIUM}
onModalClose={this.onModalClose}
>
<ModalContent
onModalClose={this.onModalClose}
>
<ModalHeader>
Remove - {sourceTitle}
</ModalHeader>
<ModalBody>
<div>
Are you sure you want to remove '{sourceTitle}' from the queue?
</div>
{
isPending ?
null :
<FormGroup>
<FormLabel>
{translate('RemoveFromDownloadClient')}
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="remove"
value={remove}
helpTextWarning={translate('RemoveHelpTextWarning')}
isDisabled={!canIgnore}
onChange={this.onRemoveChange}
/>
</FormGroup>
}
<FormGroup>
<FormLabel>
{translate('BlocklistRelease')}
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="blocklist"
value={blocklist}
helpText={translate('BlocklistReleaseHelpText')}
onChange={this.onBlocklistChange}
/>
</FormGroup>
{
blocklist &&
<FormGroup>
<FormLabel>
{translate('SkipRedownload')}
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="skipRedownload"
value={skipRedownload}
helpText={translate('SkipRedownloadHelpText')}
onChange={this.onSkipRedownloadChange}
/>
</FormGroup>
}
</ModalBody>
<ModalFooter>
<Button onPress={this.onModalClose}>
Close
</Button>
<Button
kind={kinds.DANGER}
onPress={this.onRemoveConfirmed}
>
Remove
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}
}
RemoveQueueItemModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
sourceTitle: PropTypes.string.isRequired,
canIgnore: PropTypes.bool.isRequired,
isPending: PropTypes.bool.isRequired,
onRemovePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default RemoveQueueItemModal;
@@ -0,0 +1,230 @@
import React, { useCallback, useMemo, useState } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button';
import Modal from 'Components/Modal/Modal';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './RemoveQueueItemModal.css';
interface RemovePressProps {
remove: boolean;
changeCategory: boolean;
blocklist: boolean;
skipRedownload: boolean;
}
interface RemoveQueueItemModalProps {
isOpen: boolean;
sourceTitle: string;
canChangeCategory: boolean;
canIgnore: boolean;
isPending: boolean;
selectedCount?: number;
onRemovePress(props: RemovePressProps): void;
onModalClose: () => void;
}
type RemovalMethod = 'removeFromClient' | 'changeCategory' | 'ignore';
type BlocklistMethod =
| 'doNotBlocklist'
| 'blocklistAndSearch'
| 'blocklistOnly';
function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
const {
isOpen,
sourceTitle,
canIgnore,
canChangeCategory,
isPending,
selectedCount,
onRemovePress,
onModalClose,
} = props;
const multipleSelected = selectedCount && selectedCount > 1;
const [removalMethod, setRemovalMethod] =
useState<RemovalMethod>('removeFromClient');
const [blocklistMethod, setBlocklistMethod] =
useState<BlocklistMethod>('doNotBlocklist');
const { title, message } = useMemo(() => {
if (!selectedCount) {
return {
title: translate('RemoveQueueItem', { sourceTitle }),
message: translate('RemoveQueueItemConfirmation', { sourceTitle }),
};
}
if (selectedCount === 1) {
return {
title: translate('RemoveSelectedItem'),
message: translate('RemoveSelectedItemQueueMessageText'),
};
}
return {
title: translate('RemoveSelectedItems'),
message: translate('RemoveSelectedItemsQueueMessageText', {
selectedCount,
}),
};
}, [sourceTitle, selectedCount]);
const removalMethodOptions = useMemo(() => {
return [
{
key: 'removeFromClient',
value: translate('RemoveFromDownloadClient'),
hint: multipleSelected
? translate('RemoveMultipleFromDownloadClientHint')
: translate('RemoveFromDownloadClientHint'),
},
{
key: 'changeCategory',
value: translate('ChangeCategory'),
isDisabled: !canChangeCategory,
hint: multipleSelected
? translate('ChangeCategoryMultipleHint')
: translate('ChangeCategoryHint'),
},
{
key: 'ignore',
value: multipleSelected
? translate('IgnoreDownloads')
: translate('IgnoreDownload'),
isDisabled: !canIgnore,
hint: multipleSelected
? translate('IgnoreDownloadsHint')
: translate('IgnoreDownloadHint'),
},
];
}, [canChangeCategory, canIgnore, multipleSelected]);
const blocklistMethodOptions = useMemo(() => {
return [
{
key: 'doNotBlocklist',
value: translate('DoNotBlocklist'),
hint: translate('DoNotBlocklistHint'),
},
{
key: 'blocklistAndSearch',
value: translate('BlocklistAndSearch'),
hint: multipleSelected
? translate('BlocklistAndSearchMultipleHint')
: translate('BlocklistAndSearchHint'),
},
{
key: 'blocklistOnly',
value: translate('BlocklistOnly'),
hint: multipleSelected
? translate('BlocklistMultipleOnlyHint')
: translate('BlocklistOnlyHint'),
},
];
}, [multipleSelected]);
const handleRemovalMethodChange = useCallback(
({ value }: { value: RemovalMethod }) => {
setRemovalMethod(value);
},
[setRemovalMethod]
);
const handleBlocklistMethodChange = useCallback(
({ value }: { value: BlocklistMethod }) => {
setBlocklistMethod(value);
},
[setBlocklistMethod]
);
const handleConfirmRemove = useCallback(() => {
onRemovePress({
remove: removalMethod === 'removeFromClient',
changeCategory: removalMethod === 'changeCategory',
blocklist: blocklistMethod !== 'doNotBlocklist',
skipRedownload: blocklistMethod === 'blocklistOnly',
});
setRemovalMethod('removeFromClient');
setBlocklistMethod('doNotBlocklist');
}, [
removalMethod,
blocklistMethod,
setRemovalMethod,
setBlocklistMethod,
onRemovePress,
]);
const handleModalClose = useCallback(() => {
setRemovalMethod('removeFromClient');
setBlocklistMethod('doNotBlocklist');
onModalClose();
}, [setRemovalMethod, setBlocklistMethod, onModalClose]);
return (
<Modal isOpen={isOpen} size={sizes.MEDIUM} onModalClose={handleModalClose}>
<ModalContent onModalClose={handleModalClose}>
<ModalHeader>{title}</ModalHeader>
<ModalBody>
<div className={styles.message}>{message}</div>
{isPending ? null : (
<FormGroup>
<FormLabel>{translate('RemoveQueueItemRemovalMethod')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="removalMethod"
value={removalMethod}
values={removalMethodOptions}
isDisabled={!canChangeCategory && !canIgnore}
helpTextWarning={translate(
'RemoveQueueItemRemovalMethodHelpTextWarning'
)}
onChange={handleRemovalMethodChange}
/>
</FormGroup>
)}
<FormGroup>
<FormLabel>
{multipleSelected
? translate('BlocklistReleases')
: translate('BlocklistRelease')}
</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="blocklistMethod"
value={blocklistMethod}
values={blocklistMethodOptions}
helpText={translate('BlocklistReleaseHelpText')}
onChange={handleBlocklistMethodChange}
/>
</FormGroup>
</ModalBody>
<ModalFooter>
<Button onPress={handleModalClose}>{translate('Close')}</Button>
<Button kind={kinds.DANGER} onPress={handleConfirmRemove}>
{translate('Remove')}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}
export default RemoveQueueItemModal;
@@ -1,178 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button';
import Modal from 'Components/Modal/Modal';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './RemoveQueueItemsModal.css';
class RemoveQueueItemsModal extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
remove: true,
blocklist: false,
skipRedownload: false
};
}
//
// Control
resetState = function() {
this.setState({
remove: true,
blocklist: false,
skipRedownload: false
});
};
//
// Listeners
onRemoveChange = ({ value }) => {
this.setState({ remove: value });
};
onBlocklistChange = ({ value }) => {
this.setState({ blocklist: value });
};
onSkipRedownloadChange = ({ value }) => {
this.setState({ skipRedownload: value });
};
onRemoveConfirmed = () => {
const state = this.state;
this.resetState();
this.props.onRemovePress(state);
};
onModalClose = () => {
this.resetState();
this.props.onModalClose();
};
//
// Render
render() {
const {
isOpen,
selectedCount,
canIgnore,
allPending
} = this.props;
const { remove, blocklist, skipRedownload } = this.state;
return (
<Modal
isOpen={isOpen}
size={sizes.MEDIUM}
onModalClose={this.onModalClose}
>
<ModalContent
onModalClose={this.onModalClose}
>
<ModalHeader>
{selectedCount > 1 ? translate('RemoveSelectedItems') : translate('RemoveSelectedItem')}
</ModalHeader>
<ModalBody>
<div className={styles.message}>
{selectedCount > 1 ? translate('RemoveSelectedItemsQueueMessageText', selectedCount) : translate('RemoveSelectedItemQueueMessageText')}
</div>
{
allPending ?
null :
<FormGroup>
<FormLabel>
{translate('RemoveFromDownloadClient')}
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="remove"
value={remove}
helpTextWarning={translate('RemoveHelpTextWarning')}
isDisabled={!canIgnore}
onChange={this.onRemoveChange}
/>
</FormGroup>
}
<FormGroup>
<FormLabel>
{selectedCount > 1 ? translate('BlocklistReleases') : translate('BlocklistRelease')}
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="blocklist"
value={blocklist}
helpText={translate('BlocklistReleaseHelpText')}
onChange={this.onBlocklistChange}
/>
</FormGroup>
{
blocklist &&
<FormGroup>
<FormLabel>
{translate('SkipRedownload')}
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="skipRedownload"
value={skipRedownload}
helpText={translate('SkipRedownloadHelpText')}
onChange={this.onSkipRedownloadChange}
/>
</FormGroup>
}
</ModalBody>
<ModalFooter>
<Button onPress={this.onModalClose}>
{translate('Close')}
</Button>
<Button
kind={kinds.DANGER}
onPress={this.onRemoveConfirmed}
>
{translate('Remove')}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}
}
RemoveQueueItemsModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
selectedCount: PropTypes.number.isRequired,
canIgnore: PropTypes.bool.isRequired,
allPending: PropTypes.bool.isRequired,
onRemovePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default RemoveQueueItemsModal;
+3 -6
View File
@@ -7,13 +7,10 @@ function findImage(images, coverType) {
} }
function getUrl(image, coverType, size) { function getUrl(image, coverType, size) {
if (image) { const imageUrl = image?.url;
// Remove protocol
let url = image.url;
url = url.replace(`${coverType}.jpg`, `${coverType}-${size}.jpg`); if (imageUrl) {
return imageUrl.replace(`${coverType}.jpg`, `${coverType}-${size}.jpg`);
return url;
} }
} }
@@ -44,6 +44,10 @@
margin-top: 20px; margin-top: 20px;
} }
.filterIcon {
float: right;
}
.authorNavigationButtons { .authorNavigationButtons {
position: absolute; position: absolute;
right: 0; right: 0;
+1
View File
@@ -6,6 +6,7 @@ interface CssExports {
'authorUpButton': string; 'authorUpButton': string;
'contentContainer': string; 'contentContainer': string;
'errorMessage': string; 'errorMessage': string;
'filterIcon': string;
'innerContentBody': string; 'innerContentBody': string;
'metadataMessage': string; 'metadataMessage': string;
'selectedTab': string; 'selectedTab': string;
+8 -3
View File
@@ -239,9 +239,14 @@ class AuthorDetails extends Component {
saveError, saveError,
isDeleting, isDeleting,
deleteError, deleteError,
statistics statistics = {}
} = this.props; } = this.props;
const {
bookFileCount = 0,
totalBookCount = 0
} = statistics;
const { const {
isOrganizeModalOpen, isOrganizeModalOpen,
isRetagModalOpen, isRetagModalOpen,
@@ -435,7 +440,7 @@ class AuthorDetails extends Component {
className={styles.tab} className={styles.tab}
selectedClassName={styles.selectedTab} selectedClassName={styles.selectedTab}
> >
{translate('BooksTotal', [statistics.totalBookCount])} {translate('BooksTotal', [totalBookCount])}
</Tab> </Tab>
<Tab <Tab
@@ -463,7 +468,7 @@ class AuthorDetails extends Component {
className={styles.tab} className={styles.tab}
selectedClassName={styles.selectedTab} selectedClassName={styles.selectedTab}
> >
{translate('FilesTotal', [statistics.bookFileCount])} {translate('FilesTotal', [bookFileCount])}
</Tab> </Tab>
{ {
@@ -155,7 +155,6 @@ function createMapStateToProps() {
const isRefreshing = isAuthorRefreshing || allAuthorRefreshing; const isRefreshing = isAuthorRefreshing || allAuthorRefreshing;
const isSearching = isCommandExecuting(findCommand(commands, { name: commandNames.AUTHOR_SEARCH, authorId: author.id })); const isSearching = isCommandExecuting(findCommand(commands, { name: commandNames.AUTHOR_SEARCH, authorId: author.id }));
const isRenamingFiles = isCommandExecuting(findCommand(commands, { name: commandNames.RENAME_FILES, authorId: author.id })); const isRenamingFiles = isCommandExecuting(findCommand(commands, { name: commandNames.RENAME_FILES, authorId: author.id }));
const isRenamingAuthorCommand = findCommand(commands, { name: commandNames.RENAME_AUTHOR }); const isRenamingAuthorCommand = findCommand(commands, { name: commandNames.RENAME_AUTHOR });
const isRenamingAuthor = ( const isRenamingAuthor = (
isCommandExecuting(isRenamingAuthorCommand) && isCommandExecuting(isRenamingAuthorCommand) &&
@@ -136,8 +136,9 @@
} }
.title { .title {
font-weight: 300;
font-size: 30px; font-size: 30px;
line-height: 50px; line-height: 30px;
} }
} }
@@ -25,12 +25,7 @@ const defaultFontSize = parseInt(fonts.defaultFontSize);
const lineHeight = parseFloat(fonts.lineHeight); const lineHeight = parseFloat(fonts.lineHeight);
function getFanartUrl(images) { function getFanartUrl(images) {
const fanartImage = images.find((x) => x.coverType === 'fanart'); return images.find((x) => x.coverType === 'fanart')?.url;
if (fanartImage) {
// Remove protocol
return fanartImage.url.replace(/^https?:/, '');
}
} }
class AuthorDetailsHeader extends Component { class AuthorDetailsHeader extends Component {
@@ -1,6 +1,7 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Modal from 'Components/Modal/Modal'; import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import AuthorHistoryContentConnector from './AuthorHistoryContentConnector'; import AuthorHistoryContentConnector from './AuthorHistoryContentConnector';
import AuthorHistoryModalContent from './AuthorHistoryModalContent'; import AuthorHistoryModalContent from './AuthorHistoryModalContent';
@@ -14,6 +15,7 @@ function AuthorHistoryModal(props) {
return ( return (
<Modal <Modal
isOpen={isOpen} isOpen={isOpen}
size={sizes.EXTRA_LARGE}
onModalClose={onModalClose} onModalClose={onModalClose}
> >
<AuthorHistoryContentConnector <AuthorHistoryContentConnector
@@ -5,6 +5,7 @@ import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent'; import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import translate from 'Utilities/String/translate';
import AuthorHistoryTableContent from './AuthorHistoryTableContent'; import AuthorHistoryTableContent from './AuthorHistoryTableContent';
class AuthorHistoryModalContent extends Component { class AuthorHistoryModalContent extends Component {
@@ -20,7 +21,7 @@ class AuthorHistoryModalContent extends Component {
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
History {translate('History')}
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
@@ -31,7 +32,7 @@ class AuthorHistoryModalContent extends Component {
<ModalFooter> <ModalFooter>
<Button onPress={onModalClose}> <Button onPress={onModalClose}>
Close {translate('Close')}
</Button> </Button>
</ModalFooter> </ModalFooter>
</ModalContent> </ModalContent>
@@ -4,7 +4,6 @@
word-break: break-word; word-break: break-word;
} }
.details,
.actions { .actions {
composes: cell from '~Components/Table/Cells/TableRowCell.css'; composes: cell from '~Components/Table/Cells/TableRowCell.css';
-1
View File
@@ -2,7 +2,6 @@
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'actions': string; 'actions': string;
'details': string;
'sourceTitle': string; 'sourceTitle': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import HistoryDetailsConnector from 'Activity/History/Details/HistoryDetailsConnector'; import HistoryDetailsConnector from 'Activity/History/Details/HistoryDetailsConnector';
import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell'; import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
import BookFormats from 'Book/BookFormats';
import BookQuality from 'Book/BookQuality'; import BookQuality from 'Book/BookQuality';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
@@ -11,6 +12,7 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow'; import TableRow from 'Components/Table/TableRow';
import Popover from 'Components/Tooltip/Popover'; import Popover from 'Components/Tooltip/Popover';
import { icons, kinds, tooltipPositions } from 'Helpers/Props'; import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import styles from './AuthorHistoryRow.css'; import styles from './AuthorHistoryRow.css';
@@ -75,6 +77,8 @@ class AuthorHistoryRow extends Component {
sourceTitle, sourceTitle,
quality, quality,
qualityCutoffNotMet, qualityCutoffNotMet,
customFormats,
customFormatScore,
date, date,
data, data,
book book
@@ -106,11 +110,19 @@ class AuthorHistoryRow extends Component {
/> />
</TableRowCell> </TableRowCell>
<TableRowCell>
<BookFormats formats={customFormats} />
</TableRowCell>
<TableRowCell>
{formatCustomFormatScore(customFormatScore, customFormats.length)}
</TableRowCell>
<RelativeDateCellConnector <RelativeDateCellConnector
date={date} date={date}
/> />
<TableRowCell className={styles.details}> <TableRowCell className={styles.actions}>
<Popover <Popover
anchor={ anchor={
<Icon <Icon
@@ -127,14 +139,13 @@ class AuthorHistoryRow extends Component {
} }
position={tooltipPositions.LEFT} position={tooltipPositions.LEFT}
/> />
</TableRowCell>
<TableRowCell className={styles.actions}>
{ {
eventType === 'grabbed' && eventType === 'grabbed' &&
<IconButton <IconButton
title={translate('MarkAsFailed')} title={translate('MarkAsFailed')}
name={icons.REMOVE} name={icons.REMOVE}
size={14}
onPress={this.onMarkAsFailedPress} onPress={this.onMarkAsFailedPress}
/> />
} }
@@ -160,6 +171,8 @@ AuthorHistoryRow.propTypes = {
sourceTitle: PropTypes.string.isRequired, sourceTitle: PropTypes.string.isRequired,
quality: PropTypes.object.isRequired, quality: PropTypes.object.isRequired,
qualityCutoffNotMet: PropTypes.bool.isRequired, qualityCutoffNotMet: PropTypes.bool.isRequired,
customFormats: PropTypes.arrayOf(PropTypes.object),
customFormatScore: PropTypes.number.isRequired,
date: PropTypes.string.isRequired, date: PropTypes.string.isRequired,
data: PropTypes.object.isRequired, data: PropTypes.object.isRequired,
fullAuthor: PropTypes.bool.isRequired, fullAuthor: PropTypes.bool.isRequired,
@@ -0,0 +1,9 @@
.container {
border: 1px solid var(--borderColor);
border-radius: 4px;
background-color: var(--inputBackgroundColor);
&:last-of-type {
margin-bottom: 0;
}
}
@@ -0,0 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'container': string;
}
export const cssExports: CssExports;
export default cssExports;
@@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import AuthorHistoryContentConnector from 'Author/History/AuthorHistoryContentConnector'; import AuthorHistoryContentConnector from 'Author/History/AuthorHistoryContentConnector';
import AuthorHistoryTableContent from 'Author/History/AuthorHistoryTableContent'; import AuthorHistoryTableContent from 'Author/History/AuthorHistoryTableContent';
import styles from './AuthorHistoryTable.css';
function AuthorHistoryTable(props) { function AuthorHistoryTable(props) {
const { const {
@@ -8,10 +9,12 @@ function AuthorHistoryTable(props) {
} = props; } = props;
return ( return (
<AuthorHistoryContentConnector <div className={styles.container}>
component={AuthorHistoryTableContent} <AuthorHistoryContentConnector
{...otherProps} component={AuthorHistoryTableContent}
/> {...otherProps}
/>
</div>
); );
} }
@@ -0,0 +1,5 @@
.blankpad {
padding-top: 10px;
padding-bottom: 10px;
padding-left: 2em;
}
@@ -0,0 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'blankpad': string;
}
export const cssExports: CssExports;
export default cssExports;
@@ -1,12 +1,14 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Alert from 'Components/Alert'; import Alert from 'Components/Alert';
import Icon from 'Components/Icon';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table'; import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody'; import TableBody from 'Components/Table/TableBody';
import { kinds } from 'Helpers/Props'; import { icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import AuthorHistoryRowConnector from './AuthorHistoryRowConnector'; import AuthorHistoryRowConnector from './AuthorHistoryRowConnector';
import styles from './AuthorHistoryTableContent.css';
const columns = [ const columns = [
{ {
@@ -15,32 +17,41 @@ const columns = [
}, },
{ {
name: 'book', name: 'book',
label: 'Book', label: () => translate('Book'),
isVisible: true isVisible: true
}, },
{ {
name: 'sourceTitle', name: 'sourceTitle',
label: 'Source Title', label: () => translate( 'SourceTitle'),
isVisible: true isVisible: true
}, },
{ {
name: 'quality', name: 'quality',
label: 'Quality', label: () => translate('Quality'),
isVisible: true
},
{
name: 'customFormats',
label: () => translate('CustomFormats'),
isSortable: false,
isVisible: true
},
{
name: 'customFormatScore',
label: React.createElement(Icon, {
name: icons.SCORE,
title: () => translate('CustomFormatScore')
}),
isSortable: true,
isVisible: true isVisible: true
}, },
{ {
name: 'date', name: 'date',
label: 'Date', label: () => translate('Date'),
isVisible: true
},
{
name: 'details',
label: 'Details',
isVisible: true isVisible: true
}, },
{ {
name: 'actions', name: 'actions',
label: 'Actions',
isVisible: true isVisible: true
} }
]; ];
@@ -64,7 +75,7 @@ class AuthorHistoryTableContent extends Component {
const hasItems = !!items.length; const hasItems = !!items.length;
return ( return (
<> <div>
{ {
isFetching && isFetching &&
<LoadingIndicator /> <LoadingIndicator />
@@ -79,7 +90,7 @@ class AuthorHistoryTableContent extends Component {
{ {
isPopulated && !hasItems && !error && isPopulated && !hasItems && !error &&
<div> <div className={styles.blankpad}>
{translate('NoHistory')} {translate('NoHistory')}
</div> </div>
} }
@@ -103,7 +114,7 @@ class AuthorHistoryTableContent extends Component {
</TableBody> </TableBody>
</Table> </Table>
} }
</> </div>
); );
} }
} }
@@ -16,7 +16,7 @@ import AuthorIndex from './AuthorIndex';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
createAuthorClientSideCollectionItemsSelector('authorIndex'), createAuthorClientSideCollectionItemsSelector('authorIndex'),
createCommandExecutingSelector(commandNames.REFRESH_AUTHOR), createCommandExecutingSelector(commandNames.BULK_REFRESH_AUTHOR),
createCommandExecutingSelector(commandNames.RSS_SYNC), createCommandExecutingSelector(commandNames.RSS_SYNC),
createCommandExecutingSelector(commandNames.RENAME_AUTHOR), createCommandExecutingSelector(commandNames.RENAME_AUTHOR),
createCommandExecutingSelector(commandNames.RETAG_AUTHOR), createCommandExecutingSelector(commandNames.RETAG_AUTHOR),
@@ -24,17 +24,17 @@ function createMapStateToProps() {
( (
author, author,
isRefreshingAuthor, isRefreshingAuthor,
isRssSyncExecuting,
isOrganizingAuthor, isOrganizingAuthor,
isRetaggingAuthor, isRetaggingAuthor,
isRssSyncExecuting,
dimensionsState dimensionsState
) => { ) => {
return { return {
...author, ...author,
isRefreshingAuthor, isRefreshingAuthor,
isRssSyncExecuting,
isOrganizingAuthor, isOrganizingAuthor,
isRetaggingAuthor, isRetaggingAuthor,
isRssSyncExecuting,
isSmallScreen: dimensionsState.isSmallScreen isSmallScreen: dimensionsState.isSmallScreen
}; };
} }
+46 -10
View File
@@ -3,6 +3,7 @@ import React from 'react';
import Label from 'Components/Label'; import Label from 'Components/Label';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import formatBytes from 'Utilities/Number/formatBytes'; import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
function getTooltip(title, quality, size, isMonitored, isCutoffNotMet) { function getTooltip(title, quality, size, isMonitored, isCutoffNotMet) {
const revision = quality.revision; const revision = quality.revision;
@@ -28,6 +29,36 @@ function getTooltip(title, quality, size, isMonitored, isCutoffNotMet) {
return title; return title;
} }
function revisionLabel(className, quality, showRevision) {
if (!showRevision) {
return;
}
if (quality.revision.isRepack) {
return (
<Label
className={className}
kind={kinds.PRIMARY}
title={translate('Repack')}
>
R
</Label>
);
}
if (quality.revision.version && quality.revision.version > 1) {
return (
<Label
className={className}
kind={kinds.PRIMARY}
title={translate('Proper')}
>
P
</Label>
);
}
}
function BookQuality(props) { function BookQuality(props) {
const { const {
className, className,
@@ -35,7 +66,8 @@ function BookQuality(props) {
quality, quality,
size, size,
isMonitored, isMonitored,
isCutoffNotMet isCutoffNotMet,
showRevision
} = props; } = props;
let kind = kinds.DEFAULT; let kind = kinds.DEFAULT;
@@ -50,13 +82,15 @@ function BookQuality(props) {
} }
return ( return (
<Label <span>
className={className} <Label
kind={kind} className={className}
title={getTooltip(title, quality, size, isMonitored, isCutoffNotMet)} kind={kind}
> title={getTooltip(title, quality, size, isMonitored, isCutoffNotMet)}
{quality.quality.name} >
</Label> {quality.quality.name}
</Label>{revisionLabel(className, quality, showRevision)}
</span>
); );
} }
@@ -66,12 +100,14 @@ BookQuality.propTypes = {
quality: PropTypes.object.isRequired, quality: PropTypes.object.isRequired,
size: PropTypes.number, size: PropTypes.number,
isMonitored: PropTypes.bool, isMonitored: PropTypes.bool,
isCutoffNotMet: PropTypes.bool isCutoffNotMet: PropTypes.bool,
showRevision: PropTypes.bool
}; };
BookQuality.defaultProps = { BookQuality.defaultProps = {
title: '', title: '',
isMonitored: true isMonitored: true,
showRevision: false
}; };
export default BookQuality; export default BookQuality;
+10 -4
View File
@@ -99,9 +99,14 @@ class BookDetails extends Component {
nextBook, nextBook,
isSearching, isSearching,
onRefreshPress, onRefreshPress,
onSearchPress onSearchPress,
statistics = {}
} = this.props; } = this.props;
const {
bookFileCount = 0
} = statistics;
const { const {
isOrganizeModalOpen, isOrganizeModalOpen,
isRetagModalOpen, isRetagModalOpen,
@@ -238,21 +243,21 @@ class BookDetails extends Component {
className={styles.tab} className={styles.tab}
selectedClassName={styles.selectedTab} selectedClassName={styles.selectedTab}
> >
History {translate('History')}
</Tab> </Tab>
<Tab <Tab
className={styles.tab} className={styles.tab}
selectedClassName={styles.selectedTab} selectedClassName={styles.selectedTab}
> >
Search {translate('Search')}
</Tab> </Tab>
<Tab <Tab
className={styles.tab} className={styles.tab}
selectedClassName={styles.selectedTab} selectedClassName={styles.selectedTab}
> >
Files {translate('FilesTotal', [bookFileCount])}
</Tab> </Tab>
{ {
@@ -335,6 +340,7 @@ BookDetails.propTypes = {
ratings: PropTypes.object.isRequired, ratings: PropTypes.object.isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired, images: PropTypes.arrayOf(PropTypes.object).isRequired,
links: PropTypes.arrayOf(PropTypes.object).isRequired, links: PropTypes.arrayOf(PropTypes.object).isRequired,
statistics: PropTypes.object.isRequired,
monitored: PropTypes.bool.isRequired, monitored: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired, shortDateFormat: PropTypes.string.isRequired,
isSaving: PropTypes.bool.isRequired, isSaving: PropTypes.bool.isRequired,
@@ -69,16 +69,21 @@ function createMapStateToProps() {
const previousBook = sortedBooks[bookIndex - 1] || _.last(sortedBooks); const previousBook = sortedBooks[bookIndex - 1] || _.last(sortedBooks);
const nextBook = sortedBooks[bookIndex + 1] || _.first(sortedBooks); const nextBook = sortedBooks[bookIndex + 1] || _.first(sortedBooks);
const isRefreshingCommand = findCommand(commands, { name: commandNames.REFRESH_BOOK });
const isRefreshing = (
isCommandExecuting(isRefreshingCommand) &&
isRefreshingCommand.body.bookId === book.id
);
const isSearchingCommand = findCommand(commands, { name: commandNames.BOOK_SEARCH }); const isSearchingCommand = findCommand(commands, { name: commandNames.BOOK_SEARCH });
const isSearching = ( const isSearching = (
isCommandExecuting(isSearchingCommand) && isCommandExecuting(isSearchingCommand) &&
isSearchingCommand.body.bookIds.indexOf(book.id) > -1 isSearchingCommand.body.bookIds.indexOf(book.id) > -1
); );
const isRenamingFiles = isCommandExecuting(findCommand(commands, { name: commandNames.RENAME_FILES, authorId: author.id }));
const isRefreshingCommand = findCommand(commands, { name: commandNames.REFRESH_BOOK }); const isRenamingAuthorCommand = findCommand(commands, { name: commandNames.RENAME_AUTHOR });
const isRefreshing = ( const isRenamingAuthor = (
isCommandExecuting(isRefreshingCommand) && isCommandExecuting(isRenamingAuthorCommand) &&
isRefreshingCommand.body.bookId === book.id isRenamingAuthorCommand.body.authorIds.indexOf(author.id) > -1
); );
const isFetching = isBookFilesFetching || editions.isFetching; const isFetching = isBookFilesFetching || editions.isFetching;
@@ -90,6 +95,8 @@ function createMapStateToProps() {
author, author,
isRefreshing, isRefreshing,
isSearching, isSearching,
isRenamingFiles,
isRenamingAuthor,
isFetching, isFetching,
isPopulated, isPopulated,
bookFilesError, bookFilesError,
@@ -125,9 +132,27 @@ class BookDetailsConnector extends Component {
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if (prevProps.id !== this.props.id || const {
id,
anyReleaseOk,
isRenamingFiles,
isRenamingAuthor
} = this.props;
if (
(prevProps.isRenamingFiles && !isRenamingFiles) ||
(prevProps.isRenamingAuthor && !isRenamingAuthor) ||
!_.isEqual(getMonitoredEditions(prevProps), getMonitoredEditions(this.props)) || !_.isEqual(getMonitoredEditions(prevProps), getMonitoredEditions(this.props)) ||
(prevProps.anyReleaseOk === false && this.props.anyReleaseOk === true)) { (prevProps.anyReleaseOk === false && anyReleaseOk === true)
) {
this.unpopulate();
this.populate();
}
// If the id has changed we need to clear the book
// files and fetch from the server.
if (prevProps.id !== id) {
this.unpopulate(); this.unpopulate();
this.populate(); this.populate();
} }
@@ -197,6 +222,8 @@ class BookDetailsConnector extends Component {
BookDetailsConnector.propTypes = { BookDetailsConnector.propTypes = {
id: PropTypes.number, id: PropTypes.number,
anyReleaseOk: PropTypes.bool, anyReleaseOk: PropTypes.bool,
isRenamingFiles: PropTypes.bool.isRequired,
isRenamingAuthor: PropTypes.bool.isRequired,
isBookFetching: PropTypes.bool, isBookFetching: PropTypes.bool,
isBookPopulated: PropTypes.bool, isBookPopulated: PropTypes.bool,
titleSlug: PropTypes.string.isRequired, titleSlug: PropTypes.string.isRequired,
@@ -117,8 +117,9 @@
} }
.title { .title {
font-weight: 300;
font-size: 30px; font-size: 30px;
line-height: 50px; line-height: 30px;
} }
} }
@@ -21,12 +21,7 @@ const defaultFontSize = parseInt(fonts.defaultFontSize);
const lineHeight = parseFloat(fonts.lineHeight); const lineHeight = parseFloat(fonts.lineHeight);
function getFanartUrl(images) { function getFanartUrl(images) {
const fanartImage = images.find((x) => x.coverType === 'fanart'); return images.find((x) => x.coverType === 'fanart')?.url;
if (fanartImage) {
// Remove protocol
return fanartImage.url.replace(/^https?:/, '');
}
} }
class BookDetailsHeader extends Component { class BookDetailsHeader extends Component {
@@ -16,8 +16,8 @@ import BookIndex from './BookIndex';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
createBookClientSideCollectionItemsSelector('bookIndex'), createBookClientSideCollectionItemsSelector('bookIndex'),
createCommandExecutingSelector(commandNames.REFRESH_AUTHOR), createCommandExecutingSelector(commandNames.BULK_REFRESH_AUTHOR),
createCommandExecutingSelector(commandNames.REFRESH_BOOK), createCommandExecutingSelector(commandNames.BULK_REFRESH_BOOK),
createCommandExecutingSelector(commandNames.RSS_SYNC), createCommandExecutingSelector(commandNames.RSS_SYNC),
createCommandExecutingSelector(commandNames.CUTOFF_UNMET_BOOK_SEARCH), createCommandExecutingSelector(commandNames.CUTOFF_UNMET_BOOK_SEARCH),
createCommandExecutingSelector(commandNames.MISSING_BOOK_SEARCH), createCommandExecutingSelector(commandNames.MISSING_BOOK_SEARCH),
@@ -229,7 +229,6 @@ class BookIndexRow extends Component {
className={styles[name]} className={styles[name]}
> >
{bookFileCount} {bookFileCount}
</VirtualTableRowCell> </VirtualTableRowCell>
); );
} }
@@ -0,0 +1,9 @@
.container {
border: 1px solid var(--borderColor);
border-radius: 4px;
background-color: var(--inputBackgroundColor);
&:last-of-type {
margin-bottom: 0;
}
}
@@ -0,0 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'container': string;
}
export const cssExports: CssExports;
export default cssExports;
@@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import BookFileEditorTableContentConnector from './BookFileEditorTableContentConnector'; import BookFileEditorTableContentConnector from './BookFileEditorTableContentConnector';
import styles from './BookFileEditorTable.css';
function BookFileEditorTable(props) { function BookFileEditorTable(props) {
const { const {
@@ -7,9 +8,11 @@ function BookFileEditorTable(props) {
} = props; } = props;
return ( return (
<BookFileEditorTableContentConnector <div className={styles.container}>
{...otherProps} <BookFileEditorTableContentConnector
/> {...otherProps}
/>
</div>
); );
} }
@@ -1,6 +1,6 @@
.filesTable { .filesTable {
margin-bottom: 20px; margin: 10px;
padding-top: 15px; padding-top: 5px;
border: 1px solid var(--borderColor); border: 1px solid var(--borderColor);
border-top: 1px solid var(--borderColor); border-top: 1px solid var(--borderColor);
border-radius: 4px; border-radius: 4px;
@@ -13,9 +13,15 @@
.actions { .actions {
display: flex; display: flex;
margin-right: auto; margin: 10px;
} }
.selectInput { .selectInput {
margin-left: 10px; margin-left: 10px;
} }
.blankpad {
padding-top: 10px;
padding-bottom: 10px;
padding-left: 2em;
}
@@ -2,6 +2,7 @@
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'actions': string; 'actions': string;
'blankpad': string;
'filesTable': string; 'filesTable': string;
'selectInput': string; 'selectInput': string;
} }
@@ -1,6 +1,7 @@
import _ from 'lodash'; import _ from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Alert from 'Components/Alert';
import SelectInput from 'Components/Form/SelectInput'; import SelectInput from 'Components/Form/SelectInput';
import SpinnerButton from 'Components/Link/SpinnerButton'; import SpinnerButton from 'Components/Link/SpinnerButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
@@ -120,7 +121,7 @@ class BookFileEditorTableContent extends Component {
const hasSelectedFiles = this.getSelectedIds().length > 0; const hasSelectedFiles = this.getSelectedIds().length > 0;
return ( return (
<> <div>
{ {
isFetching && !isPopulated ? isFetching && !isPopulated ?
<LoadingIndicator /> : <LoadingIndicator /> :
@@ -129,13 +130,13 @@ class BookFileEditorTableContent extends Component {
{ {
!isFetching && error ? !isFetching && error ?
<div>{error}</div> : <Alert kind={kinds.DANGER}>{error}</Alert> :
null null
} }
{ {
isPopulated && !items.length ? isPopulated && !items.length ?
<div> <div className={styles.blankpad}>
No book files to manage. No book files to manage.
</div> : </div> :
null null
@@ -173,26 +174,30 @@ class BookFileEditorTableContent extends Component {
null null
} }
<div className={styles.actions}> {
<SpinnerButton isPopulated && items.length ? (
kind={kinds.DANGER} <div className={styles.actions}>
isSpinning={isDeleting} <SpinnerButton
isDisabled={!hasSelectedFiles} kind={kinds.DANGER}
onPress={this.onDeletePress} isSpinning={isDeleting}
> isDisabled={!hasSelectedFiles}
Delete onPress={this.onDeletePress}
</SpinnerButton> >
{translate('Delete')}
</SpinnerButton>
<div className={styles.selectInput}> <div className={styles.selectInput}>
<SelectInput <SelectInput
name="quality" name="quality"
value="selectQuality" value="selectQuality"
values={qualityOptions} values={qualityOptions}
isDisabled={!hasSelectedFiles} isDisabled={!hasSelectedFiles}
onChange={this.onQualityChange} onChange={this.onQualityChange}
/> />
</div> </div>
</div> </div>
) : null
}
<ConfirmModal <ConfirmModal
isOpen={isConfirmDeleteModalOpen} isOpen={isConfirmDeleteModalOpen}
@@ -203,7 +208,7 @@ class BookFileEditorTableContent extends Component {
onConfirm={this.onConfirmDelete} onConfirm={this.onConfirmDelete}
onCancel={this.onConfirmDeleteModalClose} onCancel={this.onConfirmDeleteModalClose}
/> />
</> </div>
); );
} }
} }
+1 -1
View File
@@ -47,7 +47,7 @@ class CalendarConnector extends Component {
gotoCalendarToday gotoCalendarToday
} = this.props; } = this.props;
registerPagePopulator(this.repopulate); registerPagePopulator(this.repopulate, ['bookFileUpdated', 'bookFileDeleted']);
if (useCurrentPage) { if (useCurrentPage) {
fetchCalendar(); fetchCalendar();
@@ -1,9 +1,7 @@
.description {
line-height: $lineHeight;
}
.description { .description {
margin-left: 0; margin-left: 0;
line-height: $lineHeight;
overflow-wrap: break-word;
} }
@media (min-width: 768px) { @media (min-width: 768px) {
@@ -25,6 +25,10 @@
white-space: pre-wrap; white-space: pre-wrap;
} }
.version {
margin-top: 20px;
}
@media only screen and (max-width: $breakpointMedium) { @media only screen and (max-width: $breakpointMedium) {
.image { .image {
height: 250px; height: 250px;
@@ -6,6 +6,7 @@ interface CssExports {
'image': string; 'image': string;
'imageContainer': string; 'imageContainer': string;
'message': string; 'message': string;
'version': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
export default cssExports; export default cssExports;
@@ -1,60 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import styles from './ErrorBoundaryError.css';
function ErrorBoundaryError(props) {
const {
className,
messageClassName,
detailsClassName,
message,
error,
info
} = props;
return (
<div className={className}>
<div className={messageClassName}>
{message}
</div>
<div className={styles.imageContainer}>
<img
className={styles.image}
src={`${window.Readarr.urlBase}/Content/Images/error.png`}
/>
</div>
<details className={detailsClassName}>
{
error &&
<div>
{error.toString()}
</div>
}
<div className={styles.info}>
{info.componentStack}
</div>
</details>
</div>
);
}
ErrorBoundaryError.propTypes = {
className: PropTypes.string.isRequired,
messageClassName: PropTypes.string.isRequired,
detailsClassName: PropTypes.string.isRequired,
message: PropTypes.string.isRequired,
error: PropTypes.object.isRequired,
info: PropTypes.object.isRequired
};
ErrorBoundaryError.defaultProps = {
className: styles.container,
messageClassName: styles.message,
detailsClassName: styles.details,
message: 'There was an error loading this content'
};
export default ErrorBoundaryError;
@@ -0,0 +1,77 @@
import React, { useEffect, useState } from 'react';
import StackTrace from 'stacktrace-js';
import translate from 'Utilities/String/translate';
import styles from './ErrorBoundaryError.css';
interface ErrorBoundaryErrorProps {
className: string;
messageClassName: string;
detailsClassName: string;
message: string;
error: Error;
info: {
componentStack: string;
};
}
function ErrorBoundaryError(props: ErrorBoundaryErrorProps) {
const {
className = styles.container,
messageClassName = styles.message,
detailsClassName = styles.details,
message = translate('ErrorLoadingContent'),
error,
info,
} = props;
const [detailedError, setDetailedError] = useState<
StackTrace.StackFrame[] | null
>(null);
useEffect(() => {
if (error) {
StackTrace.fromError(error).then((de) => {
setDetailedError(de);
});
} else {
setDetailedError(null);
}
}, [error, setDetailedError]);
return (
<div className={className}>
<div className={messageClassName}>{message}</div>
<div className={styles.imageContainer}>
<img
className={styles.image}
src={`${window.Readarr.urlBase}/Content/Images/error.png`}
/>
</div>
<details className={detailsClassName}>
{error ? <div>{error.message}</div> : null}
{detailedError ? (
detailedError.map((d, index) => {
return (
<div key={index}>
{` at ${d.functionName} (${d.fileName}:${d.lineNumber}:${d.columnNumber})`}
</div>
);
})
) : (
<div>{info.componentStack}</div>
)}
{
<div className={styles.version}>
Version: {window.Readarr.version}
</div>
}
</details>
</div>
);
}
export default ErrorBoundaryError;
@@ -29,22 +29,24 @@ function CustomFiltersModalContent(props) {
<ModalBody> <ModalBody>
{ {
customFilters.map((customFilter) => { customFilters
return ( .sort((a, b) => a.label.localeCompare(b.label))
<CustomFilter .map((customFilter) => {
key={customFilter.id} return (
id={customFilter.id} <CustomFilter
label={customFilter.label} key={customFilter.id}
filters={customFilter.filters} id={customFilter.id}
selectedFilterKey={selectedFilterKey} label={customFilter.label}
isDeleting={isDeleting} filters={customFilter.filters}
deleteError={deleteError} selectedFilterKey={selectedFilterKey}
dispatchSetFilter={dispatchSetFilter} isDeleting={isDeleting}
dispatchDeleteCustomFilter={dispatchDeleteCustomFilter} deleteError={deleteError}
onEditPress={onEditCustomFilter} dispatchSetFilter={dispatchSetFilter}
/> dispatchDeleteCustomFilter={dispatchDeleteCustomFilter}
); onEditPress={onEditCustomFilter}
}) />
);
})
} }
<div className={styles.addButtonContainer}> <div className={styles.addButtonContainer}>
@@ -9,6 +9,10 @@
&:hover { &:hover {
background-color: var(--inputHoverBackgroundColor); background-color: var(--inputHoverBackgroundColor);
} }
&.isDisabled {
cursor: not-allowed;
}
} }
.optionCheck { .optionCheck {
@@ -273,6 +273,7 @@ FormInputGroup.propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
value: PropTypes.any, value: PropTypes.any,
values: PropTypes.arrayOf(PropTypes.any), values: PropTypes.arrayOf(PropTypes.any),
isDisabled: PropTypes.bool,
type: PropTypes.string.isRequired, type: PropTypes.string.isRequired,
kind: PropTypes.oneOf(kinds.all), kind: PropTypes.oneOf(kinds.all),
min: PropTypes.number, min: PropTypes.number,
+3 -1
View File
@@ -2,8 +2,10 @@
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
margin-right: $formLabelRightMarginWidth; margin-right: $formLabelRightMarginWidth;
padding-top: 8px;
min-height: 35px;
text-align: end;
font-weight: bold; font-weight: bold;
line-height: 35px;
} }
.hasError { .hasError {
@@ -39,18 +39,26 @@ class FilterMenuContent extends Component {
} }
{ {
customFilters.map((filter) => { customFilters.length > 0 ?
return ( <MenuItemSeparator /> :
<FilterMenuItem null
key={filter.id} }
filterKey={filter.id}
selectedFilterKey={selectedFilterKey} {
onPress={onFilterSelect} customFilters
> .sort((a, b) => a.label.localeCompare(b.label))
{filter.label} .map((filter) => {
</FilterMenuItem> return (
); <FilterMenuItem
}) key={filter.id}
filterKey={filter.id}
selectedFilterKey={selectedFilterKey}
onPress={onFilterSelect}
>
{filter.label}
</FilterMenuItem>
);
})
} }
{ {
@@ -202,6 +202,8 @@ class SignalRConnector extends Component {
this.props.dispatchUpdateItem({ section, ...body.resource }); this.props.dispatchUpdateItem({ section, ...body.resource });
} else if (body.action === 'deleted') { } else if (body.action === 'deleted') {
this.props.dispatchRemoveItem({ section, id: body.resource.id }); this.props.dispatchRemoveItem({ section, id: body.resource.id });
repopulatePage('bookFileDeleted');
} }
// Repopulate the page to handle recently imported file // Repopulate the page to handle recently imported file
@@ -15,5 +15,5 @@
"start_url": "../../../../", "start_url": "../../../../",
"theme_color": "#3a3f51", "theme_color": "#3a3f51",
"background_color": "#3a3f51", "background_color": "#3a3f51",
"display": "standalone" "display": "minimal-ui"
} }
+120
View File
@@ -0,0 +1,120 @@
import createAjaxRequest from 'Utilities/createAjaxRequest';
// This file contains some helpers for power users in a browser console
let hasWarned = false;
function checkActivationWarning() {
if (!hasWarned) {
console.log('Activated ReadarrApi console helpers.');
console.warn('Be warned: There will be no further confirmation checks.');
hasWarned = true;
}
}
function attachAsyncActions(promise) {
promise.filter = function() {
const args = arguments;
const res = this.then((d) => d.filter(...args));
attachAsyncActions(res);
return res;
};
promise.map = function() {
const args = arguments;
const res = this.then((d) => d.map(...args));
attachAsyncActions(res);
return res;
};
promise.all = function() {
const res = this.then((d) => Promise.all(d));
attachAsyncActions(res);
return res;
};
promise.forEach = function(action) {
const res = this.then((d) => Promise.all(d.map(action)));
attachAsyncActions(res);
return res;
};
}
class ResourceApi {
constructor(api, url) {
this.api = api;
this.url = url;
}
single(id) {
return this.api.fetch(`${this.url}/${id}`);
}
all() {
return this.api.fetch(this.url);
}
filter(pred) {
return this.all().filter(pred);
}
update(resource) {
return this.api.fetch(`${this.url}/${resource.id}`, { method: 'PUT', data: resource });
}
delete(resource) {
if (typeof resource === 'object' && resource !== null && resource.id) {
resource = resource.id;
}
if (!resource || !Number.isInteger(resource)) {
throw Error('Invalid resource', resource);
}
return this.api.fetch(`${this.url}/${resource}`, { method: 'DELETE' });
}
fetch(url, options) {
return this.api.fetch(`${this.url}${url}`, options);
}
}
class ConsoleApi {
constructor() {
this.author = new ResourceApi(this, '/author');
}
resource(url) {
return new ResourceApi(this, url);
}
fetch(url, options) {
checkActivationWarning();
options = options || {};
const req = {
url,
method: options.method || 'GET'
};
if (options.data) {
req.dataType = 'json';
req.data = JSON.stringify(options.data);
}
const promise = createAjaxRequest(req).request;
promise.fail((xhr) => {
console.error(`Failed to fetch ${url}`, xhr);
});
attachAsyncActions(promise);
return promise;
}
}
window.ReadarrApi = new ConsoleApi();
export default ConsoleApi;
@@ -28,6 +28,10 @@
text-align: center; text-align: center;
} }
.quality {
white-space: nowrap;
}
.customFormatScore { .customFormatScore {
composes: cell from '~Components/Table/Cells/TableRowCell.css'; composes: cell from '~Components/Table/Cells/TableRowCell.css';
@@ -178,7 +178,7 @@ class InteractiveSearchRow extends Component {
</TableRowCell> </TableRowCell>
<TableRowCell className={styles.quality}> <TableRowCell className={styles.quality}>
<BookQuality quality={quality} /> <BookQuality quality={quality} showRevision={true} />
</TableRowCell> </TableRowCell>
<TableRowCell className={styles.customFormatScore}> <TableRowCell className={styles.customFormatScore}>
@@ -49,7 +49,7 @@ function EditSpecificationModalContent(props) {
{...otherProps} {...otherProps}
> >
{ {
fields && fields.some((x) => x.label === 'Regular Expression') && fields && fields.some((x) => x.label === translate('CustomFormatsSpecificationRegularExpression')) &&
<Alert kind={kinds.INFO}> <Alert kind={kinds.INFO}>
<div> <div>
<div dangerouslySetInnerHTML={{ __html: 'This condition matches using Regular Expressions. Note that the characters <code>\\^$.|?*+()[{</code> have special meanings and need escaping with a <code>\\</code>' }} /> <div dangerouslySetInnerHTML={{ __html: 'This condition matches using Regular Expressions. Note that the characters <code>\\^$.|?*+()[{</code> have special meanings and need escaping with a <code>\\</code>' }} />
@@ -14,9 +14,11 @@ import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody'; import TableBody from 'Components/Table/TableBody';
import useSelectState from 'Helpers/Hooks/useSelectState'; import useSelectState from 'Helpers/Hooks/useSelectState';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import SortDirection from 'Helpers/Props/SortDirection';
import { import {
bulkDeleteDownloadClients, bulkDeleteDownloadClients,
bulkEditDownloadClients, bulkEditDownloadClients,
setManageDownloadClientsSort,
} from 'Store/Actions/settingsActions'; } from 'Store/Actions/settingsActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import { SelectStateInputProps } from 'typings/props'; import { SelectStateInputProps } from 'typings/props';
@@ -80,6 +82,8 @@ const COLUMNS = [
interface ManageDownloadClientsModalContentProps { interface ManageDownloadClientsModalContentProps {
onModalClose(): void; onModalClose(): void;
sortKey?: string;
sortDirection?: SortDirection;
} }
function ManageDownloadClientsModalContent( function ManageDownloadClientsModalContent(
@@ -94,6 +98,8 @@ function ManageDownloadClientsModalContent(
isSaving, isSaving,
error, error,
items, items,
sortKey,
sortDirection,
}: DownloadClientAppState = useSelector( }: DownloadClientAppState = useSelector(
createClientSideCollectionSelector('settings.downloadClients') createClientSideCollectionSelector('settings.downloadClients')
); );
@@ -114,6 +120,13 @@ function ManageDownloadClientsModalContent(
const selectedCount = selectedIds.length; const selectedCount = selectedIds.length;
const onSortPress = useCallback(
(value: string) => {
dispatch(setManageDownloadClientsSort({ sortKey: value }));
},
[dispatch]
);
const onDeletePress = useCallback(() => { const onDeletePress = useCallback(() => {
setIsDeleteModalOpen(true); setIsDeleteModalOpen(true);
}, [setIsDeleteModalOpen]); }, [setIsDeleteModalOpen]);
@@ -219,6 +232,9 @@ function ManageDownloadClientsModalContent(
allSelected={allSelected} allSelected={allSelected}
allUnselected={allUnselected} allUnselected={allUnselected}
onSelectAllChange={onSelectAllChange} onSelectAllChange={onSelectAllChange}
sortKey={sortKey}
sortDirection={sortDirection}
onSortPress={onSortPress}
> >
<TableBody> <TableBody>
{items.map((item) => { {items.map((item) => {
@@ -61,8 +61,12 @@ function DownloadClientOptions(props) {
legend={translate('FailedDownloadHandling')} legend={translate('FailedDownloadHandling')}
> >
<Form> <Form>
<FormGroup size={sizes.MEDIUM}> <FormGroup
<FormLabel>{translate('RedownloadFailed')}</FormLabel> advancedSettings={advancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('AutoRedownloadFailed')}</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.CHECK} type={inputTypes.CHECK}
@@ -72,7 +76,28 @@ function DownloadClientOptions(props) {
{...settings.autoRedownloadFailed} {...settings.autoRedownloadFailed}
/> />
</FormGroup> </FormGroup>
{
settings.autoRedownloadFailed.value ?
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('AutoRedownloadFailedFromInteractiveSearch')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="autoRedownloadFailedFromInteractiveSearch"
helpText={translate('AutoRedownloadFailedFromInteractiveSearchHelpText')}
onChange={onInputChange}
{...settings.autoRedownloadFailedFromInteractiveSearch}
/>
</FormGroup> :
null
}
</Form> </Form>
<Alert kind={kinds.INFO}> <Alert kind={kinds.INFO}>
{translate('RemoveDownloadsAlert')} {translate('RemoveDownloadsAlert')}
</Alert> </Alert>
@@ -8,11 +8,13 @@
} }
.name { .name {
flex: 1 0 300px; @add-mixin truncate;
flex: 0 1 600px;
} }
.foreignId { .foreignId {
flex: 0 0 200px; flex: 0 0 100px;
} }
.actions { .actions {
@@ -4,12 +4,12 @@
font-weight: bold; font-weight: bold;
} }
.foreignId { .name {
flex: 0 0 200px; flex: 0 1 600px;
} }
.name { .foreignId {
flex: 1 0 300px; flex: 0 0 100px;
} }
.addImportListExclusion { .addImportListExclusion {
@@ -14,9 +14,11 @@ import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody'; import TableBody from 'Components/Table/TableBody';
import useSelectState from 'Helpers/Hooks/useSelectState'; import useSelectState from 'Helpers/Hooks/useSelectState';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import SortDirection from 'Helpers/Props/SortDirection';
import { import {
bulkDeleteIndexers, bulkDeleteIndexers,
bulkEditIndexers, bulkEditIndexers,
setManageIndexersSort,
} from 'Store/Actions/settingsActions'; } from 'Store/Actions/settingsActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import { SelectStateInputProps } from 'typings/props'; import { SelectStateInputProps } from 'typings/props';
@@ -80,6 +82,8 @@ const COLUMNS = [
interface ManageIndexersModalContentProps { interface ManageIndexersModalContentProps {
onModalClose(): void; onModalClose(): void;
sortKey?: string;
sortDirection?: SortDirection;
} }
function ManageIndexersModalContent(props: ManageIndexersModalContentProps) { function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
@@ -92,6 +96,8 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
isSaving, isSaving,
error, error,
items, items,
sortKey,
sortDirection,
}: IndexerAppState = useSelector( }: IndexerAppState = useSelector(
createClientSideCollectionSelector('settings.indexers') createClientSideCollectionSelector('settings.indexers')
); );
@@ -112,6 +118,13 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
const selectedCount = selectedIds.length; const selectedCount = selectedIds.length;
const onSortPress = useCallback(
(value: string) => {
dispatch(setManageIndexersSort({ sortKey: value }));
},
[dispatch]
);
const onDeletePress = useCallback(() => { const onDeletePress = useCallback(() => {
setIsDeleteModalOpen(true); setIsDeleteModalOpen(true);
}, [setIsDeleteModalOpen]); }, [setIsDeleteModalOpen]);
@@ -214,6 +227,9 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
allSelected={allSelected} allSelected={allSelected}
allUnselected={allUnselected} allUnselected={allUnselected}
onSelectAllChange={onSelectAllChange} onSelectAllChange={onSelectAllChange}
sortKey={sortKey}
sortDirection={sortDirection}
onSortPress={onSortPress}
> >
<TableBody> <TableBody>
{items.map((item) => { {items.map((item) => {
@@ -212,26 +212,24 @@ class MediaManagement extends Component {
</FormGroup> </FormGroup>
{ {
settings.importExtraFiles.value && settings.importExtraFiles.value ?
<FormGroup <FormGroup
advancedSettings={advancedSettings} advancedSettings={advancedSettings}
isAdvanced={true} isAdvanced={true}
> >
<FormLabel> <FormLabel>{translate('ImportExtraFiles')}</FormLabel>
{translate('ImportExtraFiles')}
</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.TEXT} type={inputTypes.TEXT}
name="extraFileExtensions" name="extraFileExtensions"
helpTexts={[ helpTexts={[
translate('ExtraFileExtensionsHelpTexts1'), translate('ExtraFileExtensionsHelpText'),
translate('ExtraFileExtensionsHelpTexts2') translate('ExtraFileExtensionsHelpTextsExamples')
]} ]}
onChange={onInputChange} onChange={onInputChange}
{...settings.extraFileExtensions} {...settings.extraFileExtensions}
/> />
</FormGroup> </FormGroup> : null
} }
</FieldSet> </FieldSet>
} }
@@ -60,6 +60,7 @@ class Notification extends Component {
onReleaseImport, onReleaseImport,
onUpgrade, onUpgrade,
onRename, onRename,
onAuthorAdded,
onAuthorDelete, onAuthorDelete,
onBookDelete, onBookDelete,
onBookFileDelete, onBookFileDelete,
@@ -73,6 +74,7 @@ class Notification extends Component {
supportsOnReleaseImport, supportsOnReleaseImport,
supportsOnUpgrade, supportsOnUpgrade,
supportsOnRename, supportsOnRename,
supportsOnAuthorAdded,
supportsOnAuthorDelete, supportsOnAuthorDelete,
supportsOnBookDelete, supportsOnBookDelete,
supportsOnBookFileDelete, supportsOnBookFileDelete,
@@ -136,6 +138,14 @@ class Notification extends Component {
null null
} }
{
supportsOnAuthorAdded && onAuthorAdded ?
<Label kind={kinds.SUCCESS}>
{translate('OnAuthorAdded')}
</Label> :
null
}
{ {
supportsOnAuthorDelete && onAuthorDelete ? supportsOnAuthorDelete && onAuthorDelete ?
<Label kind={kinds.SUCCESS}> <Label kind={kinds.SUCCESS}>
@@ -244,6 +254,7 @@ Notification.propTypes = {
onReleaseImport: PropTypes.bool.isRequired, onReleaseImport: PropTypes.bool.isRequired,
onUpgrade: PropTypes.bool.isRequired, onUpgrade: PropTypes.bool.isRequired,
onRename: PropTypes.bool.isRequired, onRename: PropTypes.bool.isRequired,
onAuthorAdded: PropTypes.bool.isRequired,
onAuthorDelete: PropTypes.bool.isRequired, onAuthorDelete: PropTypes.bool.isRequired,
onBookDelete: PropTypes.bool.isRequired, onBookDelete: PropTypes.bool.isRequired,
onBookFileDelete: PropTypes.bool.isRequired, onBookFileDelete: PropTypes.bool.isRequired,
@@ -257,6 +268,7 @@ Notification.propTypes = {
supportsOnReleaseImport: PropTypes.bool.isRequired, supportsOnReleaseImport: PropTypes.bool.isRequired,
supportsOnUpgrade: PropTypes.bool.isRequired, supportsOnUpgrade: PropTypes.bool.isRequired,
supportsOnRename: PropTypes.bool.isRequired, supportsOnRename: PropTypes.bool.isRequired,
supportsOnAuthorAdded: PropTypes.bool.isRequired,
supportsOnAuthorDelete: PropTypes.bool.isRequired, supportsOnAuthorDelete: PropTypes.bool.isRequired,
supportsOnBookDelete: PropTypes.bool.isRequired, supportsOnBookDelete: PropTypes.bool.isRequired,
supportsOnBookFileDelete: PropTypes.bool.isRequired, supportsOnBookFileDelete: PropTypes.bool.isRequired,
@@ -19,6 +19,7 @@ function NotificationEventItems(props) {
onReleaseImport, onReleaseImport,
onUpgrade, onUpgrade,
onRename, onRename,
onAuthorAdded,
onAuthorDelete, onAuthorDelete,
onBookDelete, onBookDelete,
onBookFileDelete, onBookFileDelete,
@@ -32,6 +33,7 @@ function NotificationEventItems(props) {
supportsOnReleaseImport, supportsOnReleaseImport,
supportsOnUpgrade, supportsOnUpgrade,
supportsOnRename, supportsOnRename,
supportsOnAuthorAdded,
supportsOnAuthorDelete, supportsOnAuthorDelete,
supportsOnBookDelete, supportsOnBookDelete,
supportsOnBookFileDelete, supportsOnBookFileDelete,
@@ -123,6 +125,17 @@ function NotificationEventItems(props) {
/> />
</div> </div>
<div>
<FormInputGroup
type={inputTypes.CHECK}
name="onAuthorAdded"
helpText={translate('OnAuthorAddedHelpText')}
isDisabled={!supportsOnAuthorAdded.value}
{...onAuthorAdded}
onChange={onInputChange}
/>
</div>
<div> <div>
<FormInputGroup <FormInputGroup
type={inputTypes.CHECK} type={inputTypes.CHECK}
+7
View File
@@ -223,6 +223,13 @@ class UISettings extends Component {
helpTextWarning={translate('UILanguageHelpTextWarning')} helpTextWarning={translate('UILanguageHelpTextWarning')}
onChange={onInputChange} onChange={onInputChange}
{...settings.uiLanguage} {...settings.uiLanguage}
errors={
languages.some((language) => language.key === settings.uiLanguage.value) ?
settings.uiLanguage.errors :
[
...settings.uiLanguage.errors,
{ message: translate('InvalidUILanguage') }
]}
/> />
</FormGroup> </FormGroup>
</FieldSet> </FieldSet>
@@ -1,4 +1,5 @@
import { createAction } from 'redux-actions'; import { createAction } from 'redux-actions';
import { sortDirections } from 'Helpers/Props';
import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler'; import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler';
import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler'; import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler';
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; 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 createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler'; import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler';
import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler'; 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 createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import { createThunk } from 'Store/thunks'; import { createThunk } from 'Store/thunks';
@@ -33,6 +35,7 @@ export const CANCEL_TEST_DOWNLOAD_CLIENT = 'settings/downloadClients/cancelTestD
export const TEST_ALL_DOWNLOAD_CLIENTS = 'settings/downloadClients/testAllDownloadClients'; export const TEST_ALL_DOWNLOAD_CLIENTS = 'settings/downloadClients/testAllDownloadClients';
export const BULK_EDIT_DOWNLOAD_CLIENTS = 'settings/downloadClients/bulkEditDownloadClients'; export const BULK_EDIT_DOWNLOAD_CLIENTS = 'settings/downloadClients/bulkEditDownloadClients';
export const BULK_DELETE_DOWNLOAD_CLIENTS = 'settings/downloadClients/bulkDeleteDownloadClients'; export const BULK_DELETE_DOWNLOAD_CLIENTS = 'settings/downloadClients/bulkDeleteDownloadClients';
export const SET_MANAGE_DOWNLOAD_CLIENTS_SORT = 'settings/downloadClients/setManageDownloadClientsSort';
// //
// Action Creators // Action Creators
@@ -49,6 +52,7 @@ export const cancelTestDownloadClient = createThunk(CANCEL_TEST_DOWNLOAD_CLIENT)
export const testAllDownloadClients = createThunk(TEST_ALL_DOWNLOAD_CLIENTS); export const testAllDownloadClients = createThunk(TEST_ALL_DOWNLOAD_CLIENTS);
export const bulkEditDownloadClients = createThunk(BULK_EDIT_DOWNLOAD_CLIENTS); export const bulkEditDownloadClients = createThunk(BULK_EDIT_DOWNLOAD_CLIENTS);
export const bulkDeleteDownloadClients = createThunk(BULK_DELETE_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) => { export const setDownloadClientValue = createAction(SET_DOWNLOAD_CLIENT_VALUE, (payload) => {
return { return {
@@ -88,7 +92,14 @@ export default {
isTesting: false, isTesting: false,
isTestingAll: false, isTestingAll: false,
items: [], items: [],
pendingChanges: {} pendingChanges: {},
sortKey: 'name',
sortDirection: sortDirections.ASCENDING,
sortPredicates: {
name: function(item) {
return item.name.toLowerCase();
}
}
}, },
// //
@@ -121,7 +132,10 @@ export default {
return selectedSchema; return selectedSchema;
}); });
} },
[SET_MANAGE_DOWNLOAD_CLIENTS_SORT]: createSetClientSideCollectionSortReducer(section)
} }
}; };
@@ -1,4 +1,5 @@
import { createAction } from 'redux-actions'; import { createAction } from 'redux-actions';
import { sortDirections } from 'Helpers/Props';
import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler'; import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler';
import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler'; import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler';
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; 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 createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler'; import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler';
import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler'; 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 createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import { createThunk } from 'Store/thunks'; import { createThunk } from 'Store/thunks';
@@ -36,6 +38,7 @@ export const CANCEL_TEST_INDEXER = 'settings/indexers/cancelTestIndexer';
export const TEST_ALL_INDEXERS = 'settings/indexers/testAllIndexers'; export const TEST_ALL_INDEXERS = 'settings/indexers/testAllIndexers';
export const BULK_EDIT_INDEXERS = 'settings/indexers/bulkEditIndexers'; export const BULK_EDIT_INDEXERS = 'settings/indexers/bulkEditIndexers';
export const BULK_DELETE_INDEXERS = 'settings/indexers/bulkDeleteIndexers'; export const BULK_DELETE_INDEXERS = 'settings/indexers/bulkDeleteIndexers';
export const SET_MANAGE_INDEXERS_SORT = 'settings/indexers/setManageIndexersSort';
// //
// Action Creators // Action Creators
@@ -53,6 +56,7 @@ export const cancelTestIndexer = createThunk(CANCEL_TEST_INDEXER);
export const testAllIndexers = createThunk(TEST_ALL_INDEXERS); export const testAllIndexers = createThunk(TEST_ALL_INDEXERS);
export const bulkEditIndexers = createThunk(BULK_EDIT_INDEXERS); export const bulkEditIndexers = createThunk(BULK_EDIT_INDEXERS);
export const bulkDeleteIndexers = createThunk(BULK_DELETE_INDEXERS); export const bulkDeleteIndexers = createThunk(BULK_DELETE_INDEXERS);
export const setManageIndexersSort = createAction(SET_MANAGE_INDEXERS_SORT);
export const setIndexerValue = createAction(SET_INDEXER_VALUE, (payload) => { export const setIndexerValue = createAction(SET_INDEXER_VALUE, (payload) => {
return { return {
@@ -92,7 +96,14 @@ export default {
isTesting: false, isTesting: false,
isTestingAll: false, isTestingAll: false,
items: [], items: [],
pendingChanges: {} pendingChanges: {},
sortKey: 'name',
sortDirection: sortDirections.ASCENDING,
sortPredicates: {
name: function(item) {
return item.name.toLowerCase();
}
}
}, },
// //
@@ -140,7 +151,13 @@ export default {
delete selectedSchema.name; delete selectedSchema.name;
selectedSchema.fields = selectedSchema.fields.map((field) => { 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; newState.selectedSchema = selectedSchema;
@@ -151,7 +168,10 @@ export default {
}; };
return updateSectionState(state, section, newState); return updateSectionState(state, section, newState);
} },
[SET_MANAGE_INDEXERS_SORT]: createSetClientSideCollectionSortReducer(section)
} }
}; };
@@ -106,6 +106,7 @@ export default {
selectedSchema.onReleaseImport = selectedSchema.supportsOnReleaseImport; selectedSchema.onReleaseImport = selectedSchema.supportsOnReleaseImport;
selectedSchema.onUpgrade = selectedSchema.supportsOnUpgrade; selectedSchema.onUpgrade = selectedSchema.supportsOnUpgrade;
selectedSchema.onRename = selectedSchema.supportsOnRename; selectedSchema.onRename = selectedSchema.supportsOnRename;
selectedSchema.onAuthorAdded = selectedSchema.supportsOnAuthorAdded;
selectedSchema.onAuthorDelete = selectedSchema.supportsOnAuthorDelete; selectedSchema.onAuthorDelete = selectedSchema.supportsOnAuthorDelete;
selectedSchema.onBookDelete = selectedSchema.supportsOnBookDelete; selectedSchema.onBookDelete = selectedSchema.supportsOnBookDelete;
selectedSchema.onBookFileDelete = selectedSchema.supportsOnBookFileDelete; selectedSchema.onBookFileDelete = selectedSchema.supportsOnBookFileDelete;
@@ -41,6 +41,14 @@ export const defaultState = {
}, },
columns: [ columns: [
{
name: 'select',
columnLabel: 'Select',
isSortable: false,
isVisible: true,
isModifiable: false,
isHidden: true
},
{ {
name: 'path', name: 'path',
label: 'Path', label: 'Path',
@@ -158,7 +158,7 @@ export const defaultState = {
bookFileCount: function(item) { bookFileCount: function(item) {
const { statistics = {} } = item; const { statistics = {} } = item;
return statistics.bookCount || 0; return statistics.bookFileCount || 0;
}, },
ratings: function(item) { ratings: function(item) {
@@ -84,11 +84,6 @@ export const defaultState = {
label: 'Source Title', label: 'Source Title',
isVisible: false isVisible: false
}, },
{
name: 'sourceTitle',
label: 'Source Title',
isVisible: false
},
{ {
name: 'customFormatScore', name: 'customFormatScore',
columnLabel: 'Custom Format Score', columnLabel: 'Custom Format Score',
@@ -5,6 +5,7 @@ import { sortDirections } from 'Helpers/Props';
import { createThunk, handleThunks } from 'Store/thunks'; import { createThunk, handleThunks } from 'Store/thunks';
import createAjaxRequest from 'Utilities/createAjaxRequest'; import createAjaxRequest from 'Utilities/createAjaxRequest';
import updateSectionState from 'Utilities/State/updateSectionState'; import updateSectionState from 'Utilities/State/updateSectionState';
import naturalExpansion from 'Utilities/String/naturalExpansion';
import { set, update, updateItem } from './baseActions'; import { set, update, updateItem } from './baseActions';
import createFetchHandler from './Creators/createFetchHandler'; import createFetchHandler from './Creators/createFetchHandler';
import createHandleActions from './Creators/createHandleActions'; import createHandleActions from './Creators/createHandleActions';
@@ -17,6 +18,7 @@ export const section = 'interactiveImport';
const booksSection = `${section}.books`; const booksSection = `${section}.books`;
const bookFilesSection = `${section}.bookFiles`; const bookFilesSection = `${section}.bookFiles`;
let abortCurrentFetchRequest = null;
let abortCurrentRequest = null; let abortCurrentRequest = null;
let currentIds = []; let currentIds = [];
@@ -32,15 +34,17 @@ export const defaultState = {
error: null, error: null,
items: [], items: [],
pendingChanges: {}, pendingChanges: {},
sortKey: 'quality', sortKey: 'path',
sortDirection: sortDirections.DESCENDING, sortDirection: sortDirections.ASCENDING,
secondarySortKey: 'path',
secondarySortDirection: sortDirections.ASCENDING,
recentFolders: [], recentFolders: [],
importMode: 'chooseImportMode', importMode: 'chooseImportMode',
sortPredicates: { sortPredicates: {
path: function(item, direction) { path: function(item, direction) {
const path = item.path; const path = item.path;
return path.toLowerCase(); return naturalExpansion(path.toLowerCase());
}, },
author: function(item, direction) { author: function(item, direction) {
@@ -74,6 +78,8 @@ export const defaultState = {
}; };
export const persistState = [ export const persistState = [
'interactiveImport.sortKey',
'interactiveImport.sortDirection',
'interactiveImport.recentFolders', 'interactiveImport.recentFolders',
'interactiveImport.importMode' 'interactiveImport.importMode'
]; ];
@@ -122,6 +128,11 @@ export const clearInteractiveImportBookFiles = createAction(CLEAR_INTERACTIVE_IM
// Action Handlers // Action Handlers
export const actionHandlers = handleThunks({ export const actionHandlers = handleThunks({
[FETCH_INTERACTIVE_IMPORT_ITEMS]: function(getState, payload, dispatch) { [FETCH_INTERACTIVE_IMPORT_ITEMS]: function(getState, payload, dispatch) {
if (abortCurrentFetchRequest) {
abortCurrentFetchRequest();
abortCurrentFetchRequest = null;
}
if (!payload.downloadId && !payload.folder) { if (!payload.downloadId && !payload.folder) {
dispatch(set({ section, error: { message: '`downloadId` or `folder` is required.' } })); dispatch(set({ section, error: { message: '`downloadId` or `folder` is required.' } }));
return; return;
@@ -129,12 +140,14 @@ export const actionHandlers = handleThunks({
dispatch(set({ section, isFetching: true })); dispatch(set({ section, isFetching: true }));
const promise = createAjaxRequest({ const { request, abortRequest } = createAjaxRequest({
url: '/manualimport', url: '/manualimport',
data: payload data: payload
}).request; });
promise.done((data) => { abortCurrentFetchRequest = abortRequest;
request.done((data) => {
dispatch(batchActions([ dispatch(batchActions([
update({ section, data }), update({ section, data }),
@@ -147,7 +160,11 @@ export const actionHandlers = handleThunks({
])); ]));
}); });
promise.fail((xhr) => { request.fail((xhr) => {
if (xhr.aborted) {
return;
}
dispatch(set({ dispatch(set({
section, section,
isFetching: false, isFetching: false,
+6 -4
View File
@@ -371,13 +371,14 @@ export const actionHandlers = handleThunks({
id, id,
remove, remove,
blocklist, blocklist,
skipRedownload skipRedownload,
changeCategory
} = payload; } = payload;
dispatch(updateItem({ section: paged, id, isRemoving: true })); dispatch(updateItem({ section: paged, id, isRemoving: true }));
const promise = createAjaxRequest({ const promise = createAjaxRequest({
url: `/queue/${id}?removeFromClient=${remove}&blocklist=${blocklist}&skipRedownload=${skipRedownload}`, url: `/queue/${id}?removeFromClient=${remove}&blocklist=${blocklist}&skipRedownload=${skipRedownload}&changeCategory=${changeCategory}`,
method: 'DELETE' method: 'DELETE'
}).request; }).request;
@@ -395,7 +396,8 @@ export const actionHandlers = handleThunks({
ids, ids,
remove, remove,
blocklist, blocklist,
skipRedownload skipRedownload,
changeCategory
} = payload; } = payload;
dispatch(batchActions([ dispatch(batchActions([
@@ -411,7 +413,7 @@ export const actionHandlers = handleThunks({
])); ]));
const promise = createAjaxRequest({ const promise = createAjaxRequest({
url: `/queue/bulk?removeFromClient=${remove}&blocklist=${blocklist}&skipRedownload=${skipRedownload}`, url: `/queue/bulk?removeFromClient=${remove}&blocklist=${blocklist}&skipRedownload=${skipRedownload}&changeCategory=${changeCategory}`,
method: 'DELETE', method: 'DELETE',
dataType: 'json', dataType: 'json',
data: JSON.stringify({ ids }) data: JSON.stringify({ ids })
+6 -6
View File
@@ -22,9 +22,9 @@ class About extends Component {
isNetCore, isNetCore,
isDocker, isDocker,
runtimeVersion, runtimeVersion,
migrationVersion,
databaseVersion, databaseVersion,
databaseType, databaseType,
migrationVersion,
appData, appData,
startupPath, startupPath,
mode, mode,
@@ -66,13 +66,13 @@ class About extends Component {
} }
<DescriptionListItem <DescriptionListItem
title={translate('DBMigration')} title={translate('Database')}
data={migrationVersion} data={`${titleCase(databaseType)} ${databaseVersion}`}
/> />
<DescriptionListItem <DescriptionListItem
title={translate('Database')} title={translate('DatabaseMigration')}
data={`${titleCase(databaseType)} ${databaseVersion}`} data={migrationVersion}
/> />
<DescriptionListItem <DescriptionListItem
@@ -114,9 +114,9 @@ About.propTypes = {
isNetCore: PropTypes.bool.isRequired, isNetCore: PropTypes.bool.isRequired,
runtimeVersion: PropTypes.string.isRequired, runtimeVersion: PropTypes.string.isRequired,
isDocker: PropTypes.bool.isRequired, isDocker: PropTypes.bool.isRequired,
migrationVersion: PropTypes.number.isRequired,
databaseType: PropTypes.string.isRequired, databaseType: PropTypes.string.isRequired,
databaseVersion: PropTypes.string.isRequired, databaseVersion: PropTypes.string.isRequired,
migrationVersion: PropTypes.number.isRequired,
appData: PropTypes.string.isRequired, appData: PropTypes.string.isRequired,
startupPath: PropTypes.string.isRequired, startupPath: PropTypes.string.isRequired,
mode: PropTypes.string.isRequired, mode: PropTypes.string.isRequired,
@@ -71,6 +71,7 @@ function getInternalLink(source) {
function getTestLink(source, props) { function getTestLink(source, props) {
switch (source) { switch (source) {
case 'IndexerStatusCheck': case 'IndexerStatusCheck':
case 'IndexerLongTermStatusCheck':
return ( return (
<SpinnerIconButton <SpinnerIconButton
name={icons.TEST} name={icons.TEST}
@@ -1,5 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent'; import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody'; import PageContentBody from 'Components/Page/PageContentBody';
@@ -9,8 +10,12 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper'; import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import VirtualTable from 'Components/Table/VirtualTable'; import VirtualTable from 'Components/Table/VirtualTable';
import VirtualTableRow from 'Components/Table/VirtualTableRow'; import VirtualTableRow from 'Components/Table/VirtualTableRow';
import { align, icons, sortDirections } from 'Helpers/Props'; import { align, icons, kinds, sortDirections } from 'Helpers/Props';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import UnmappedFilesTableHeader from './UnmappedFilesTableHeader'; import UnmappedFilesTableHeader from './UnmappedFilesTableHeader';
import UnmappedFilesTableRow from './UnmappedFilesTableRow'; import UnmappedFilesTableRow from './UnmappedFilesTableRow';
@@ -23,10 +28,43 @@ class UnmappedFilesTable extends Component {
super(props, context); super(props, context);
this.state = { this.state = {
scroller: null scroller: null,
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {}
}; };
} }
componentDidMount() {
this.setSelectedState();
}
componentDidUpdate(prevProps) {
const {
items,
sortKey,
sortDirection,
isDeleting,
deleteError
} = this.props;
if (sortKey !== prevProps.sortKey ||
sortDirection !== prevProps.sortDirection ||
hasDifferentItemsOrOrder(prevProps.items, items)
) {
this.setSelectedState();
}
const hasFinishedDeleting = prevProps.isDeleting &&
!isDeleting &&
!deleteError;
if (hasFinishedDeleting) {
this.onSelectAllChange({ value: false });
}
}
// //
// Control // Control
@@ -34,6 +72,68 @@ class UnmappedFilesTable extends Component {
this.setState({ scroller: ref }); this.setState({ scroller: ref });
}; };
getSelectedIds = () => {
if (this.state.allUnselected) {
return [];
}
return getSelectedIds(this.state.selectedState);
};
setSelectedState() {
const {
items
} = this.props;
const {
selectedState
} = this.state;
const newSelectedState = {};
items.forEach((file) => {
const isItemSelected = selectedState[file.id];
if (isItemSelected) {
newSelectedState[file.id] = isItemSelected;
} else {
newSelectedState[file.id] = false;
}
});
const selectedCount = getSelectedIds(newSelectedState).length;
const newStateCount = Object.keys(newSelectedState).length;
let isAllSelected = false;
let isAllUnselected = false;
if (selectedCount === 0) {
isAllUnselected = true;
} else if (selectedCount === newStateCount) {
isAllSelected = true;
}
this.setState({ selectedState: newSelectedState, allSelected: isAllSelected, allUnselected: isAllUnselected });
}
onSelectAllChange = ({ value }) => {
this.setState(selectAll(this.state.selectedState, value));
};
onSelectAllPress = () => {
this.onSelectAllChange({ value: !this.state.allSelected });
};
onSelectedChange = ({ id, value, shiftKey = false }) => {
this.setState((state) => {
return toggleSelected(state, this.props.items, id, value, shiftKey);
});
};
onDeleteUnmappedFilesPress = () => {
const selectedIds = this.getSelectedIds();
this.props.deleteUnmappedFiles(selectedIds);
};
rowRenderer = ({ key, rowIndex, style }) => { rowRenderer = ({ key, rowIndex, style }) => {
const { const {
items, items,
@@ -41,6 +141,10 @@ class UnmappedFilesTable extends Component {
deleteUnmappedFile deleteUnmappedFile
} = this.props; } = this.props;
const {
selectedState
} = this.state;
const item = items[rowIndex]; const item = items[rowIndex];
return ( return (
@@ -51,6 +155,8 @@ class UnmappedFilesTable extends Component {
<UnmappedFilesTableRow <UnmappedFilesTableRow
key={item.id} key={item.id}
columns={columns} columns={columns}
isSelected={selectedState[item.id]}
onSelectedChange={this.onSelectedChange}
deleteUnmappedFile={deleteUnmappedFile} deleteUnmappedFile={deleteUnmappedFile}
{...item} {...item}
/> />
@@ -63,6 +169,7 @@ class UnmappedFilesTable extends Component {
const { const {
isFetching, isFetching,
isPopulated, isPopulated,
isDeleting,
error, error,
items, items,
columns, columns,
@@ -72,13 +179,19 @@ class UnmappedFilesTable extends Component {
onSortPress, onSortPress,
isScanningFolders, isScanningFolders,
onAddMissingAuthorsPress, onAddMissingAuthorsPress,
deleteUnmappedFiles,
...otherProps ...otherProps
} = this.props; } = this.props;
const { const {
scroller scroller,
allSelected,
allUnselected,
selectedState
} = this.state; } = this.state;
const selectedTrackFileIds = this.getSelectedIds();
return ( return (
<PageContent title={translate('UnmappedFiles')}> <PageContent title={translate('UnmappedFiles')}>
<PageToolbar> <PageToolbar>
@@ -90,6 +203,13 @@ class UnmappedFilesTable extends Component {
isSpinning={isScanningFolders} isSpinning={isScanningFolders}
onPress={onAddMissingAuthorsPress} onPress={onAddMissingAuthorsPress}
/> />
<PageToolbarButton
label={translate('DeleteSelected')}
iconName={icons.DELETE}
isDisabled={selectedTrackFileIds.length === 0}
isSpinning={isDeleting}
onPress={this.onDeleteUnmappedFilesPress}
/>
</PageToolbarSection> </PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}> <PageToolbarSection alignContent={align.RIGHT}>
@@ -117,9 +237,9 @@ class UnmappedFilesTable extends Component {
{ {
isPopulated && !error && !items.length && isPopulated && !error && !items.length &&
<div> <Alert kind={kinds.INFO}>
Success! My work is done, all files on disk are matched to known books. Success! My work is done, all files on disk are matched to known books.
</div> </Alert>
} }
{ {
@@ -138,8 +258,12 @@ class UnmappedFilesTable extends Component {
sortDirection={sortDirection} sortDirection={sortDirection}
onTableOptionChange={onTableOptionChange} onTableOptionChange={onTableOptionChange}
onSortPress={onSortPress} onSortPress={onSortPress}
allSelected={allSelected}
allUnselected={allUnselected}
onSelectAllChange={this.onSelectAllChange}
/> />
} }
selectedState={selectedState}
sortKey={sortKey} sortKey={sortKey}
sortDirection={sortDirection} sortDirection={sortDirection}
/> />
@@ -153,6 +277,8 @@ class UnmappedFilesTable extends Component {
UnmappedFilesTable.propTypes = { UnmappedFilesTable.propTypes = {
isFetching: PropTypes.bool.isRequired, isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired, isPopulated: PropTypes.bool.isRequired,
isDeleting: PropTypes.bool.isRequired,
deleteError: PropTypes.object,
error: PropTypes.object, error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired,
@@ -161,6 +287,7 @@ UnmappedFilesTable.propTypes = {
onTableOptionChange: PropTypes.func.isRequired, onTableOptionChange: PropTypes.func.isRequired,
onSortPress: PropTypes.func.isRequired, onSortPress: PropTypes.func.isRequired,
deleteUnmappedFile: PropTypes.func.isRequired, deleteUnmappedFile: PropTypes.func.isRequired,
deleteUnmappedFiles: PropTypes.func.isRequired,
isScanningFolders: PropTypes.bool.isRequired, isScanningFolders: PropTypes.bool.isRequired,
onAddMissingAuthorsPress: PropTypes.func.isRequired onAddMissingAuthorsPress: PropTypes.func.isRequired
}; };
@@ -5,7 +5,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames'; import * as commandNames from 'Commands/commandNames';
import withCurrentPage from 'Components/withCurrentPage'; import withCurrentPage from 'Components/withCurrentPage';
import { deleteBookFile, fetchBookFiles, setBookFilesSort, setBookFilesTableOption } from 'Store/Actions/bookFileActions'; import { deleteBookFile, deleteBookFiles, fetchBookFiles, setBookFilesSort, setBookFilesTableOption } from 'Store/Actions/bookFileActions';
import { executeCommand } from 'Store/Actions/commandActions'; import { executeCommand } from 'Store/Actions/commandActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
@@ -28,7 +28,9 @@ function createMapStateToProps() {
items, items,
...otherProps ...otherProps
} = bookFiles; } = bookFiles;
const unmappedFiles = _.filter(items, { bookId: 0 }); const unmappedFiles = _.filter(items, { bookId: 0 });
return { return {
items: unmappedFiles, items: unmappedFiles,
...otherProps, ...otherProps,
@@ -57,6 +59,10 @@ function createMapDispatchToProps(dispatch, props) {
dispatch(deleteBookFile({ id })); dispatch(deleteBookFile({ id }));
}, },
deleteUnmappedFiles(bookFileIds) {
dispatch(deleteBookFiles({ bookFileIds }));
},
onAddMissingAuthorsPress() { onAddMissingAuthorsPress() {
dispatch(executeCommand({ dispatch(executeCommand({
name: commandNames.RESCAN_FOLDERS, name: commandNames.RESCAN_FOLDERS,
@@ -106,7 +112,8 @@ UnmappedFilesTableConnector.propTypes = {
onSortPress: PropTypes.func.isRequired, onSortPress: PropTypes.func.isRequired,
onTableOptionChange: PropTypes.func.isRequired, onTableOptionChange: PropTypes.func.isRequired,
fetchUnmappedFiles: PropTypes.func.isRequired, fetchUnmappedFiles: PropTypes.func.isRequired,
deleteUnmappedFile: PropTypes.func.isRequired deleteUnmappedFile: PropTypes.func.isRequired,
deleteUnmappedFiles: PropTypes.func.isRequired
}; };
export default withCurrentPage( export default withCurrentPage(
@@ -4,6 +4,7 @@ import IconButton from 'Components/Link/IconButton';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper'; import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import VirtualTableHeader from 'Components/Table/VirtualTableHeader'; import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell'; import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
import { icons } from 'Helpers/Props'; import { icons } from 'Helpers/Props';
// import hasGrowableColumns from './hasGrowableColumns'; // import hasGrowableColumns from './hasGrowableColumns';
import styles from './UnmappedFilesTableHeader.css'; import styles from './UnmappedFilesTableHeader.css';
@@ -12,6 +13,9 @@ function UnmappedFilesTableHeader(props) {
const { const {
columns, columns,
onTableOptionChange, onTableOptionChange,
allSelected,
allUnselected,
onSelectAllChange,
...otherProps ...otherProps
} = props; } = props;
@@ -30,6 +34,17 @@ function UnmappedFilesTableHeader(props) {
return null; return null;
} }
if (name === 'select') {
return (
<VirtualTableSelectAllHeaderCell
key={name}
allSelected={allSelected}
allUnselected={allUnselected}
onSelectAllChange={onSelectAllChange}
/>
);
}
if (name === 'actions') { if (name === 'actions') {
return ( return (
<VirtualTableHeaderCell <VirtualTableHeaderCell
@@ -71,6 +86,9 @@ function UnmappedFilesTableHeader(props) {
UnmappedFilesTableHeader.propTypes = { UnmappedFilesTableHeader.propTypes = {
columns: PropTypes.arrayOf(PropTypes.object).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired,
allSelected: PropTypes.bool.isRequired,
allUnselected: PropTypes.bool.isRequired,
onSelectAllChange: PropTypes.func.isRequired,
onTableOptionChange: PropTypes.func.isRequired onTableOptionChange: PropTypes.func.isRequired
}; };
@@ -20,3 +20,9 @@
flex: 0 0 100px; flex: 0 0 100px;
} }
.checkInput {
composes: input from '~Components/Form/CheckInput.css';
margin-top: 0;
}
@@ -2,6 +2,7 @@
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'actions': string; 'actions': string;
'checkInput': string;
'dateAdded': string; 'dateAdded': string;
'path': string; 'path': string;
'quality': string; 'quality': string;
@@ -6,6 +6,7 @@ import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal'; import ConfirmModal from 'Components/Modal/ConfirmModal';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell'; import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
import { icons, kinds } from 'Helpers/Props'; import { icons, kinds } from 'Helpers/Props';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal'; import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import formatBytes from 'Utilities/Number/formatBytes'; import formatBytes from 'Utilities/Number/formatBytes';
@@ -69,7 +70,9 @@ class UnmappedFilesTableRow extends Component {
size, size,
dateAdded, dateAdded,
quality, quality,
columns columns,
isSelected,
onSelectedChange
} = this.props; } = this.props;
const folder = path.substring(0, Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'))); const folder = path.substring(0, Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')));
@@ -93,6 +96,19 @@ class UnmappedFilesTableRow extends Component {
return null; return null;
} }
if (name === 'select') {
return (
<VirtualTableSelectCell
inputClassName={styles.checkInput}
id={id}
key={name}
isSelected={isSelected}
isDisabled={false}
onSelectedChange={onSelectedChange}
/>
);
}
if (name === 'path') { if (name === 'path') {
return ( return (
<VirtualTableRowCell <VirtualTableRowCell
@@ -208,6 +224,8 @@ UnmappedFilesTableRow.propTypes = {
quality: PropTypes.object.isRequired, quality: PropTypes.object.isRequired,
dateAdded: PropTypes.string.isRequired, dateAdded: PropTypes.string.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired,
isSelected: PropTypes.bool,
onSelectedChange: PropTypes.func.isRequired,
deleteUnmappedFile: PropTypes.func.isRequired deleteUnmappedFile: PropTypes.func.isRequired
}; };
@@ -0,0 +1,11 @@
const regex = /\d+/g;
function naturalExpansion(input) {
if (!input) {
return '';
}
return input.replace(regex, (n) => n.padStart(8, '0'));
}
export default naturalExpansion;
+3 -1
View File
@@ -1,9 +1,11 @@
const regex = /\b\w+/g;
function titleCase(input) { function titleCase(input) {
if (!input) { if (!input) {
return ''; return '';
} }
return input.replace(/\b\w+/g, (match) => { return input.replace(regex, (match) => {
return match.charAt(0).toUpperCase() + match.substr(1).toLowerCase(); return match.charAt(0).toUpperCase() + match.substr(1).toLowerCase();
}); });
} }
+9 -11
View File
@@ -25,20 +25,18 @@ export async function fetchTranslations(): Promise<boolean> {
export default function translate( export default function translate(
key: string, key: string,
tokens?: Record<string, string | number | boolean> tokens: Record<string, string | number | boolean> = {}
) { ) {
const translation = translations[key] || key; const translation = translations[key] || key;
if (tokens) { tokens.appName = 'Readarr';
// 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.replace(/\{([a-z0-9]+?)\}/gi, (match, tokenMatch) => // Fallback to the old behaviour for translations not yet updated to use named tokens
String(tokens[tokenMatch] ?? match) Object.values(tokens).forEach((value, index) => {
); tokens[index] = value;
} });
return translation; return translation.replace(/\{([a-z0-9]+?)\}/gi, (match, tokenMatch) =>
String(tokens[tokenMatch] ?? match)
);
} }
@@ -53,7 +53,7 @@ class CutoffUnmetConnector extends Component {
gotoCutoffUnmetFirstPage gotoCutoffUnmetFirstPage
} = this.props; } = this.props;
registerPagePopulator(this.repopulate, ['bookFileUpdated']); registerPagePopulator(this.repopulate, ['bookFileUpdated', 'bookFileDeleted']);
if (useCurrentPage) { if (useCurrentPage) {
fetchCutoffUnmet(); fetchCutoffUnmet();
@@ -50,7 +50,7 @@ class MissingConnector extends Component {
gotoMissingFirstPage gotoMissingFirstPage
} = this.props; } = this.props;
registerPagePopulator(this.repopulate, ['bookFileUpdated']); registerPagePopulator(this.repopulate, ['bookFileUpdated', 'bookFileDeleted']);
if (useCurrentPage) { if (useCurrentPage) {
fetchMissing(); fetchMissing();
+2
View File
@@ -4,6 +4,8 @@ import { render } from 'react-dom';
import createAppStore from 'Store/createAppStore'; import createAppStore from 'Store/createAppStore';
import App from './App/App'; import App from './App/App';
import 'Diag/ConsoleApi';
export async function bootstrap() { export async function bootstrap() {
const history = createBrowserHistory(); const history = createBrowserHistory();
const store = createAppStore(history); const store = createAppStore(history);
+18 -20
View File
@@ -30,7 +30,7 @@
"@fortawesome/free-regular-svg-icons": "6.4.0", "@fortawesome/free-regular-svg-icons": "6.4.0",
"@fortawesome/free-solid-svg-icons": "6.4.0", "@fortawesome/free-solid-svg-icons": "6.4.0",
"@fortawesome/react-fontawesome": "0.2.0", "@fortawesome/react-fontawesome": "0.2.0",
"@microsoft/signalr": "6.0.16", "@microsoft/signalr": "6.0.25",
"@sentry/browser": "7.51.2", "@sentry/browser": "7.51.2",
"@sentry/integrations": "7.51.2", "@sentry/integrations": "7.51.2",
"@types/node": "18.16.16", "@types/node": "18.16.16",
@@ -83,30 +83,28 @@
"redux-localstorage": "0.4.1", "redux-localstorage": "0.4.1",
"redux-thunk": "2.3.0", "redux-thunk": "2.3.0",
"reselect": "4.1.8", "reselect": "4.1.8",
"stacktrace-js": "2.0.2",
"typescript": "4.9.5" "typescript": "4.9.5"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.22.9", "@babel/core": "7.22.11",
"@babel/eslint-parser": "7.22.9", "@babel/eslint-parser": "7.22.11",
"@babel/plugin-proposal-class-properties": "7.18.6",
"@babel/plugin-proposal-export-default-from": "7.22.5", "@babel/plugin-proposal-export-default-from": "7.22.5",
"@babel/plugin-proposal-export-namespace-from": "7.18.9",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6",
"@babel/plugin-proposal-optional-chaining": "7.21.0",
"@babel/plugin-syntax-dynamic-import": "7.8.3", "@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/preset-env": "7.22.9", "@babel/preset-env": "7.22.15",
"@babel/preset-react": "7.22.5", "@babel/preset-react": "7.22.5",
"@babel/preset-typescript": "7.22.5", "@babel/preset-typescript": "7.22.11",
"@types/lodash": "4.14.197", "@types/lodash": "4.14.197",
"@types/react-lazyload": "3.2.1",
"@types/redux-actions": "2.6.2", "@types/redux-actions": "2.6.2",
"@typescript-eslint/eslint-plugin": "6.0.0", "@typescript-eslint/eslint-plugin": "6.5.0",
"@typescript-eslint/parser": "6.0.0", "@typescript-eslint/parser": "6.5.0",
"autoprefixer": "10.4.14", "autoprefixer": "10.4.14",
"babel-loader": "9.1.3", "babel-loader": "9.1.3",
"babel-plugin-inline-classnames": "2.0.1", "babel-plugin-inline-classnames": "2.0.1",
"babel-plugin-transform-react-remove-prop-types": "0.4.24", "babel-plugin-transform-react-remove-prop-types": "0.4.24",
"core-js": "3.31.1", "core-js": "3.32.1",
"css-loader": "6.7.3", "css-loader": "6.8.1",
"css-modules-typescript-loader": "4.0.1", "css-modules-typescript-loader": "4.0.1",
"eslint": "8.44.0", "eslint": "8.44.0",
"eslint-config-prettier": "8.8.0", "eslint-config-prettier": "8.8.0",
@@ -120,10 +118,10 @@
"file-loader": "6.2.0", "file-loader": "6.2.0",
"filemanager-webpack-plugin": "8.0.0", "filemanager-webpack-plugin": "8.0.0",
"fork-ts-checker-webpack-plugin": "8.0.0", "fork-ts-checker-webpack-plugin": "8.0.0",
"html-webpack-plugin": "5.5.1", "html-webpack-plugin": "5.5.3",
"loader-utils": "^3.2.1", "loader-utils": "^3.2.1",
"mini-css-extract-plugin": "2.7.5", "mini-css-extract-plugin": "2.7.6",
"postcss": "8.4.23", "postcss": "8.4.31",
"postcss-color-function": "4.1.0", "postcss-color-function": "4.1.0",
"postcss-loader": "7.3.0", "postcss-loader": "7.3.0",
"postcss-mixins": "9.0.4", "postcss-mixins": "9.0.4",
@@ -135,14 +133,14 @@
"rimraf": "4.4.1", "rimraf": "4.4.1",
"run-sequence": "2.2.1", "run-sequence": "2.2.1",
"streamqueue": "1.1.2", "streamqueue": "1.1.2",
"style-loader": "3.3.2", "style-loader": "3.3.3",
"stylelint": "15.10.1", "stylelint": "15.10.3",
"stylelint-order": "6.0.3", "stylelint-order": "6.0.3",
"terser-webpack-plugin": "5.3.9", "terser-webpack-plugin": "5.3.9",
"ts-loader": "9.4.3", "ts-loader": "9.4.4",
"typescript-plugin-css-modules": "5.0.1", "typescript-plugin-css-modules": "5.0.1",
"url-loader": "4.1.1", "url-loader": "4.1.1",
"webpack": "5.88.1", "webpack": "5.88.2",
"webpack-cli": "5.1.4", "webpack-cli": "5.1.4",
"webpack-livereload-plugin": "3.0.2", "webpack-livereload-plugin": "3.0.2",
"worker-loader": "3.0.8" "worker-loader": "3.0.8"
+9 -8
View File
@@ -4,10 +4,11 @@
<PackageVersion Include="AutoFixture" Version="4.17.0" /> <PackageVersion Include="AutoFixture" Version="4.17.0" />
<PackageVersion Include="coverlet.collector" Version="3.0.4-preview.27.ge7cb7c3b40" PrivateAssets="all" /> <PackageVersion Include="coverlet.collector" Version="3.0.4-preview.27.ge7cb7c3b40" PrivateAssets="all" />
<PackageVersion Include="Dapper" Version="2.0.123" /> <PackageVersion Include="Dapper" Version="2.0.123" />
<PackageVersion Include="DryIoc.dll" Version="5.4.0" /> <PackageVersion Include="DryIoc.dll" Version="5.4.3" />
<PackageVersion Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0" /> <PackageVersion Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0" />
<PackageVersion Include="Equ" Version="2.3.0" /> <PackageVersion Include="Equ" Version="2.3.0" />
<PackageVersion Include="FluentAssertions" Version="5.10.3" /> <PackageVersion Include="FluentAssertions" Version="5.10.3" />
<PackageVersion Include="Polly" Version="8.2.0" />
<PackageVersion Include="Servarr.FluentMigrator.Runner" Version="3.3.2.9" /> <PackageVersion Include="Servarr.FluentMigrator.Runner" Version="3.3.2.9" />
<PackageVersion Include="Servarr.FluentMigrator.Runner.SQLite" Version="3.3.2.9" /> <PackageVersion Include="Servarr.FluentMigrator.Runner.SQLite" Version="3.3.2.9" />
<PackageVersion Include="Servarr.FluentMigrator.Runner.Postgres" Version="3.3.2.9" /> <PackageVersion Include="Servarr.FluentMigrator.Runner.Postgres" Version="3.3.2.9" />
@@ -16,11 +17,11 @@
<PackageVersion Include="ImpromptuInterface" Version="7.0.1" /> <PackageVersion Include="ImpromptuInterface" Version="7.0.1" />
<PackageVersion Include="LazyCache" Version="2.4.0" /> <PackageVersion Include="LazyCache" Version="2.4.0" />
<PackageVersion Include="Mailkit" Version="3.6.0" /> <PackageVersion Include="Mailkit" Version="3.6.0" />
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.16" /> <PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.25" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" /> <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="6.0.1" /> <PackageVersion Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.1" /> <PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="6.0.0" /> <PackageVersion Include="Microsoft.Extensions.Logging" Version="6.0.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.4.1" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageVersion Include="Microsoft.Win32.Registry" Version="5.0.0" /> <PackageVersion Include="Microsoft.Win32.Registry" Version="5.0.0" />
@@ -32,7 +33,7 @@
<PackageVersion Include="NLog.Extensions.Logging" Version="5.2.3" /> <PackageVersion Include="NLog.Extensions.Logging" Version="5.2.3" />
<PackageVersion Include="NLog" Version="5.1.4" /> <PackageVersion Include="NLog" Version="5.1.4" />
<PackageVersion Include="NLog.Targets.Syslog" Version="7.0.0" /> <PackageVersion Include="NLog.Targets.Syslog" Version="7.0.0" />
<PackageVersion Include="Npgsql" Version="7.0.4" /> <PackageVersion Include="Npgsql" Version="7.0.6" />
<PackageVersion Include="NUnit3TestAdapter" Version="4.2.1" /> <PackageVersion Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageVersion Include="NUnit" Version="3.13.3" /> <PackageVersion Include="NUnit" Version="3.13.3" />
<PackageVersion Include="NunitXml.TestLogger" Version="3.0.117" /> <PackageVersion Include="NunitXml.TestLogger" Version="3.0.117" />
@@ -43,7 +44,7 @@
<PackageVersion Include="Selenium.WebDriver.ChromeDriver" Version="91.0.4472.10100" /> <PackageVersion Include="Selenium.WebDriver.ChromeDriver" Version="91.0.4472.10100" />
<PackageVersion Include="Sentry" Version="3.31.0" /> <PackageVersion Include="Sentry" Version="3.31.0" />
<PackageVersion Include="SharpZipLib" Version="1.4.2" /> <PackageVersion Include="SharpZipLib" Version="1.4.2" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.0.1" /> <PackageVersion Include="SixLabors.ImageSharp" Version="3.0.2" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.1.118" /> <PackageVersion Include="StyleCop.Analyzers" Version="1.1.118" />
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.5.0" /> <PackageVersion Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.5.0" />
<PackageVersion Include="System.Buffers" Version="4.5.1" /> <PackageVersion Include="System.Buffers" Version="4.5.1" />
@@ -57,10 +58,10 @@
<PackageVersion Include="System.Resources.Extensions" Version="6.0.0" /> <PackageVersion Include="System.Resources.Extensions" Version="6.0.0" />
<PackageVersion Include="System.Runtime.Loader" Version="4.3.0" /> <PackageVersion Include="System.Runtime.Loader" Version="4.3.0" />
<PackageVersion Include="System.Security.Principal.Windows" Version="5.0.0" /> <PackageVersion Include="System.Security.Principal.Windows" Version="5.0.0" />
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="6.0.0" /> <PackageVersion Include="System.ServiceProcess.ServiceController" Version="6.0.1" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="6.0.0" /> <PackageVersion Include="System.Text.Encoding.CodePages" Version="6.0.0" />
<PackageVersion Include="System.Text.Json" Version="6.0.7" /> <PackageVersion Include="System.Text.Json" Version="6.0.9" />
<PackageVersion Include="System.ValueTuple" Version="4.5.0" /> <PackageVersion Include="System.ValueTuple" Version="4.5.0" />
<PackageVersion Include="TagLibSharp-Lidarr" Version="2.2.0.19" /> <PackageVersion Include="TagLibSharp-Lidarr" Version="2.2.0.19" />
</ItemGroup> </ItemGroup>
</Project> </Project>
@@ -1,6 +1,9 @@
using System.Collections.Generic;
using FluentAssertions; using FluentAssertions;
using Moq;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Core.Annotations; using NzbDrone.Core.Annotations;
using NzbDrone.Core.Localization;
using NzbDrone.Test.Common; using NzbDrone.Test.Common;
using Readarr.Http.ClientSchema; using Readarr.Http.ClientSchema;
@@ -9,6 +12,16 @@ namespace NzbDrone.Api.Test.ClientSchemaTests
[TestFixture] [TestFixture]
public class SchemaBuilderFixture : TestBase public class SchemaBuilderFixture : TestBase
{ {
[SetUp]
public void Setup()
{
Mocker.GetMock<ILocalizationService>()
.Setup(s => s.GetLocalizedString(It.IsAny<string>(), It.IsAny<Dictionary<string, object>>()))
.Returns<string, Dictionary<string, object>>((s, d) => s);
SchemaBuilder.Initialize(Mocker.Container);
}
[Test] [Test]
public void should_return_field_for_every_property() public void should_return_field_for_every_property()
{ {
@@ -6,6 +6,7 @@ using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using FluentAssertions; using FluentAssertions;
using Moq; using Moq;
using NLog; using NLog;
@@ -114,21 +115,31 @@ namespace NzbDrone.Common.Test.Http
} }
[Test] [Test]
public void should_execute_simple_get() public async Task should_execute_simple_get()
{ {
var request = new HttpRequest($"https://{_httpBinHost}/get"); var request = new HttpRequest($"https://{_httpBinHost}/get");
var response = Subject.Execute(request); var response = await Subject.ExecuteAsync(request);
response.Content.Should().NotBeNullOrWhiteSpace(); response.Content.Should().NotBeNullOrWhiteSpace();
} }
[Test] [Test]
public void should_execute_https_get() public void should_throw_timeout_request()
{
var request = new HttpRequest($"https://{_httpBinHost}/delay/10");
request.RequestTimeout = new TimeSpan(0, 0, 5);
Assert.ThrowsAsync<WebException>(async () => await Subject.ExecuteAsync(request));
}
[Test]
public async Task should_execute_https_get()
{ {
var request = new HttpRequest($"https://{_httpBinHost}/get"); var request = new HttpRequest($"https://{_httpBinHost}/get");
var response = Subject.Execute(request); var response = await Subject.ExecuteAsync(request);
response.Content.Should().NotBeNullOrWhiteSpace(); response.Content.Should().NotBeNullOrWhiteSpace();
} }
@@ -140,47 +151,47 @@ namespace NzbDrone.Common.Test.Http
Mocker.GetMock<IConfigService>().SetupGet(x => x.CertificateValidation).Returns(validationType); Mocker.GetMock<IConfigService>().SetupGet(x => x.CertificateValidation).Returns(validationType);
var request = new HttpRequest($"https://expired.badssl.com"); var request = new HttpRequest($"https://expired.badssl.com");
Assert.Throws<HttpRequestException>(() => Subject.Execute(request)); Assert.ThrowsAsync<HttpRequestException>(async () => await Subject.ExecuteAsync(request));
ExceptionVerification.ExpectedErrors(1); ExceptionVerification.ExpectedErrors(1);
} }
[Test] [Test]
public void bad_ssl_should_pass_if_remote_validation_disabled() public async Task bad_ssl_should_pass_if_remote_validation_disabled()
{ {
Mocker.GetMock<IConfigService>().SetupGet(x => x.CertificateValidation).Returns(CertificateValidationType.Disabled); Mocker.GetMock<IConfigService>().SetupGet(x => x.CertificateValidation).Returns(CertificateValidationType.Disabled);
var request = new HttpRequest($"https://expired.badssl.com"); var request = new HttpRequest($"https://expired.badssl.com");
Subject.Execute(request); await Subject.ExecuteAsync(request);
ExceptionVerification.ExpectedErrors(0); ExceptionVerification.ExpectedErrors(0);
} }
[Test] [Test]
public void should_execute_typed_get() public async Task should_execute_typed_get()
{ {
var request = new HttpRequest($"https://{_httpBinHost}/get?test=1"); var request = new HttpRequest($"https://{_httpBinHost}/get?test=1");
var response = Subject.Get<HttpBinResource>(request); var response = await Subject.GetAsync<HttpBinResource>(request);
response.Resource.Url.EndsWith("/get?test=1"); response.Resource.Url.EndsWith("/get?test=1");
response.Resource.Args.Should().Contain("test", "1"); response.Resource.Args.Should().Contain("test", "1");
} }
[Test] [Test]
public void should_execute_simple_post() public async Task should_execute_simple_post()
{ {
var message = "{ my: 1 }"; var message = "{ my: 1 }";
var request = new HttpRequest($"https://{_httpBinHost}/post"); var request = new HttpRequest($"https://{_httpBinHost}/post");
request.SetContent(message); request.SetContent(message);
var response = Subject.Post<HttpBinResource>(request); var response = await Subject.PostAsync<HttpBinResource>(request);
response.Resource.Data.Should().Be(message); response.Resource.Data.Should().Be(message);
} }
[Test] [Test]
public void should_execute_post_with_content_type() public async Task should_execute_post_with_content_type()
{ {
var message = "{ my: 1 }"; var message = "{ my: 1 }";
@@ -188,17 +199,16 @@ namespace NzbDrone.Common.Test.Http
request.SetContent(message); request.SetContent(message);
request.Headers.ContentType = "application/json"; request.Headers.ContentType = "application/json";
var response = Subject.Post<HttpBinResource>(request); var response = await Subject.PostAsync<HttpBinResource>(request);
response.Resource.Data.Should().Be(message); response.Resource.Data.Should().Be(message);
} }
[Test] [Test]
public void should_execute_get_using_gzip() public async Task should_execute_get_using_gzip()
{ {
var request = new HttpRequest($"https://{_httpBinHost}/gzip"); var request = new HttpRequest($"https://{_httpBinHost}/gzip");
var response = await Subject.GetAsync<HttpBinResource>(request);
var response = Subject.Get<HttpBinResource>(request);
response.Resource.Headers["Accept-Encoding"].ToString().Should().Contain("gzip"); response.Resource.Headers["Accept-Encoding"].ToString().Should().Contain("gzip");
@@ -208,11 +218,10 @@ namespace NzbDrone.Common.Test.Http
[Test] [Test]
[Platform(Exclude = "MacOsX", Reason = "Azure agent update prevents brotli on OSX")] [Platform(Exclude = "MacOsX", Reason = "Azure agent update prevents brotli on OSX")]
public void should_execute_get_using_brotli() public async Task should_execute_get_using_brotli()
{ {
var request = new HttpRequest($"https://{_httpBinHost}/brotli"); var request = new HttpRequest($"https://{_httpBinHost}/brotli");
var response = await Subject.GetAsync<HttpBinResource>(request);
var response = Subject.Get<HttpBinResource>(request);
response.Resource.Headers["Accept-Encoding"].ToString().Should().Contain("br"); response.Resource.Headers["Accept-Encoding"].ToString().Should().Contain("br");
@@ -230,7 +239,7 @@ namespace NzbDrone.Common.Test.Http
{ {
var request = new HttpRequest($"https://{_httpBinHost}/status/{statusCode}"); var request = new HttpRequest($"https://{_httpBinHost}/status/{statusCode}");
var exception = Assert.Throws<HttpException>(() => Subject.Get<HttpBinResource>(request)); var exception = Assert.ThrowsAsync<HttpException>(async () => await Subject.GetAsync<HttpBinResource>(request));
((int)exception.Response.StatusCode).Should().Be(statusCode); ((int)exception.Response.StatusCode).Should().Be(statusCode);
@@ -243,7 +252,7 @@ namespace NzbDrone.Common.Test.Http
var request = new HttpRequest($"https://{_httpBinHost}/status/{HttpStatusCode.NotFound}"); var request = new HttpRequest($"https://{_httpBinHost}/status/{HttpStatusCode.NotFound}");
request.SuppressHttpErrorStatusCodes = new[] { HttpStatusCode.NotFound }; request.SuppressHttpErrorStatusCodes = new[] { HttpStatusCode.NotFound };
var exception = Assert.Throws<HttpException>(() => Subject.Get<HttpBinResource>(request)); Assert.ThrowsAsync<HttpException>(async () => await Subject.GetAsync<HttpBinResource>(request));
ExceptionVerification.IgnoreWarns(); ExceptionVerification.IgnoreWarns();
} }
@@ -253,7 +262,7 @@ namespace NzbDrone.Common.Test.Http
{ {
var request = new HttpRequest($"https://{_httpBinHost}/status/{HttpStatusCode.NotFound}"); var request = new HttpRequest($"https://{_httpBinHost}/status/{HttpStatusCode.NotFound}");
var exception = Assert.Throws<HttpException>(() => Subject.Get<HttpBinResource>(request)); var exception = Assert.ThrowsAsync<HttpException>(async () => await Subject.GetAsync<HttpBinResource>(request));
ExceptionVerification.ExpectedWarns(1); ExceptionVerification.ExpectedWarns(1);
} }
@@ -264,28 +273,28 @@ namespace NzbDrone.Common.Test.Http
var request = new HttpRequest($"https://{_httpBinHost}/status/{HttpStatusCode.NotFound}"); var request = new HttpRequest($"https://{_httpBinHost}/status/{HttpStatusCode.NotFound}");
request.LogHttpError = false; request.LogHttpError = false;
var exception = Assert.Throws<HttpException>(() => Subject.Get<HttpBinResource>(request)); Assert.ThrowsAsync<HttpException>(async () => await Subject.GetAsync<HttpBinResource>(request));
ExceptionVerification.ExpectedWarns(0); ExceptionVerification.ExpectedWarns(0);
} }
[Test] [Test]
public void should_not_follow_redirects_when_not_in_production() public async Task should_not_follow_redirects_when_not_in_production()
{ {
var request = new HttpRequest($"https://{_httpBinHost}/redirect/1"); var request = new HttpRequest($"https://{_httpBinHost}/redirect/1");
Subject.Get(request); await Subject.GetAsync(request);
ExceptionVerification.ExpectedErrors(1); ExceptionVerification.ExpectedErrors(1);
} }
[Test] [Test]
public void should_follow_redirects() public async Task should_follow_redirects()
{ {
var request = new HttpRequest($"https://{_httpBinHost}/redirect/1"); var request = new HttpRequest($"https://{_httpBinHost}/redirect/1");
request.AllowAutoRedirect = true; request.AllowAutoRedirect = true;
var response = Subject.Get(request); var response = await Subject.GetAsync(request);
response.StatusCode.Should().Be(HttpStatusCode.OK); response.StatusCode.Should().Be(HttpStatusCode.OK);
@@ -293,12 +302,12 @@ namespace NzbDrone.Common.Test.Http
} }
[Test] [Test]
public void should_not_follow_redirects() public async Task should_not_follow_redirects()
{ {
var request = new HttpRequest($"https://{_httpBinHost}/redirect/1"); var request = new HttpRequest($"https://{_httpBinHost}/redirect/1");
request.AllowAutoRedirect = false; request.AllowAutoRedirect = false;
var response = Subject.Get(request); var response = await Subject.GetAsync(request);
response.StatusCode.Should().Be(HttpStatusCode.Found); response.StatusCode.Should().Be(HttpStatusCode.Found);
@@ -306,14 +315,14 @@ namespace NzbDrone.Common.Test.Http
} }
[Test] [Test]
public void should_follow_redirects_to_https() public async Task should_follow_redirects_to_https()
{ {
var request = new HttpRequestBuilder($"https://{_httpBinHost}/redirect-to") var request = new HttpRequestBuilder($"https://{_httpBinHost}/redirect-to")
.AddQueryParam("url", $"https://readarr.com/") .AddQueryParam("url", $"https://readarr.com/")
.Build(); .Build();
request.AllowAutoRedirect = true; request.AllowAutoRedirect = true;
var response = Subject.Get(request); var response = await Subject.GetAsync(request);
response.StatusCode.Should().Be(HttpStatusCode.OK); response.StatusCode.Should().Be(HttpStatusCode.OK);
response.Content.Should().Contain("Readarr"); response.Content.Should().Contain("Readarr");
@@ -327,17 +336,17 @@ namespace NzbDrone.Common.Test.Http
var request = new HttpRequest($"https://{_httpBinHost}/redirect/6"); var request = new HttpRequest($"https://{_httpBinHost}/redirect/6");
request.AllowAutoRedirect = true; request.AllowAutoRedirect = true;
Assert.Throws<WebException>(() => Subject.Get(request)); Assert.ThrowsAsync<WebException>(async () => await Subject.GetAsync(request));
ExceptionVerification.ExpectedErrors(0); ExceptionVerification.ExpectedErrors(0);
} }
[Test] [Test]
public void should_send_user_agent() public async Task should_send_user_agent()
{ {
var request = new HttpRequest($"https://{_httpBinHost}/get"); var request = new HttpRequest($"https://{_httpBinHost}/get");
var response = Subject.Get<HttpBinResource>(request); var response = await Subject.GetAsync<HttpBinResource>(request);
response.Resource.Headers.Should().ContainKey("User-Agent"); response.Resource.Headers.Should().ContainKey("User-Agent");
@@ -347,24 +356,24 @@ namespace NzbDrone.Common.Test.Http
} }
[TestCase("Accept", "text/xml, text/rss+xml, application/rss+xml")] [TestCase("Accept", "text/xml, text/rss+xml, application/rss+xml")]
public void should_send_headers(string header, string value) public async Task should_send_headers(string header, string value)
{ {
var request = new HttpRequest($"https://{_httpBinHost}/get"); var request = new HttpRequest($"https://{_httpBinHost}/get");
request.Headers.Add(header, value); request.Headers.Add(header, value);
var response = Subject.Get<HttpBinResource>(request); var response = await Subject.GetAsync<HttpBinResource>(request);
response.Resource.Headers[header].ToString().Should().Be(value); response.Resource.Headers[header].ToString().Should().Be(value);
} }
[Test] [Test]
public void should_download_file() public async Task should_download_file()
{ {
var file = GetTempFilePath(); var file = GetTempFilePath();
var url = "https://readarr.com/img/slider/artistdetails.png"; var url = "https://readarr.com/img/slider/artistdetails.png";
Subject.DownloadFile(url, file); await Subject.DownloadFileAsync(url, file);
var fileInfo = new FileInfo(file); var fileInfo = new FileInfo(file);
fileInfo.Exists.Should().BeTrue(); fileInfo.Exists.Should().BeTrue();
@@ -372,7 +381,7 @@ namespace NzbDrone.Common.Test.Http
} }
[Test] [Test]
public void should_download_file_with_redirect() public async Task should_download_file_with_redirect()
{ {
var file = GetTempFilePath(); var file = GetTempFilePath();
@@ -380,7 +389,7 @@ namespace NzbDrone.Common.Test.Http
.AddQueryParam("url", $"https://readarr.com/img/slider/artistdetails.png") .AddQueryParam("url", $"https://readarr.com/img/slider/artistdetails.png")
.Build(); .Build();
Subject.DownloadFile(request.Url.FullUri, file); await Subject.DownloadFileAsync(request.Url.FullUri, file);
ExceptionVerification.ExpectedErrors(0); ExceptionVerification.ExpectedErrors(0);
@@ -394,7 +403,7 @@ namespace NzbDrone.Common.Test.Http
{ {
var file = GetTempFilePath(); var file = GetTempFilePath();
Assert.Throws<HttpException>(() => Subject.DownloadFile("https://download.sonarr.tv/wrongpath", file)); Assert.ThrowsAsync<HttpException>(async () => await Subject.DownloadFileAsync("https://download.sonarr.tv/wrongpath", file));
File.Exists(file).Should().BeFalse(); File.Exists(file).Should().BeFalse();
@@ -402,7 +411,7 @@ namespace NzbDrone.Common.Test.Http
} }
[Test] [Test]
public void should_not_write_redirect_content_to_stream() public async Task should_not_write_redirect_content_to_stream()
{ {
var file = GetTempFilePath(); var file = GetTempFilePath();
@@ -412,7 +421,7 @@ namespace NzbDrone.Common.Test.Http
request.AllowAutoRedirect = false; request.AllowAutoRedirect = false;
request.ResponseStream = fileStream; request.ResponseStream = fileStream;
var response = Subject.Get(request); var response = await Subject.GetAsync(request);
response.StatusCode.Should().Be(HttpStatusCode.Moved); response.StatusCode.Should().Be(HttpStatusCode.Moved);
} }
@@ -427,12 +436,12 @@ namespace NzbDrone.Common.Test.Http
} }
[Test] [Test]
public void should_send_cookie() public async Task should_send_cookie()
{ {
var request = new HttpRequest($"https://{_httpBinHost}/get"); var request = new HttpRequest($"https://{_httpBinHost}/get");
request.Cookies["my"] = "cookie"; request.Cookies["my"] = "cookie";
var response = Subject.Get<HttpBinResource>(request); var response = await Subject.GetAsync<HttpBinResource>(request);
response.Resource.Headers.Should().ContainKey("Cookie"); response.Resource.Headers.Should().ContainKey("Cookie");
@@ -461,13 +470,13 @@ namespace NzbDrone.Common.Test.Http
} }
[Test] [Test]
public void should_preserve_cookie_during_session() public async Task should_preserve_cookie_during_session()
{ {
GivenOldCookie(); GivenOldCookie();
var request = new HttpRequest($"https://{_httpBinHost2}/get"); var request = new HttpRequest($"https://{_httpBinHost2}/get");
var response = Subject.Get<HttpBinResource>(request); var response = await Subject.GetAsync<HttpBinResource>(request);
response.Resource.Headers.Should().ContainKey("Cookie"); response.Resource.Headers.Should().ContainKey("Cookie");
@@ -477,30 +486,30 @@ namespace NzbDrone.Common.Test.Http
} }
[Test] [Test]
public void should_not_send_cookie_to_other_host() public async Task should_not_send_cookie_to_other_host()
{ {
GivenOldCookie(); GivenOldCookie();
var request = new HttpRequest($"https://{_httpBinHost}/get"); var request = new HttpRequest($"https://{_httpBinHost}/get");
var response = Subject.Get<HttpBinResource>(request); var response = await Subject.GetAsync<HttpBinResource>(request);
response.Resource.Headers.Should().NotContainKey("Cookie"); response.Resource.Headers.Should().NotContainKey("Cookie");
} }
[Test] [Test]
public void should_not_store_request_cookie() public async Task should_not_store_request_cookie()
{ {
var requestGet = new HttpRequest($"https://{_httpBinHost}/get"); var requestGet = new HttpRequest($"https://{_httpBinHost}/get");
requestGet.Cookies.Add("my", "cookie"); requestGet.Cookies.Add("my", "cookie");
requestGet.AllowAutoRedirect = false; requestGet.AllowAutoRedirect = false;
requestGet.StoreRequestCookie = false; requestGet.StoreRequestCookie = false;
requestGet.StoreResponseCookie = false; requestGet.StoreResponseCookie = false;
var responseGet = Subject.Get<HttpBinResource>(requestGet); var responseGet = await Subject.GetAsync<HttpBinResource>(requestGet);
var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies"); var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies");
requestCookies.AllowAutoRedirect = false; requestCookies.AllowAutoRedirect = false;
var responseCookies = Subject.Get<HttpCookieResource>(requestCookies); var responseCookies = await Subject.GetAsync<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().BeEmpty(); responseCookies.Resource.Cookies.Should().BeEmpty();
@@ -508,18 +517,18 @@ namespace NzbDrone.Common.Test.Http
} }
[Test] [Test]
public void should_store_request_cookie() public async Task should_store_request_cookie()
{ {
var requestGet = new HttpRequest($"https://{_httpBinHost}/get"); var requestGet = new HttpRequest($"https://{_httpBinHost}/get");
requestGet.Cookies.Add("my", "cookie"); requestGet.Cookies.Add("my", "cookie");
requestGet.AllowAutoRedirect = false; requestGet.AllowAutoRedirect = false;
requestGet.StoreRequestCookie.Should().BeTrue(); requestGet.StoreRequestCookie.Should().BeTrue();
requestGet.StoreResponseCookie = false; requestGet.StoreResponseCookie = false;
var responseGet = Subject.Get<HttpBinResource>(requestGet); var responseGet = await Subject.GetAsync<HttpBinResource>(requestGet);
var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies"); var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies");
requestCookies.AllowAutoRedirect = false; requestCookies.AllowAutoRedirect = false;
var responseCookies = Subject.Get<HttpCookieResource>(requestCookies); var responseCookies = await Subject.GetAsync<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie"); responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
@@ -527,7 +536,7 @@ namespace NzbDrone.Common.Test.Http
} }
[Test] [Test]
public void should_delete_request_cookie() public async Task should_delete_request_cookie()
{ {
var requestDelete = new HttpRequest($"https://{_httpBinHost}/cookies/delete?my"); var requestDelete = new HttpRequest($"https://{_httpBinHost}/cookies/delete?my");
requestDelete.Cookies.Add("my", "cookie"); requestDelete.Cookies.Add("my", "cookie");
@@ -536,13 +545,13 @@ namespace NzbDrone.Common.Test.Http
requestDelete.StoreResponseCookie = false; requestDelete.StoreResponseCookie = false;
// Delete and redirect since that's the only way to check the internal temporary cookie container // Delete and redirect since that's the only way to check the internal temporary cookie container
var responseCookies = Subject.Get<HttpCookieResource>(requestDelete); var responseCookies = await Subject.GetAsync<HttpCookieResource>(requestDelete);
responseCookies.Resource.Cookies.Should().BeEmpty(); responseCookies.Resource.Cookies.Should().BeEmpty();
} }
[Test] [Test]
public void should_clear_request_cookie() public async Task should_clear_request_cookie()
{ {
var requestSet = new HttpRequest($"https://{_httpBinHost}/cookies"); var requestSet = new HttpRequest($"https://{_httpBinHost}/cookies");
requestSet.Cookies.Add("my", "cookie"); requestSet.Cookies.Add("my", "cookie");
@@ -550,7 +559,7 @@ namespace NzbDrone.Common.Test.Http
requestSet.StoreRequestCookie = true; requestSet.StoreRequestCookie = true;
requestSet.StoreResponseCookie = false; requestSet.StoreResponseCookie = false;
var responseSet = Subject.Get<HttpCookieResource>(requestSet); var responseSet = await Subject.GetAsync<HttpCookieResource>(requestSet);
var requestClear = new HttpRequest($"https://{_httpBinHost}/cookies"); var requestClear = new HttpRequest($"https://{_httpBinHost}/cookies");
requestClear.Cookies.Add("my", null); requestClear.Cookies.Add("my", null);
@@ -558,24 +567,24 @@ namespace NzbDrone.Common.Test.Http
requestClear.StoreRequestCookie = true; requestClear.StoreRequestCookie = true;
requestClear.StoreResponseCookie = false; requestClear.StoreResponseCookie = false;
var responseClear = Subject.Get<HttpCookieResource>(requestClear); var responseClear = await Subject.GetAsync<HttpCookieResource>(requestClear);
responseClear.Resource.Cookies.Should().BeEmpty(); responseClear.Resource.Cookies.Should().BeEmpty();
} }
[Test] [Test]
public void should_not_store_response_cookie() public async Task should_not_store_response_cookie()
{ {
var requestSet = new HttpRequest($"https://{_httpBinHost}/cookies/set?my=cookie"); var requestSet = new HttpRequest($"https://{_httpBinHost}/cookies/set?my=cookie");
requestSet.AllowAutoRedirect = false; requestSet.AllowAutoRedirect = false;
requestSet.StoreRequestCookie = false; requestSet.StoreRequestCookie = false;
requestSet.StoreResponseCookie.Should().BeFalse(); requestSet.StoreResponseCookie.Should().BeFalse();
var responseSet = Subject.Get(requestSet); var responseSet = await Subject.GetAsync(requestSet);
var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies"); var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies");
var responseCookies = Subject.Get<HttpCookieResource>(requestCookies); var responseCookies = await Subject.GetAsync<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().BeEmpty(); responseCookies.Resource.Cookies.Should().BeEmpty();
@@ -583,18 +592,18 @@ namespace NzbDrone.Common.Test.Http
} }
[Test] [Test]
public void should_store_response_cookie() public async Task should_store_response_cookie()
{ {
var requestSet = new HttpRequest($"https://{_httpBinHost}/cookies/set?my=cookie"); var requestSet = new HttpRequest($"https://{_httpBinHost}/cookies/set?my=cookie");
requestSet.AllowAutoRedirect = false; requestSet.AllowAutoRedirect = false;
requestSet.StoreRequestCookie = false; requestSet.StoreRequestCookie = false;
requestSet.StoreResponseCookie = true; requestSet.StoreResponseCookie = true;
var responseSet = Subject.Get(requestSet); var responseSet = await Subject.GetAsync(requestSet);
var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies"); var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies");
var responseCookies = Subject.Get<HttpCookieResource>(requestCookies); var responseCookies = await Subject.GetAsync<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie"); responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
@@ -602,13 +611,13 @@ namespace NzbDrone.Common.Test.Http
} }
[Test] [Test]
public void should_temp_store_response_cookie() public async Task should_temp_store_response_cookie()
{ {
var requestSet = new HttpRequest($"https://{_httpBinHost}/cookies/set?my=cookie"); var requestSet = new HttpRequest($"https://{_httpBinHost}/cookies/set?my=cookie");
requestSet.AllowAutoRedirect = true; requestSet.AllowAutoRedirect = true;
requestSet.StoreRequestCookie = false; requestSet.StoreRequestCookie = false;
requestSet.StoreResponseCookie.Should().BeFalse(); requestSet.StoreResponseCookie.Should().BeFalse();
var responseSet = Subject.Get<HttpCookieResource>(requestSet); var responseSet = await Subject.GetAsync<HttpCookieResource>(requestSet);
// Set and redirect since that's the only way to check the internal temporary cookie container // Set and redirect since that's the only way to check the internal temporary cookie container
responseSet.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie"); responseSet.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
@@ -617,7 +626,7 @@ namespace NzbDrone.Common.Test.Http
} }
[Test] [Test]
public void should_overwrite_response_cookie() public async Task should_overwrite_response_cookie()
{ {
var requestSet = new HttpRequest($"https://{_httpBinHost}/cookies/set?my=cookie"); var requestSet = new HttpRequest($"https://{_httpBinHost}/cookies/set?my=cookie");
requestSet.Cookies.Add("my", "oldcookie"); requestSet.Cookies.Add("my", "oldcookie");
@@ -625,11 +634,11 @@ namespace NzbDrone.Common.Test.Http
requestSet.StoreRequestCookie = false; requestSet.StoreRequestCookie = false;
requestSet.StoreResponseCookie = true; requestSet.StoreResponseCookie = true;
var responseSet = Subject.Get(requestSet); var responseSet = await Subject.GetAsync(requestSet);
var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies"); var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies");
var responseCookies = Subject.Get<HttpCookieResource>(requestCookies); var responseCookies = await Subject.GetAsync<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie"); responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
@@ -637,7 +646,7 @@ namespace NzbDrone.Common.Test.Http
} }
[Test] [Test]
public void should_overwrite_temp_response_cookie() public async Task should_overwrite_temp_response_cookie()
{ {
var requestSet = new HttpRequest($"https://{_httpBinHost}/cookies/set?my=cookie"); var requestSet = new HttpRequest($"https://{_httpBinHost}/cookies/set?my=cookie");
requestSet.Cookies.Add("my", "oldcookie"); requestSet.Cookies.Add("my", "oldcookie");
@@ -645,13 +654,13 @@ namespace NzbDrone.Common.Test.Http
requestSet.StoreRequestCookie = true; requestSet.StoreRequestCookie = true;
requestSet.StoreResponseCookie = false; requestSet.StoreResponseCookie = false;
var responseSet = Subject.Get<HttpCookieResource>(requestSet); var responseSet = await Subject.GetAsync<HttpCookieResource>(requestSet);
responseSet.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie"); responseSet.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies"); var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies");
var responseCookies = Subject.Get<HttpCookieResource>(requestCookies); var responseCookies = await Subject.GetAsync<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "oldcookie"); responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "oldcookie");
@@ -659,14 +668,14 @@ namespace NzbDrone.Common.Test.Http
} }
[Test] [Test]
public void should_not_delete_response_cookie() public async Task should_not_delete_response_cookie()
{ {
var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies"); var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies");
requestCookies.Cookies.Add("my", "cookie"); requestCookies.Cookies.Add("my", "cookie");
requestCookies.AllowAutoRedirect = false; requestCookies.AllowAutoRedirect = false;
requestCookies.StoreRequestCookie = true; requestCookies.StoreRequestCookie = true;
requestCookies.StoreResponseCookie = false; requestCookies.StoreResponseCookie = false;
var responseCookies = Subject.Get<HttpCookieResource>(requestCookies); var responseCookies = await Subject.GetAsync<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie"); responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
@@ -675,13 +684,13 @@ namespace NzbDrone.Common.Test.Http
requestDelete.StoreRequestCookie = false; requestDelete.StoreRequestCookie = false;
requestDelete.StoreResponseCookie = false; requestDelete.StoreResponseCookie = false;
var responseDelete = Subject.Get(requestDelete); var responseDelete = await Subject.GetAsync(requestDelete);
requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies"); requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies");
requestCookies.StoreRequestCookie = false; requestCookies.StoreRequestCookie = false;
requestCookies.StoreResponseCookie = false; requestCookies.StoreResponseCookie = false;
responseCookies = Subject.Get<HttpCookieResource>(requestCookies); responseCookies = await Subject.GetAsync<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie"); responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
@@ -689,14 +698,14 @@ namespace NzbDrone.Common.Test.Http
} }
[Test] [Test]
public void should_delete_response_cookie() public async Task should_delete_response_cookie()
{ {
var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies"); var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies");
requestCookies.Cookies.Add("my", "cookie"); requestCookies.Cookies.Add("my", "cookie");
requestCookies.AllowAutoRedirect = false; requestCookies.AllowAutoRedirect = false;
requestCookies.StoreRequestCookie = true; requestCookies.StoreRequestCookie = true;
requestCookies.StoreResponseCookie = false; requestCookies.StoreResponseCookie = false;
var responseCookies = Subject.Get<HttpCookieResource>(requestCookies); var responseCookies = await Subject.GetAsync<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie"); responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
@@ -705,13 +714,13 @@ namespace NzbDrone.Common.Test.Http
requestDelete.StoreRequestCookie = false; requestDelete.StoreRequestCookie = false;
requestDelete.StoreResponseCookie = true; requestDelete.StoreResponseCookie = true;
var responseDelete = Subject.Get(requestDelete); var responseDelete = await Subject.GetAsync(requestDelete);
requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies"); requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies");
requestCookies.StoreRequestCookie = false; requestCookies.StoreRequestCookie = false;
requestCookies.StoreResponseCookie = false; requestCookies.StoreResponseCookie = false;
responseCookies = Subject.Get<HttpCookieResource>(requestCookies); responseCookies = await Subject.GetAsync<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().BeEmpty(); responseCookies.Resource.Cookies.Should().BeEmpty();
@@ -719,14 +728,14 @@ namespace NzbDrone.Common.Test.Http
} }
[Test] [Test]
public void should_delete_temp_response_cookie() public async Task should_delete_temp_response_cookie()
{ {
var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies"); var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies");
requestCookies.Cookies.Add("my", "cookie"); requestCookies.Cookies.Add("my", "cookie");
requestCookies.AllowAutoRedirect = false; requestCookies.AllowAutoRedirect = false;
requestCookies.StoreRequestCookie = true; requestCookies.StoreRequestCookie = true;
requestCookies.StoreResponseCookie = false; requestCookies.StoreResponseCookie = false;
var responseCookies = Subject.Get<HttpCookieResource>(requestCookies); var responseCookies = await Subject.GetAsync<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie"); responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
@@ -734,7 +743,7 @@ namespace NzbDrone.Common.Test.Http
requestDelete.AllowAutoRedirect = true; requestDelete.AllowAutoRedirect = true;
requestDelete.StoreRequestCookie = false; requestDelete.StoreRequestCookie = false;
requestDelete.StoreResponseCookie = false; requestDelete.StoreResponseCookie = false;
var responseDelete = Subject.Get<HttpCookieResource>(requestDelete); var responseDelete = await Subject.GetAsync<HttpCookieResource>(requestDelete);
responseDelete.Resource.Cookies.Should().BeEmpty(); responseDelete.Resource.Cookies.Should().BeEmpty();
@@ -752,13 +761,13 @@ namespace NzbDrone.Common.Test.Http
{ {
var request = new HttpRequest($"https://{_httpBinHost}/status/429"); var request = new HttpRequest($"https://{_httpBinHost}/status/429");
Assert.Throws<TooManyRequestsException>(() => Subject.Get(request)); Assert.ThrowsAsync<TooManyRequestsException>(async () => await Subject.GetAsync(request));
ExceptionVerification.IgnoreWarns(); ExceptionVerification.IgnoreWarns();
} }
[Test] [Test]
public void should_call_interceptor() public async Task should_call_interceptor()
{ {
Mocker.SetConstant<IEnumerable<IHttpRequestInterceptor>>(new[] { Mocker.GetMock<IHttpRequestInterceptor>().Object }); Mocker.SetConstant<IEnumerable<IHttpRequestInterceptor>>(new[] { Mocker.GetMock<IHttpRequestInterceptor>().Object });
@@ -772,7 +781,7 @@ namespace NzbDrone.Common.Test.Http
var request = new HttpRequest($"https://{_httpBinHost}/get"); var request = new HttpRequest($"https://{_httpBinHost}/get");
Subject.Get(request); await Subject.GetAsync(request);
Mocker.GetMock<IHttpRequestInterceptor>() Mocker.GetMock<IHttpRequestInterceptor>()
.Verify(v => v.PreRequest(It.IsAny<HttpRequest>()), Times.Once()); .Verify(v => v.PreRequest(It.IsAny<HttpRequest>()), Times.Once());
@@ -783,7 +792,7 @@ namespace NzbDrone.Common.Test.Http
[TestCase("en-US")] [TestCase("en-US")]
[TestCase("es-ES")] [TestCase("es-ES")]
public void should_parse_malformed_cloudflare_cookie(string culture) public async Task should_parse_malformed_cloudflare_cookie(string culture)
{ {
var origCulture = Thread.CurrentThread.CurrentCulture; var origCulture = Thread.CurrentThread.CurrentCulture;
Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo(culture); Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo(culture);
@@ -799,11 +808,11 @@ namespace NzbDrone.Common.Test.Http
requestSet.AllowAutoRedirect = false; requestSet.AllowAutoRedirect = false;
requestSet.StoreResponseCookie = true; requestSet.StoreResponseCookie = true;
var responseSet = Subject.Get(requestSet); var responseSet = await Subject.GetAsync(requestSet);
var request = new HttpRequest($"https://{_httpBinHost}/get"); var request = new HttpRequest($"https://{_httpBinHost}/get");
var response = Subject.Get<HttpBinResource>(request); var response = await Subject.GetAsync<HttpBinResource>(request);
response.Resource.Headers.Should().ContainKey("Cookie"); response.Resource.Headers.Should().ContainKey("Cookie");
@@ -821,7 +830,7 @@ namespace NzbDrone.Common.Test.Http
} }
[TestCase("lang_code=en; expires=Wed, 23-Dec-2026 18:09:14 GMT; Max-Age=31536000; path=/; domain=.abc.com")] [TestCase("lang_code=en; expires=Wed, 23-Dec-2026 18:09:14 GMT; Max-Age=31536000; path=/; domain=.abc.com")]
public void should_reject_malformed_domain_cookie(string malformedCookie) public async Task should_reject_malformed_domain_cookie(string malformedCookie)
{ {
try try
{ {
@@ -831,11 +840,11 @@ namespace NzbDrone.Common.Test.Http
requestSet.AllowAutoRedirect = false; requestSet.AllowAutoRedirect = false;
requestSet.StoreResponseCookie = true; requestSet.StoreResponseCookie = true;
var responseSet = Subject.Get(requestSet); var responseSet = await Subject.GetAsync(requestSet);
var request = new HttpRequest($"https://{_httpBinHost}/get"); var request = new HttpRequest($"https://{_httpBinHost}/get");
var response = Subject.Get<HttpBinResource>(request); var response = await Subject.GetAsync<HttpBinResource>(request);
response.Resource.Headers.Should().NotContainKey("Cookie"); response.Resource.Headers.Should().NotContainKey("Cookie");
@@ -847,12 +856,12 @@ namespace NzbDrone.Common.Test.Http
} }
[Test] [Test]
public void should_correctly_use_basic_auth_with_basic_network_credential() public async Task should_correctly_use_basic_auth()
{ {
var request = new HttpRequest($"https://{_httpBinHost}/basic-auth/username/password"); var request = new HttpRequest($"https://{_httpBinHost}/basic-auth/username/password");
request.Credentials = new BasicNetworkCredential("username", "password"); request.Credentials = new BasicNetworkCredential("username", "password");
var response = Subject.Execute(request); var response = await Subject.ExecuteAsync(request);
response.StatusCode.Should().Be(HttpStatusCode.OK); response.StatusCode.Should().Be(HttpStatusCode.OK);
} }
@@ -70,15 +70,15 @@ namespace NzbDrone.Common.Test.InstrumentationTests
[TestCase(@"[Info] MigrationController: *** Migrating Database=readarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;token=mySecret;Enlist=False&username=mySecret;mypassword=mySecret;mypass=shouldkeep1;test_token=mySecret;password=123%@%_@!#^#@;use_password=mySecret;get_token=shouldkeep2;usetoken=shouldkeep3;passwrd=mySecret;")] [TestCase(@"[Info] MigrationController: *** Migrating Database=readarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;token=mySecret;Enlist=False&username=mySecret;mypassword=mySecret;mypass=shouldkeep1;test_token=mySecret;password=123%@%_@!#^#@;use_password=mySecret;get_token=shouldkeep2;usetoken=shouldkeep3;passwrd=mySecret;")]
// Announce URLs (passkeys) Magnet & Tracker // Announce URLs (passkeys) Magnet & Tracker
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2f9pr04sg601233210imaveql2tyu8xyui%2fannounce""}")] [TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2f9pr04sg601233210IMAveQL2tyu8xyui%2fannounce""}")]
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2ftracker.php%2f9pr04sg601233210imaveql2tyu8xyui%2fannounce""}")] [TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2ftracker.php%2f9pr04sg601233210IMAveQL2tyu8xyui%2fannounce""}")]
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2fannounce%2f9pr04sg601233210imaveql2tyu8xyui""}")] [TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2fannounce%2f9pr04sg601233210IMAveQL2tyu8xyui""}")]
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2fannounce.php%3fpasskey%3d9pr04sg601233210imaveql2tyu8xyui""}")] [TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2fannounce.php%3fpasskey%3d9pr04sg601233210IMAveQL2tyu8xyui""}")]
[TestCase(@"tracker"":""https://xxx.yyy/9pr04sg601233210imaveql2tyu8xyui/announce""}")] [TestCase(@"tracker"":""https://xxx.yyy/9pr04sg601233210IMAveQL2tyu8xyui/announce""}")]
[TestCase(@"tracker"":""https://xxx.yyy/tracker.php/9pr04sg601233210imaveql2tyu8xyui/announce""}")] [TestCase(@"tracker"":""https://xxx.yyy/tracker.php/9pr04sg601233210IMAveQL2tyu8xyui/announce""}")]
[TestCase(@"tracker"":""https://xxx.yyy/announce/9pr04sg601233210imaveql2tyu8xyui""}")] [TestCase(@"tracker"":""https://xxx.yyy/announce/9pr04sg601233210IMAveQL2tyu8xyui""}")]
[TestCase(@"tracker"":""https://xxx.yyy/announce.php?passkey=9pr04sg601233210imaveql2tyu8xyui""}")] [TestCase(@"tracker"":""https://xxx.yyy/announce.php?passkey=9pr04sg601233210IMAveQL2tyu8xyui""}")]
[TestCase(@"tracker"":""http://xxx.yyy/announce.php?passkey=9pr04sg601233210imaveql2tyu8xyui"",""info"":""http://xxx.yyy/info?a=b""")] [TestCase(@"tracker"":""http://xxx.yyy/announce.php?passkey=9pr04sg601233210IMAveQL2tyu8xyui"",""info"":""http://xxx.yyy/info?a=b""")]
// Notifiarr // Notifiarr
[TestCase(@"https://xxx.yyy/api/v1/notification/readarr/9pr04sg6-0123-3210-imav-eql2tyu8xyui")] [TestCase(@"https://xxx.yyy/api/v1/notification/readarr/9pr04sg6-0123-3210-imav-eql2tyu8xyui")]
@@ -162,7 +162,7 @@ namespace NzbDrone.Common.Extensions
{ {
if (text.IsNullOrWhiteSpace()) if (text.IsNullOrWhiteSpace())
{ {
throw new ArgumentNullException("text"); throw new ArgumentNullException(nameof(text));
} }
return text.IndexOfAny(Path.GetInvalidPathChars()) >= 0; return text.IndexOfAny(Path.GetInvalidPathChars()) >= 0;

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