Compare commits

...

118 Commits

Author SHA1 Message Date
Bogdan 7cc02f95af Fix filename for class 2025-06-10 13:21:32 +03:00
Bogdan 1f92bf6679 Fix fullscreen automation screenshots 2025-06-10 13:16:08 +03:00
Taloth Saldono a8d4aa6770 Used ReflectionOnly and/or public types where possible to avoid loading related assemblies unnecessarily. 2025-06-08 10:38:03 +03:00
Bogdan 7661b5bc87 Bump version to 0.4.18 2025-06-08 10:31:09 +03:00
Weblate 200ef600cd Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Ilbebino <tommasobellandi08@gmail.com>
Co-authored-by: NanderTGA <nander.roobaert@gmail.com>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
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/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_Hans/
Translation: Servarr/Readarr
2025-06-07 18:16:49 +03:00
Bogdan ad6228983b Skip failing tests 2025-06-03 11:18:20 +03:00
Servarr 582ec9f7ce Automated API Docs update 2025-06-02 00:04:28 +03:00
Taloth Saldono 525e855038 Fixed: Return remote image links for RemotePoster and RemoteCover
(cherry picked from commit 4219cdb3644f96e1e8f3178fe0a50430c1004506)

Fixes #4101
Closes #212
2025-06-01 23:55:06 +03:00
Weblate 7a629ed044 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Hugoren Martinako <aumpfbahn@gmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Youngzheimer <me@youngzheimer.com>
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/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt/
Translation: Servarr/Readarr
2025-05-04 21:07:53 +03:00
Bogdan 7f501322dd Bump version to 0.4.17 2025-05-04 21:07:14 +03:00
Samuel Mercer 18bca0b228 Fixed: Displayed root folder path getting truncated when adding an author with a long name 2025-05-01 13:53:47 +03:00
Bogdan 9ddac60b47 Bump version to 0.4.16 2025-04-20 09:01:31 +03:00
Bogdan bd8bc0b35b Pass messages with arguments to NLog in LoggerExtensions 2025-04-13 13:17:00 +03:00
Bogdan ae623f4481 Fixed: Use template for log messages in Import Books 2025-04-13 12:55:42 +03:00
Bogdan e67d133bb6 Mark as template for log progress messages 2025-04-13 12:55:42 +03:00
Weblate 772ea95ce4 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Hugoren Martinako <aumpfbahn@gmail.com>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
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/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ca/
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/de/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/el/
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/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/it/
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/nb_NO/
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/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/sv/
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/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/vi/
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
2025-04-13 10:09:39 +03:00
Bogdan 5459a7bb7e Bump version to 0.4.15 2025-04-13 09:48:49 +03:00
Servarr 614f98f9b4 Automated API Docs update 2025-04-08 22:09:31 +03:00
Bogdan 55763dae43 Fixed disabled options for SelectInput 2025-04-08 21:53:35 +03:00
Bogdan a362dab503 Console warnings for missing translations on development builds
(cherry picked from commit 67a1ecb0fea4e6c7dfdb68fbe3ef30d4c22398d8)

Closes #3863
2025-04-08 21:53:35 +03:00
Mark McDowall dba9fbf254 Fixed: Trying to add an author when root folders hadn't populated
(cherry picked from commit a6d0dddaf7fa51f334e32d4fd49486d06fb6ba65)
2025-04-08 21:41:48 +03:00
Bogdan 59a7605385 Improve validation message for AuthorFolderAsRootFolderValidator
(cherry picked from commit a117001de673e80abd90d54a34a7c86292b3a649)
2025-04-08 21:41:48 +03:00
Mark McDowall f819e582cf New: Author folder hint when selecting a root folder while adding a new author
(cherry picked from commit dd09f31abb4dd3f699bcff0a47577075300c70ee)

Fix AuthorFolderAsRootFolderValidator

(cherry picked from commit 0ce81e1ab69d43fde382cc4ae22cd46fe626dea7)
2025-04-08 21:41:48 +03:00
Weblate 0972d41bf8 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Hugoren Martinako <aumpfbahn@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ca/
Translation: Servarr/Readarr
2025-04-08 17:28:32 +03:00
Servarr 05d2335bfe Automated API Docs update 2025-04-08 17:27:23 +03:00
Bogdan 5b4f54a959 Prevent NullRef for cases when media covers have nullable urls
(cherry picked from commit a26df9e9afa8d925c2ad62c126d4edebec7e4e54)

Closes #2981
2025-04-08 16:30:39 +03:00
Taloth Saldono bb5ad605fd Fixed: Posters not always showing when searching for new authors
(cherry picked from commit 10dc884fa87a8337e9f0622c269adede0b262029)

Co-authored-by: optimous012

Closes #145
2025-04-08 16:30:33 +03:00
Bogdan 52c3a95e63 Bump browserslist-db 2025-04-08 15:52:06 +03:00
Mark McDowall 52c5460537 New: Prevent Remote Path Mapping local folder being set to System folder or '/'
(cherry picked from commit 0f904e091702a2ac53771ee3aeb5aafe62688035)
2025-04-08 15:48:56 +03:00
Mark McDowall 280cec3d0e Fixed: Set output encoding to UTF-8 when running external processes
(cherry picked from commit f8e57b09856278a6d0c65f18704e96a33459687d)
2025-04-08 15:46:48 +03:00
Mark McDowall f10c2c01d8 Update WikiUrl type in API docs
(cherry picked from commit 9bd619ccfe074abe396bbf043a36a5be18a7ba4b)
2025-04-08 15:46:26 +03:00
Bogdan 2b6a328dac Bump Selenium.WebDriver.ChromeDriver 2025-04-08 15:46:14 +03:00
Bogdan 4078525f67 Log delete statements only once 2025-04-08 15:45:20 +03:00
Bogdan 214e4270ac Fixed: Disallow tags creation with empty label 2025-04-08 15:45:10 +03:00
Bogdan 5396dd3e8e Bump linux agent to ubuntu-22.04 2025-04-02 00:11:18 +03:00
Bogdan d5d4996c40 Bump version to 0.4.14 2025-03-30 10:29:09 +03:00
Weblate 8b2223a9c4 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ca/
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/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/ko/
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/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translation: Servarr/Readarr
2025-03-28 10:48:17 +02:00
Lasidar 2a3e7f8dae Improve Author status details in UI 2025-03-28 10:46:55 +02:00
Bogdan 6406ed6289 Fixed: Include Book for history/since
Fixes #4011
2025-03-27 19:54:30 +02:00
Bogdan d5b0831b0f Improve appearance for loading errors on author details 2025-03-26 12:49:19 +02:00
Bogdan 8d72d5dbab Convert QualityProfileName to TypeScript 2025-03-26 12:45:53 +02:00
Bogdan 74d1ab84e2 Bump browserslist-db 2025-03-26 12:35:30 +02:00
Mark McDowall 7341d20c51 Fixed: Deleting author folder fails when files/folders aren't instantly removed
(cherry picked from commit c84699ed5d5a2f59f236c26a8999d25a1102ec02)
2025-03-26 12:35:30 +02:00
Bogdan 5abf4f2992 Cleanup unused sorting fields for bulk manage providers
(cherry picked from commit 6115236d3853f70a18b73aef15ebe4e18ab48e40)
2025-03-26 12:35:30 +02:00
Bogdan cc90050c77 New: Display indexer in download failed details
(cherry picked from commit a324052debf63a8db73a2f3c79201864892bb62c)
2025-03-26 12:35:30 +02:00
Bogdan 7ba8f8baee Fixed: Inherit indexer, size and release group for marked as failed history
(cherry picked from commit e08c9d5501e65aabce3456b2dd7571867508d88f)
2025-03-26 12:35:30 +02:00
Mark McDowall 70aec175ef Improve logging when login fails due to CryptographicException
(cherry picked from commit 1449941471cbb8885e9298317b9a30f2576d7941)
2025-03-26 12:35:30 +02:00
Bogdan 080dd301f3 Fixed: Priority validation for indexers and download clients
(cherry picked from commit f0e320f3aa501f120721503b8256f464a31be783)
2025-03-26 12:35:30 +02:00
Mark McDowall bab45481db Fixed: Allow tables to scroll on tablets in portrait mode
(cherry picked from commit 5fb632eb46cf77ea4f61d407f6429d9c32dba766)
2025-03-26 12:35:30 +02:00
Mark McDowall 3e1e03e0ce Fixed: Drop downs flickering in some cases
(cherry picked from commit 3b024443c5447b7638a69a99809bf44b2419261f)
2025-03-26 12:35:30 +02:00
Bogdan bb599d6dc9 Bump Npgsql, System.Memory and System.ValueTuple 2025-03-26 12:35:25 +02:00
Weblate c94685842f Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ru/
Translation: Servarr/Readarr
2025-03-26 12:17:43 +02:00
Bogdan 5966f6da51 Bump version to 0.4.13 2025-03-09 11:51:20 +02:00
Bogdan 229e0dfe5d Bump SixLabors.ImageSharp and Polly 2025-03-07 23:33:05 +02:00
Bogdan 5173daa265 Bump version to 0.4.12 2025-03-02 19:06:49 +02:00
Bogdan c2f770f242 Fixed: Instance name must contain application name 2025-03-01 13:36:36 +02:00
Bogdan 0b7ce67635 Use develop branch for update package tests 2025-02-23 22:12:25 +02:00
Weblate bc74456944 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
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/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/it/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translation: Servarr/Readarr
2025-02-18 12:00:58 +02:00
Bogdan fa460567a7 New: Bypass IP addresses ranges in proxies
(cherry picked from commit 402db9128c214d4c5af6583643cb49d3aa7a28b5)

Closes #3690
2025-02-18 11:55:14 +02:00
bakerboy448 7dfceb307b Fixed: Trim spaces and empty values in Proxy Bypass List
(cherry picked from commit 846333ddf0d9da775c80d004fdb9b41e700ef359)

Closes #3688
2025-02-18 11:52:30 +02:00
Weblate 305ad235a5 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fi/
Translation: Servarr/Readarr
2025-02-16 18:39:51 +02:00
Servarr 74c20e41bf Automated API Docs update 2025-02-16 18:01:02 +02:00
ManiMatter 347289b173 New: Last Searched column on Wanted screens 2025-02-16 17:55:01 +02:00
Bogdan 0ef3d2a5cc Fix download links for FileList when passkey contains spaces 2025-02-16 12:21:34 +02:00
Mark McDowall e5519d60c9 Upgrade node to 20.11.1
(cherry picked from commit c6071f6d81a968d3ed7c6bf4bae035961b54d128)
2025-02-13 12:42:57 +02:00
Weblate 3a85b3a060 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Craze <christian.strey@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com>
Co-authored-by: Mailme Dashite <mailmedashite@protonmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: ahgharaghani <ah.gharaghani@gmail.com>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: warkurre86 <tom.novo.86@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/
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/de/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fa/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ko/
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_Hans/
Translation: Servarr/Readarr
2025-02-07 19:14:45 -06:00
Bogdan c1cdf44322 Bump version to 0.4.11 2025-01-26 14:59:57 +02:00
Bogdan f861e54139 Rename MusicbrainzId references to ForeignId 2025-01-20 16:45:27 +02:00
jcassette 279e1029e0 New: reflink support for ZFS
(cherry picked from commit a840bb542362d58006b6cc27affd58ee6b965b80)
2025-01-19 17:18:38 +02:00
Bogdan b9ed39175b Bump version to 0.4.10 2025-01-19 17:15:27 +02:00
Bogdan faba3ada95 Bump Polly, System.Buffers and System.Memory
Closes #3952
2025-01-15 18:23:01 +02:00
Qstick e8647aee05 Bump SonarCloud azure extension to 3.X
(cherry picked from commit 7b8e352d876cd8f8e5b6296f0c3938bed4db8bb8)

Bump SonarCloud azure extension for UI analysis to 3.X

(cherry picked from commit 396b2ae7c10c7df749ea23ea93608b56482175a1)
2025-01-15 18:13:04 +02:00
Weblate eaf5ce52bc Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: 1 <1228553526@qq.com>
Co-authored-by: Alexander Balya <alexander.balya@gmail.com>
Co-authored-by: Ano10 <Ano10@users.noreply.translate.servarr.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Matti Meikäläinen <diefor-93@hotmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Tommy Au <smarttommyau@gmail.com>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: marapavelka <mara.pavelka@gmail.com>
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/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/ko/
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_TW/
Translation: Servarr/Readarr
2025-01-12 15:18:43 +02:00
Bogdan 73ab2760e4 Bump version to 0.4.9 2025-01-12 15:17:47 +02:00
Bogdan 3bb036e8c6 Fixed warning for central package version management 2025-01-09 22:09:48 +02:00
Bogdan 6e05456d6a Set minor version for core-js in babel/preset-env
(cherry picked from commit 2e83d59f61957cbc2171bef097fe2410e72729ad)

Closes #3941
2025-01-05 14:41:58 +02:00
Bogdan 8563a42822 Update core-js 2025-01-05 14:41:51 +02:00
Mark McDowall 841d38f4a5 Upgrade babel to 7.26.0
(cherry picked from commit bfcd017012730c97eb587ae2d2e91f72ee7a1de3)

Closes #3943
2025-01-05 14:40:02 +02:00
Mark McDowall 9326d88eb6 Upgrade Font Awesome to 6.7.1
(cherry picked from commit 016b5718386593c030f14fcac307c93ee1ceeca6)

Closes #3944
2025-01-05 14:38:03 +02:00
Bogdan 015da61004 Bump MailKit to 4.8.0 and Microsoft.Data.SqlClient to 2.1.7
Closes #3951
2025-01-05 14:37:16 +02:00
Mark McDowall d02ea4b121 Don't send session information to Sentry
(cherry picked from commit fae24e98fb9230c2f3701caef457332952c6723f)

Closes #3957
2025-01-05 14:31:17 +02:00
Bruno Garcia 7bc9d700f9 Update Sentry SDK add features
Co-authored-by: Stefan Jandl <reg@bitfox.at>
(cherry picked from commit 6377c688fc7b35749d608bf62796446bb5bcb11b)
2025-01-05 14:30:42 +02:00
Stevie Robinson 661d72ef9b Fixed: Listening on all IPv4 Addresses
(cherry picked from commit 035c474f10c257331a5f47e863d24af82537e335)
2025-01-05 14:26:30 +02:00
Stevie Robinson 258a8d1c95 Fixed: qBittorrent Ratio Limit Check
(cherry picked from commit 4dcc015fb19ceb57d2e8f4985c5137e765829d1c)
2025-01-05 14:26:17 +02:00
Bogdan d4459b9475 Bump version to 0.4.8 2025-01-05 14:26:01 +02:00
Bogdan a550c6554f Check if backup folder is writable on backup
(cherry picked from commit 8aad79fd3e14eb885724a5e5790803c289be2f25)

Closes #3961
2024-12-31 12:21:54 +02:00
Bogdan c1b26eec8d Suggest adding IP to RPC whitelist for on failed Transmission auth
(cherry picked from commit f05e552e8e6dc02cd26444073ab9a678dcb36492)
2024-12-31 12:20:58 +02:00
Bogdan ffe5ede55d Bump version to 0.4.7 2024-12-22 13:25:47 +02:00
gains goblin 9005860899 Fixed: Mapping Author GR ID from import lists to AuthorGoodReadsId 2024-12-15 16:28:18 -06:00
Bogdan c67f67109e Ignore metadata tests temporarily once again 2024-12-15 23:05:58 +02:00
Bogdan 51b9744e25 Fixed: Refresh backup list on deletion
(cherry picked from commit 3b00112447361b19c04851a510e63f812597a043)
2024-12-15 05:32:13 +02:00
Mark McDowall 334d824633 Fixed: Error getting processes in some cases
(cherry picked from commit b552d4e9f7ca7388404aa0d52566010a54cb0244)
2024-12-15 05:31:53 +02:00
Weblate ae01387ca9 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dani Talens <databio@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: hhjuhl <hans@kopula.dk>
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/tr/
Translation: Servarr/Readarr
2024-12-14 02:35:17 +02:00
Weblate 4eb13e0938 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: 4kwins <hanszimmerme@gmail.com>
Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Ardenet <1213193613@qq.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Mizuyoru_TW <mizuyoru.tw@gmail.com>
Co-authored-by: Robin Dadswell <robin@robindadswell.tech>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: keysuck <joshkkim@gmail.com>
Co-authored-by: mryx007 <mryx@mail.de>
Co-authored-by: thelooter <evekolb2204@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/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_HANS/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_Hans/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_TW/
Translation: Servarr/Readarr
2024-12-08 12:36:16 +02:00
Servarr 6dbb826f2f Automated API Docs update 2024-12-08 12:35:20 +02:00
Bogdan 52dfa57dd7 Bump version to 0.4.6 2024-12-08 12:26:41 +02:00
Mark McDowall f354b3bc47 New: Support for new SABnzbd history retention values
(cherry picked from commit e361f18837d98c089f7dc9c0190221ca8e2cf225)

Closes #3885
2024-12-04 18:03:20 +02:00
Bogdan 2d9e6788e6 Bump Polly, Npgsql, PdfSharpCore and ImageSharp 2024-12-04 18:02:21 +02:00
soup 0d121fe9c0 New: Add config file setting for CGNAT authentication bypass
(cherry picked from commit 4c41a4f368046f73f82306bbd73bec992392938b)

Closes #3903
2024-12-04 17:56:17 +02:00
Emmanuel Ferdman 892c34fe35 Fix license link in API docs (#3910)
Signed-off-by: Emmanuel Ferdman <emmanuelferdman@gmail.com>
2024-12-02 17:15:09 +02:00
Gylesie 24f6007594 Remove unnecessary heap allocations in local IP check
(cherry picked from commit ed536a85ad5f2062bf6f01f80efddb19fa935f63)
2024-12-02 02:34:56 +02:00
Mark McDowall 5028ed4027 Webpack web target
(cherry picked from commit a90866a73e6cff9a286c23e60c74672f4c0d317a)
2024-11-27 12:25:08 +02:00
Bogdan 05f303436b Bump version to 0.4.5 2024-11-23 19:52:18 +02:00
Bogdan 5635de96a8 Fixed: Initial state for qBittorrent v5.0
(cherry picked from commit ff724b7f4099284b8062f1625cf07b7822782edf)
2024-11-15 20:01:26 -06:00
Mickaël Thomas ce59f32023 New: Support stoppedUP and stoppedDL states from qBittorrent
(cherry picked from commit 73a4bdea5247ee87e6bbae95f5325e1f03c88a7f)
2024-11-15 20:01:26 -06:00
bakerboy448 6d675a5207 Fix Goodreads test 2024-11-15 17:57:51 -06:00
Bogdan b093b23900 Pin ReportGenerator in Azure Pipelines for .NET 6
(cherry picked from commit 50ce480abf043140e209d2d2959fbea8dd5dd2ab)
2024-11-15 15:45:16 -06:00
Weblate 884ac2cb6f Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Fixer <ygj59783@zslsz.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ro/
Translation: Servarr/Readarr
2024-11-07 17:04:03 +02:00
Weblate 295a6c4255 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
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/de/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/el/
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/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/id/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/it/
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/nb_NO/
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/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/sv/
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/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translation: Servarr/Readarr
2024-11-05 02:26:50 +02:00
Mark McDowall 74a59d5790 Use current time for cache break in development
(cherry picked from commit 020ed32fcfab1c6fbe57af5ea650300272c93fd7)
2024-11-05 02:25:32 +02:00
Bogdan ae23e5f187 Bump version to 0.4.4 2024-11-03 11:43:03 +02:00
Bogdan ba2add0d54 Fix app name in translations 2024-11-02 21:33:05 +02:00
Servarr b6ebeb31c8 Multiple Translations updated by Weblate (#3723)
ignore-downstream












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/hr/
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/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translation: Servarr/Readarr

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Ardenet <1213193613@qq.com>
Co-authored-by: Kuzmich <kuzmich55@gmail.com>
Co-authored-by: Mathias <mathias@rodilbach.dk>
Co-authored-by: angelsky11 <angelsky11@gmail.com>
Co-authored-by: genoher <genoher@gmail.com>
Co-authored-by: jsain <josip.sain@gmail.com>
Co-authored-by: liuwqq <843384478@qq.com>
Co-authored-by: Bogdan <mynameisbogdan@users.noreply.github.com>
2024-11-02 21:29:00 +02:00
Bogdan b8bd645560 Fixed: Prevent UI errors for authors with invalid quality and metadata profiles 2024-11-02 01:04:11 +02:00
Bogdan e0d904fa69 Improve message for grab errors due to no matching tags
Co-authored-by: zakary <zak@ary.dev>
(cherry picked from commit df672487cf1d5f067849367a2bfb0068defc315d)

Closes #3814
2024-10-27 09:50:14 +02:00
Bogdan cb532caca4 Fixed: Status check for completed directories in Deluge
(cherry picked from commit 33139d4b53c1adad769c7e2b0510e8990c66b84a)
2024-10-27 09:47:15 +02:00
Bogdan e1af8ad37f Bump version to 0.4.3 2024-10-27 09:46:46 +02:00
Bogdan c4f30da648 Cleanse exceptions in event logs
(cherry picked from commit 404e6d68ea526ab521cd39ecda1bf3b02285765d)
2024-10-27 09:45:22 +02:00
188 changed files with 5164 additions and 2980 deletions
+9 -9
View File
@@ -9,7 +9,7 @@ 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.4.2' majorVersion: '0.4.18'
minorVersion: $[counter('minorVersion', 1)] minorVersion: $[counter('minorVersion', 1)]
readarrVersion: '$(majorVersion).$(minorVersion)' readarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(readarrVersion)' buildName: '$(Build.SourceBranchName).$(readarrVersion)'
@@ -19,7 +19,7 @@ variables:
nodeVersion: '20.X' nodeVersion: '20.X'
innoVersion: '6.2.0' innoVersion: '6.2.0'
windowsImage: 'windows-2022' windowsImage: 'windows-2022'
linuxImage: 'ubuntu-20.04' linuxImage: 'ubuntu-22.04'
macImage: 'macOS-13' macImage: 'macOS-13'
trigger: trigger:
@@ -1102,19 +1102,19 @@ stages:
vmImage: ${{ variables.windowsImage }} vmImage: ${{ variables.windowsImage }}
steps: steps:
- checkout: self # Need history for Sonar analysis - checkout: self # Need history for Sonar analysis
- task: SonarCloudPrepare@2 - task: SonarCloudPrepare@3
env: env:
SONAR_SCANNER_OPTS: '' SONAR_SCANNER_OPTS: ''
inputs: inputs:
SonarCloud: 'SonarCloud' SonarCloud: 'SonarCloud'
organization: 'readarr' organization: 'readarr'
scannerMode: 'CLI' scannerMode: 'cli'
configMode: 'manual' configMode: 'manual'
cliProjectKey: 'readarrui' cliProjectKey: 'readarrui'
cliProjectName: 'ReadarrUI' cliProjectName: 'ReadarrUI'
cliProjectVersion: '$(readarrVersion)' cliProjectVersion: '$(readarrVersion)'
cliSources: './frontend' cliSources: './frontend'
- task: SonarCloudAnalyze@2 - task: SonarCloudAnalyze@3
- job: Api_Docs - job: Api_Docs
displayName: API Docs displayName: API Docs
@@ -1190,12 +1190,12 @@ stages:
submodules: true submodules: true
- powershell: Set-Service SCardSvr -StartupType Manual - powershell: Set-Service SCardSvr -StartupType Manual
displayName: Enable Windows Test Service displayName: Enable Windows Test Service
- task: SonarCloudPrepare@2 - task: SonarCloudPrepare@3
condition: eq(variables['System.PullRequest.IsFork'], 'False') condition: eq(variables['System.PullRequest.IsFork'], 'False')
inputs: inputs:
SonarCloud: 'SonarCloud' SonarCloud: 'SonarCloud'
organization: 'readarr' organization: 'readarr'
scannerMode: 'MSBuild' scannerMode: 'dotnet'
projectKey: 'Readarr_Readarr' projectKey: 'Readarr_Readarr'
projectName: 'Readarr' projectName: 'Readarr'
projectVersion: '$(readarrVersion)' projectVersion: '$(readarrVersion)'
@@ -1208,10 +1208,10 @@ stages:
./build.sh --backend -f net6.0 -r win-x64 ./build.sh --backend -f net6.0 -r win-x64
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
displayName: Coverage Unit Tests displayName: Coverage Unit Tests
- task: SonarCloudAnalyze@2 - task: SonarCloudAnalyze@3
condition: eq(variables['System.PullRequest.IsFork'], 'False') condition: eq(variables['System.PullRequest.IsFork'], 'False')
displayName: Publish SonarCloud Results displayName: Publish SonarCloud Results
- task: reportgenerator@5 - task: reportgenerator@5.3.11
displayName: Generate Coverage Report displayName: Generate Coverage Report
inputs: inputs:
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml' reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
+2 -1
View File
@@ -26,6 +26,7 @@ module.exports = (env) => {
const config = { const config = {
mode: isProduction ? 'production' : 'development', mode: isProduction ? 'production' : 'development',
devtool: isProduction ? 'source-map' : 'eval-source-map', devtool: isProduction ? 'source-map' : 'eval-source-map',
target: 'web',
stats: { stats: {
children: false children: false
@@ -181,7 +182,7 @@ module.exports = (env) => {
loose: true, loose: true,
debug: false, debug: false,
useBuiltIns: 'entry', useBuiltIns: 'entry',
corejs: 3 corejs: '3.39'
} }
] ]
] ]
@@ -165,7 +165,8 @@ function HistoryDetails(props) {
if (eventType === 'downloadFailed') { if (eventType === 'downloadFailed') {
const { const {
message message,
indexer
} = data; } = data;
return ( return (
@@ -177,11 +178,21 @@ function HistoryDetails(props) {
/> />
{ {
!!message && indexer ?
<DescriptionListItem
title={translate('Indexer')}
data={indexer}
/> :
null
}
{
message ?
<DescriptionListItem <DescriptionListItem
title={translate('Message')} title={translate('Message')}
data={message} data={message}
/> /> :
null
} }
</DescriptionList> </DescriptionList>
); );
+3
View File
@@ -1,5 +1,7 @@
import ModelBase from 'App/ModelBase'; import ModelBase from 'App/ModelBase';
export type AuthorStatus = 'continuing' | 'ended';
interface Author extends ModelBase { interface Author extends ModelBase {
added: string; added: string;
genres: string[]; genres: string[];
@@ -10,6 +12,7 @@ interface Author extends ModelBase {
metadataProfileId: number; metadataProfileId: number;
rootFolderPath: string; rootFolderPath: string;
sortName: string; sortName: string;
status: AuthorStatus;
tags: number[]; tags: number[];
authorName: string; authorName: string;
isSaving?: boolean; isSaving?: boolean;
+21
View File
@@ -0,0 +1,21 @@
import { AuthorStatus } from 'Author/Author';
import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
export function getAuthorStatusDetails(status: AuthorStatus) {
let statusDetails = {
icon: icons.AUTHOR_CONTINUING,
title: translate('StatusEndedContinuing'),
message: translate('ContinuingMoreBooksAreExpected'),
};
if (status === 'ended') {
statusDetails = {
icon: icons.AUTHOR_ENDED,
title: translate('StatusEndedEnded'),
message: translate('ContinuingNoAdditionalBooksAreExpected'),
};
}
return statusDetails;
}
+13 -9
View File
@@ -7,6 +7,7 @@ import AuthorHistoryTable from 'Author/History/AuthorHistoryTable';
import MonitoringOptionsModal from 'Author/MonitoringOptions/MonitoringOptionsModal'; import MonitoringOptionsModal from 'Author/MonitoringOptions/MonitoringOptionsModal';
import BookEditorFooter from 'Book/Editor/BookEditorFooter'; import BookEditorFooter from 'Book/Editor/BookEditorFooter';
import BookFileEditorTable from 'BookFile/Editor/BookFileEditorTable'; import BookFileEditorTable from 'BookFile/Editor/BookFileEditorTable';
import Alert from 'Components/Alert';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
@@ -17,7 +18,7 @@ import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import SwipeHeaderConnector from 'Components/Swipe/SwipeHeaderConnector'; import SwipeHeaderConnector from 'Components/Swipe/SwipeHeaderConnector';
import { align, icons } from 'Helpers/Props'; import { align, icons, kinds } from 'Helpers/Props';
import InteractiveSearchFilterMenuConnector from 'InteractiveSearch/InteractiveSearchFilterMenuConnector'; import InteractiveSearchFilterMenuConnector from 'InteractiveSearch/InteractiveSearchFilterMenuConnector';
import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable'; import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable';
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector'; import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
@@ -412,22 +413,25 @@ class AuthorDetails extends Component {
<div className={styles.contentContainer}> <div className={styles.contentContainer}>
{ {
!isPopulated && !booksError && !bookFilesError && !isPopulated && !booksError && !bookFilesError ?
<LoadingIndicator /> <LoadingIndicator /> :
null
} }
{ {
!isFetching && booksError && !isFetching && booksError ?
<div> <Alert kind={kinds.DANGER}>
{translate('LoadingBooksFailed')} {translate('LoadingBooksFailed')}
</div> </Alert> :
null
} }
{ {
!isFetching && bookFilesError && !isFetching && bookFilesError ?
<div> <Alert kind={kinds.DANGER}>
{translate('LoadingBookFilesFailed')} {translate('LoadingBookFilesFailed')}
</div> </Alert> :
null
} }
{ {
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import TextTruncate from 'react-text-truncate'; import TextTruncate from 'react-text-truncate';
import AuthorPoster from 'Author/AuthorPoster'; import AuthorPoster from 'Author/AuthorPoster';
import { getAuthorStatusDetails } from 'Author/AuthorStatus';
import HeartRating from 'Components/HeartRating'; import HeartRating from 'Components/HeartRating';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import Label from 'Components/Label'; import Label from 'Components/Label';
@@ -11,7 +12,7 @@ import MonitorToggleButton from 'Components/MonitorToggleButton';
import Popover from 'Components/Tooltip/Popover'; import Popover from 'Components/Tooltip/Popover';
import Tooltip from 'Components/Tooltip/Tooltip'; import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props'; import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector'; import QualityProfileName from 'Settings/Profiles/Quality/QualityProfileName';
import fonts from 'Styles/Variables/fonts'; import fonts from 'Styles/Variables/fonts';
import formatBytes from 'Utilities/Number/formatBytes'; import formatBytes from 'Utilities/Number/formatBytes';
import stripHtml from 'Utilities/String/stripHtml'; import stripHtml from 'Utilities/String/stripHtml';
@@ -87,11 +88,11 @@ class AuthorDetailsHeader extends Component {
titleWidth titleWidth
} = this.state; } = this.state;
const statusDetails = getAuthorStatusDetails(status);
const fanartUrl = getFanartUrl(images); const fanartUrl = getFanartUrl(images);
const marqueeWidth = titleWidth - (isSmallScreen ? 85 : 160); const marqueeWidth = titleWidth - (isSmallScreen ? 85 : 160);
const continuing = status === 'continuing';
let bookFilesCountMessage = translate('BookFilesCountMessage'); let bookFilesCountMessage = translate('BookFilesCountMessage');
if (bookFileCount === 1) { if (bookFileCount === 1) {
@@ -213,7 +214,7 @@ class AuthorDetailsHeader extends Component {
<span className={styles.qualityProfileName}> <span className={styles.qualityProfileName}>
{ {
<QualityProfileNameConnector <QualityProfileName
qualityProfileId={qualityProfileId} qualityProfileId={qualityProfileId}
/> />
} }
@@ -236,16 +237,16 @@ class AuthorDetailsHeader extends Component {
<Label <Label
className={styles.detailsLabel} className={styles.detailsLabel}
title={continuing ? translate('ContinuingMoreBooksAreExpected') : translate('ContinuingNoAdditionalBooksAreExpected')} title={statusDetails.message}
size={sizes.LARGE} size={sizes.LARGE}
> >
<Icon <Icon
name={continuing ? icons.AUTHOR_CONTINUING : icons.AUTHOR_ENDED} name={statusDetails.icon}
size={17} size={17}
/> />
<span className={styles.qualityProfileName}> <span className={styles.qualityProfileName}>
{continuing ? 'Continuing' : 'Deceased'} {statusDetails.title}
</span> </span>
</Label> </Label>
@@ -5,6 +5,7 @@ import dimensions from 'Styles/Variables/dimensions';
import formatDateTime from 'Utilities/Date/formatDateTime'; import formatDateTime from 'Utilities/Date/formatDateTime';
import getRelativeDate from 'Utilities/Date/getRelativeDate'; import getRelativeDate from 'Utilities/Date/getRelativeDate';
import formatBytes from 'Utilities/Number/formatBytes'; import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
import AuthorIndexOverviewInfoRow from './AuthorIndexOverviewInfoRow'; import AuthorIndexOverviewInfoRow from './AuthorIndexOverviewInfoRow';
import styles from './AuthorIndexOverviewInfo.css'; import styles from './AuthorIndexOverviewInfo.css';
@@ -76,9 +77,9 @@ function getInfoRowProps(row, props) {
}; };
} }
if (name === 'qualityProfileId') { if (name === 'qualityProfileId' && !!props.qualityProfile?.name) {
return { return {
title: 'Quality Profile', title: translate('QualityProfile'),
iconName: icons.PROFILE, iconName: icons.PROFILE,
label: props.qualityProfile.name label: props.qualityProfile.name
}; };
@@ -235,12 +235,12 @@ class AuthorIndexPoster extends Component {
</div> </div>
} }
{ {showQualityProfile && !!qualityProfile?.name ? (
showQualityProfile && <div className={styles.title} title={translate('QualityProfile')}>
<div className={styles.title}> {qualityProfile.name}
{qualityProfile.name} </div>
</div> ) : null}
}
{ {
nextAiring && nextAiring &&
<div className={styles.nextAiring}> <div className={styles.nextAiring}>
@@ -209,7 +209,7 @@ class AuthorIndexRow extends Component {
key={name} key={name}
className={styles[name]} className={styles[name]}
> >
{qualityProfile.name} {qualityProfile?.name ?? ''}
</VirtualTableRowCell> </VirtualTableRowCell>
); );
} }
@@ -220,7 +220,7 @@ class AuthorIndexRow extends Component {
key={name} key={name}
className={styles[name]} className={styles[name]}
> >
{metadataProfile.name} {metadataProfile?.name ?? ''}
</VirtualTableRowCell> </VirtualTableRowCell>
); );
} }
@@ -1,5 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { getAuthorStatusDetails } from 'Author/AuthorStatus';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import VirtualTableRowCell from 'Components/Table/Cells/TableRowCell'; import VirtualTableRowCell from 'Components/Table/Cells/TableRowCell';
import { icons } from 'Helpers/Props'; import { icons } from 'Helpers/Props';
@@ -15,6 +16,8 @@ function AuthorStatusCell(props) {
...otherProps ...otherProps
} = props; } = props;
const statusDetails = getAuthorStatusDetails(status);
return ( return (
<Component <Component
className={className} className={className}
@@ -28,8 +31,8 @@ function AuthorStatusCell(props) {
<Icon <Icon
className={styles.statusIcon} className={styles.statusIcon}
name={status === 'ended' ? icons.AUTHOR_ENDED : icons.AUTHOR_CONTINUING} name={statusDetails.icon}
title={status === 'ended' ? translate('StatusEndedDeceased') : translate('StatusEndedContinuing')} title={`${statusDetails.title}: ${statusDetails.message}`}
/> />
</Component> </Component>
); );
+3 -2
View File
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Button from 'Components/Link/Button'; import Button from 'Components/Link/Button';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './NoAuthor.css'; import styles from './NoAuthor.css';
function NoAuthor(props) { function NoAuthor(props) {
@@ -31,7 +32,7 @@ function NoAuthor(props) {
to="/settings/mediamanagement" to="/settings/mediamanagement"
kind={kinds.PRIMARY} kind={kinds.PRIMARY}
> >
Add Root Folder {translate('AddRootFolder')}
</Button> </Button>
</div> </div>
@@ -40,7 +41,7 @@ function NoAuthor(props) {
to="/add/search" to="/add/search"
kind={kinds.PRIMARY} kind={kinds.PRIMARY}
> >
Add New Author {translate('AddNewAuthor')}
</Button> </Button>
</div> </div>
</div> </div>
@@ -5,6 +5,7 @@ import dimensions from 'Styles/Variables/dimensions';
import formatDateTime from 'Utilities/Date/formatDateTime'; import formatDateTime from 'Utilities/Date/formatDateTime';
import getRelativeDate from 'Utilities/Date/getRelativeDate'; import getRelativeDate from 'Utilities/Date/getRelativeDate';
import formatBytes from 'Utilities/Number/formatBytes'; import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
import BookIndexOverviewInfoRow from './BookIndexOverviewInfoRow'; import BookIndexOverviewInfoRow from './BookIndexOverviewInfoRow';
import styles from './BookIndexOverviewInfo.css'; import styles from './BookIndexOverviewInfo.css';
@@ -71,9 +72,9 @@ function getInfoRowProps(row, props) {
}; };
} }
if (name === 'qualityProfileId') { if (name === 'qualityProfileId' && !!props.qualityProfile?.name) {
return { return {
title: 'Quality Profile', title: translate('QualityProfile'),
iconName: icons.PROFILE, iconName: icons.PROFILE,
label: props.qualityProfile.name label: props.qualityProfile.name
}; };
@@ -250,12 +250,12 @@ class BookIndexPoster extends Component {
</div> </div>
} }
{ {showQualityProfile && !!qualityProfile?.name ? (
showQualityProfile && <div className={styles.title} title={translate('QualityProfile')}>
<div className={styles.title}> {qualityProfile.name}
{qualityProfile.name} </div>
</div> ) : null}
}
{ {
nextAiring && nextAiring &&
<div className={styles.nextAiring}> <div className={styles.nextAiring}>
@@ -195,7 +195,7 @@ class BookIndexRow extends Component {
key={name} key={name}
className={styles[name]} className={styles[name]}
> >
{qualityProfile.name} {qualityProfile?.name ?? ''}
</VirtualTableRowCell> </VirtualTableRowCell>
); );
} }
+5 -4
View File
@@ -1,12 +1,11 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import AuthorNameLink from 'Author/AuthorNameLink'; import AuthorNameLink from 'Author/AuthorNameLink';
import { getAuthorStatusDetails } from 'Author/AuthorStatus';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import MonitorToggleButton from 'Components/MonitorToggleButton'; import MonitorToggleButton from 'Components/MonitorToggleButton';
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell'; import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell'; import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import BookshelfBook from './BookshelfBook'; import BookshelfBook from './BookshelfBook';
import styles from './BookshelfRow.css'; import styles from './BookshelfRow.css';
@@ -30,6 +29,8 @@ class BookshelfRow extends Component {
onBookMonitoredPress onBookMonitoredPress
} = this.props; } = this.props;
const statusDetails = getAuthorStatusDetails(status);
return ( return (
<> <>
<VirtualTableSelectCell <VirtualTableSelectCell
@@ -52,8 +53,8 @@ class BookshelfRow extends Component {
<VirtualTableRowCell className={styles.status}> <VirtualTableRowCell className={styles.status}>
<Icon <Icon
className={styles.statusIcon} className={styles.statusIcon}
name={status === 'ended' ? icons.AUTHOR_ENDED : icons.AUTHOR_CONTINUING} name={statusDetails.icon}
title={status === 'ended' ? translate('StatusEndedEnded') : translate('StatusEndedContinuing')} title={statusDetails.title}
/> />
</VirtualTableRowCell> </VirtualTableRowCell>
@@ -20,6 +20,8 @@ import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
import TextInput from './TextInput'; import TextInput from './TextInput';
import styles from './EnhancedSelectInput.css'; import styles from './EnhancedSelectInput.css';
const MINIMUM_DISTANCE_FROM_EDGE = 10;
function isArrowKey(keyCode) { function isArrowKey(keyCode) {
return keyCode === keyCodes.UP_ARROW || keyCode === keyCodes.DOWN_ARROW; return keyCode === keyCodes.UP_ARROW || keyCode === keyCodes.DOWN_ARROW;
} }
@@ -137,18 +139,9 @@ class EnhancedSelectInput extends Component {
// Listeners // Listeners
onComputeMaxHeight = (data) => { onComputeMaxHeight = (data) => {
const {
top,
bottom
} = data.offsets.reference;
const windowHeight = window.innerHeight; const windowHeight = window.innerHeight;
if ((/^botton/).test(data.placement)) { data.styles.maxHeight = windowHeight - MINIMUM_DISTANCE_FROM_EDGE;
data.styles.maxHeight = windowHeight - bottom;
} else {
data.styles.maxHeight = top;
}
return data; return data;
}; };
@@ -457,6 +450,10 @@ class EnhancedSelectInput extends Component {
order: 851, order: 851,
enabled: true, enabled: true,
fn: this.onComputeMaxHeight fn: this.onComputeMaxHeight
},
preventOverflow: {
enabled: true,
boundariesElement: 'viewport'
} }
}} }}
> >
@@ -28,8 +28,7 @@ function createMapStateToProps() {
if (includeNoChange) { if (includeNoChange) {
values.unshift({ values.unshift({
key: 'noChange', key: 'noChange',
value: '', value: translate('NoChange'),
name: translate('NoChange'),
isDisabled: includeNoChangeDisabled, isDisabled: includeNoChangeDisabled,
isMissing: false isMissing: false
}); });
@@ -39,7 +38,6 @@ function createMapStateToProps() {
values.push({ values.push({
key: '', key: '',
value: '', value: '',
name: '',
isDisabled: true, isDisabled: true,
isHidden: true isHidden: true
}); });
@@ -56,8 +54,7 @@ function createMapStateToProps() {
values.push({ values.push({
key: ADD_NEW_KEY, key: ADD_NEW_KEY,
value: '', value: 'Add a new path'
name: 'Add a new path'
}); });
return { return {
@@ -105,6 +102,27 @@ class RootFolderSelectInputConnector extends Component {
} }
} }
componentDidUpdate(prevProps) {
const {
name,
value,
values,
onChange
} = this.props;
if (prevProps.values === values) {
return;
}
if (!value && values.length && values.some((v) => !!v.key && v.key !== ADD_NEW_KEY)) {
const defaultValue = values[0];
if (defaultValue.key !== ADD_NEW_KEY) {
onChange({ name, value: defaultValue.key });
}
}
}
// //
// Render // Render
@@ -13,6 +13,15 @@
} }
} }
.value {
display: flex;
}
.authorFolder {
flex: 0 0 auto;
color: var(--disabledColor);
}
.freeSpace { .freeSpace {
margin-left: 15px; margin-left: 15px;
color: var(--darkGray); color: var(--darkGray);
@@ -1,10 +1,12 @@
// This file is automatically generated. // This file is automatically generated.
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'authorFolder': string;
'freeSpace': string; 'freeSpace': string;
'isMissing': string; 'isMissing': string;
'isMobile': string; 'isMobile': string;
'optionText': string; 'optionText': string;
'value': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
export default cssExports; export default cssExports;
@@ -7,18 +7,24 @@ import styles from './RootFolderSelectInputOption.css';
function RootFolderSelectInputOption(props) { function RootFolderSelectInputOption(props) {
const { const {
id,
value, value,
name, name,
freeSpace, freeSpace,
authorFolder,
isMissing, isMissing,
isMobile, isMobile,
isWindows,
...otherProps ...otherProps
} = props; } = props;
const text = value === '' ? name : `${name} [${value}]`; const slashCharacter = isWindows ? '\\' : '/';
const text = name === '' ? value : `[${name}] ${value}`;
return ( return (
<EnhancedSelectInputOption <EnhancedSelectInputOption
id={id}
isMobile={isMobile} isMobile={isMobile}
{...otherProps} {...otherProps}
> >
@@ -27,7 +33,18 @@ function RootFolderSelectInputOption(props) {
isMobile && styles.isMobile isMobile && styles.isMobile
)} )}
> >
<div>{text}</div> <div className={styles.value}>
{text}
{
authorFolder && id !== 'addNew' ?
<div className={styles.authorFolder}>
{slashCharacter}
{authorFolder}
</div> :
null
}
</div>
{ {
freeSpace == null ? freeSpace == null ?
@@ -50,11 +67,18 @@ function RootFolderSelectInputOption(props) {
} }
RootFolderSelectInputOption.propTypes = { RootFolderSelectInputOption.propTypes = {
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
value: PropTypes.string.isRequired, value: PropTypes.string.isRequired,
freeSpace: PropTypes.number, freeSpace: PropTypes.number,
authorFolder: PropTypes.string,
isMissing: PropTypes.bool, isMissing: PropTypes.bool,
isMobile: PropTypes.bool.isRequired isMobile: PropTypes.bool.isRequired,
isWindows: PropTypes.bool
};
RootFolderSelectInputOption.defaultProps = {
name: ''
}; };
export default RootFolderSelectInputOption; export default RootFolderSelectInputOption;
@@ -7,12 +7,22 @@
overflow: hidden; overflow: hidden;
} }
.path { .pathContainer {
@add-mixin truncate; @add-mixin truncate;
display: flex;
flex: 1 0 0; flex: 1 0 0;
} }
.path {
flex: 0 1 auto;
}
.authorFolder {
@add-mixin truncate;
flex: 0 1 auto;
color: var(--disabledColor);
}
.freeSpace { .freeSpace {
flex: 0 0 auto; flex: 0 0 auto;
margin-left: 15px; margin-left: 15px;
@@ -1,8 +1,10 @@
// This file is automatically generated. // This file is automatically generated.
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'authorFolder': string;
'freeSpace': string; 'freeSpace': string;
'path': string; 'path': string;
'pathContainer': string;
'selectedValue': string; 'selectedValue': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
@@ -9,19 +9,34 @@ function RootFolderSelectInputSelectedValue(props) {
name, name,
value, value,
freeSpace, freeSpace,
authorFolder,
includeFreeSpace, includeFreeSpace,
isWindows,
...otherProps ...otherProps
} = props; } = props;
const text = value === '' ? name : `${name} [${value}]`; const slashCharacter = isWindows ? '\\' : '/';
const text = name === '' ? value : `[${name}] ${value}`;
return ( return (
<EnhancedSelectInputSelectedValue <EnhancedSelectInputSelectedValue
className={styles.selectedValue} className={styles.selectedValue}
{...otherProps} {...otherProps}
> >
<div className={styles.path}> <div className={styles.pathContainer}>
{text} <div className={styles.path}>
{text}
</div>
{
authorFolder ?
<div className={styles.authorFolder}>
{slashCharacter}
{authorFolder}
</div> :
null
}
</div> </div>
{ {
@@ -38,10 +53,13 @@ RootFolderSelectInputSelectedValue.propTypes = {
name: PropTypes.string, name: PropTypes.string,
value: PropTypes.string, value: PropTypes.string,
freeSpace: PropTypes.number, freeSpace: PropTypes.number,
authorFolder: PropTypes.string,
isWindows: PropTypes.bool,
includeFreeSpace: PropTypes.bool.isRequired includeFreeSpace: PropTypes.bool.isRequired
}; };
RootFolderSelectInputSelectedValue.defaultProps = { RootFolderSelectInputSelectedValue.defaultProps = {
name: '',
includeFreeSpace: true includeFreeSpace: true
}; };
@@ -52,6 +52,7 @@ class SelectInput extends Component {
const { const {
key, key,
value: optionValue, value: optionValue,
isDisabled: optionIsDisabled = false,
...otherOptionProps ...otherOptionProps
} = option; } = option;
@@ -59,6 +60,7 @@ class SelectInput extends Component {
<option <option
key={key} key={key}
value={key} value={key}
disabled={optionIsDisabled}
{...otherOptionProps} {...otherOptionProps}
> >
{typeof optionValue === 'function' ? optionValue() : optionValue} {typeof optionValue === 'function' ? optionValue() : optionValue}
-7
View File
@@ -83,13 +83,6 @@
} }
@media only screen and (max-width: $breakpointMedium) { @media only screen and (max-width: $breakpointMedium) {
.modal.small,
.modal.medium {
width: 90%;
}
}
@media only screen and (max-width: $breakpointSmall) {
.modalContainer { .modalContainer {
position: fixed; position: fixed;
} }
@@ -4,7 +4,7 @@
line-height: 1.52857143; line-height: 1.52857143;
} }
@media only screen and (max-width: $breakpointSmall) { @media only screen and (max-width: $breakpointMedium) {
.cell { .cell {
white-space: nowrap; white-space: nowrap;
} }
@@ -7,7 +7,7 @@
white-space: nowrap; white-space: nowrap;
} }
@media only screen and (max-width: $breakpointSmall) { @media only screen and (max-width: $breakpointMedium) {
.cell { .cell {
white-space: nowrap; white-space: nowrap;
} }
+1 -1
View File
@@ -10,7 +10,7 @@
border-collapse: collapse; border-collapse: collapse;
} }
@media only screen and (max-width: $breakpointSmall) { @media only screen and (max-width: $breakpointMedium) {
.tableContainer { .tableContainer {
min-width: 100%; min-width: 100%;
width: fit-content; width: fit-content;
@@ -9,7 +9,7 @@
margin-left: 10px; margin-left: 10px;
} }
@media only screen and (max-width: $breakpointSmall) { @media only screen and (max-width: $breakpointMedium) {
.headerCell { .headerCell {
white-space: nowrap; white-space: nowrap;
} }
+1 -1
View File
@@ -60,7 +60,7 @@
height: 25px; height: 25px;
} }
@media only screen and (max-width: $breakpointSmall) { @media only screen and (max-width: $breakpointMedium) {
.pager { .pager {
flex-wrap: wrap; flex-wrap: wrap;
} }
@@ -9,7 +9,7 @@
margin-left: 10px; margin-left: 10px;
} }
@media only screen and (max-width: $breakpointSmall) { @media only screen and (max-width: $breakpointMedium) {
.headerCell { .headerCell {
white-space: nowrap; white-space: nowrap;
} }
+2 -1
View File
@@ -35,11 +35,12 @@
.message { .message {
margin-top: 30px; margin-top: 30px;
text-align: center; text-align: center;
font-weight: 300;
font-size: $largeFontSize;
} }
.helpText { .helpText {
margin-bottom: 10px; margin-bottom: 10px;
font-weight: 300;
font-size: 24px; font-size: 24px;
} }
+23 -2
View File
@@ -82,7 +82,8 @@ class AddNewItem extends Component {
render() { render() {
const { const {
error, error,
items items,
hasExistingAuthors
} = this.props; } = this.props;
const term = this.state.term; const term = this.state.term;
@@ -186,7 +187,8 @@ class AddNewItem extends Component {
} }
{ {
!term && term ?
null :
<div className={styles.message}> <div className={styles.message}>
<div className={styles.helpText}> <div className={styles.helpText}>
{translate('ItsEasyToAddANewAuthorOrBookJustStartTypingTheNameOfTheItemYouWantToAdd')} {translate('ItsEasyToAddANewAuthorOrBookJustStartTypingTheNameOfTheItemYouWantToAdd')}
@@ -199,6 +201,24 @@ class AddNewItem extends Component {
</div> </div>
} }
{
!term && !hasExistingAuthors ?
<div className={styles.message}>
<div className={styles.noAuthorsText}>
You haven't added any authors yet, do you want to add an existing library location (Root Folder) and update?
</div>
<div>
<Button
to="/settings/mediamanagement"
kind={kinds.PRIMARY}
>
{translate('AddRootFolder')}
</Button>
</div>
</div> :
null
}
<div /> <div />
</PageContentBody> </PageContentBody>
</PageContent> </PageContent>
@@ -213,6 +233,7 @@ AddNewItem.propTypes = {
isAdding: PropTypes.bool.isRequired, isAdding: PropTypes.bool.isRequired,
addError: PropTypes.object, addError: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
hasExistingAuthors: PropTypes.bool.isRequired,
onSearchChange: PropTypes.func.isRequired, onSearchChange: PropTypes.func.isRequired,
onClearSearch: PropTypes.func.isRequired onClearSearch: PropTypes.func.isRequired
}; };
+4 -2
View File
@@ -10,13 +10,15 @@ import AddNewItem from './AddNewItem';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.search, (state) => state.search,
(state) => state.authors.items.length,
(state) => state.router.location, (state) => state.router.location,
(search, location) => { (search, existingAuthorsCount, location) => {
const { params } = parseUrl(location.search); const { params } = parseUrl(location.search);
return { return {
...search,
term: params.term, term: params.term,
...search hasExistingAuthors: existingAuthorsCount > 0
}; };
} }
); );
@@ -9,6 +9,7 @@ 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 { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import AddAuthorOptionsForm from '../Common/AddAuthorOptionsForm.js'; import AddAuthorOptionsForm from '../Common/AddAuthorOptionsForm.js';
import styles from './AddNewAuthorModalContent.css'; import styles from './AddNewAuthorModalContent.css';
@@ -54,7 +55,7 @@ class AddNewAuthorModalContent extends Component {
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
Add new Author {translate('AddNewAuthor')}
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
@@ -133,7 +134,7 @@ class AddNewAuthorModalContent extends Component {
AddNewAuthorModalContent.propTypes = { AddNewAuthorModalContent.propTypes = {
authorName: PropTypes.string.isRequired, authorName: PropTypes.string.isRequired,
disambiguation: PropTypes.string.isRequired, disambiguation: PropTypes.string,
overview: PropTypes.string, overview: PropTypes.string,
images: PropTypes.arrayOf(PropTypes.object).isRequired, images: PropTypes.arrayOf(PropTypes.object).isRequired,
isAdding: PropTypes.bool.isRequired, isAdding: PropTypes.bool.isRequired,
@@ -4,6 +4,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { addAuthor, setAuthorAddDefault } from 'Store/Actions/searchActions'; import { addAuthor, setAuthorAddDefault } from 'Store/Actions/searchActions';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
import selectSettings from 'Store/Selectors/selectSettings'; import selectSettings from 'Store/Selectors/selectSettings';
import AddNewAuthorModalContent from './AddNewAuthorModalContent'; import AddNewAuthorModalContent from './AddNewAuthorModalContent';
@@ -12,7 +13,8 @@ function createMapStateToProps() {
(state) => state.search, (state) => state.search,
(state) => state.settings.metadataProfiles, (state) => state.settings.metadataProfiles,
createDimensionsSelector(), createDimensionsSelector(),
(searchState, metadataProfiles, dimensions) => { createSystemStatusSelector(),
(searchState, metadataProfiles, dimensions, systemStatus) => {
const { const {
isAdding, isAdding,
addError, addError,
@@ -32,6 +34,7 @@ function createMapStateToProps() {
isSmallScreen: dimensions.isSmallScreen, isSmallScreen: dimensions.isSmallScreen,
validationErrors, validationErrors,
validationWarnings, validationWarnings,
isWindows: systemStatus.isWindows,
...settings ...settings
}; };
} }
@@ -78,6 +78,7 @@ class AddNewAuthorSearchResult extends Component {
status, status,
overview, overview,
ratings, ratings,
folder,
images, images,
isExistingAuthor, isExistingAuthor,
isSmallScreen isSmallScreen
@@ -205,6 +206,7 @@ class AddNewAuthorSearchResult extends Component {
disambiguation={disambiguation} disambiguation={disambiguation}
year={year} year={year}
overview={overview} overview={overview}
folder={folder}
images={images} images={images}
onModalClose={this.onAddAuthorModalClose} onModalClose={this.onAddAuthorModalClose}
/> />
@@ -222,6 +224,7 @@ AddNewAuthorSearchResult.propTypes = {
status: PropTypes.string.isRequired, status: PropTypes.string.isRequired,
overview: PropTypes.string, overview: PropTypes.string,
ratings: PropTypes.object.isRequired, ratings: PropTypes.object.isRequired,
folder: PropTypes.string.isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired, images: PropTypes.arrayOf(PropTypes.object).isRequired,
isExistingAuthor: PropTypes.bool.isRequired, isExistingAuthor: PropTypes.bool.isRequired,
isSmallScreen: PropTypes.bool.isRequired isSmallScreen: PropTypes.bool.isRequired
@@ -10,6 +10,7 @@ import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import stripHtml from 'Utilities/String/stripHtml'; import stripHtml from 'Utilities/String/stripHtml';
import translate from 'Utilities/String/translate';
import AddAuthorOptionsForm from '../Common/AddAuthorOptionsForm.js'; import AddAuthorOptionsForm from '../Common/AddAuthorOptionsForm.js';
import styles from './AddNewBookModalContent.css'; import styles from './AddNewBookModalContent.css';
@@ -58,7 +59,7 @@ class AddNewBookModalContent extends Component {
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
Add new Book {translate('AddNewBook')}
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
@@ -4,6 +4,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { addBook, setBookAddDefault } from 'Store/Actions/searchActions'; import { addBook, setBookAddDefault } from 'Store/Actions/searchActions';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
import selectSettings from 'Store/Selectors/selectSettings'; import selectSettings from 'Store/Selectors/selectSettings';
import AddNewBookModalContent from './AddNewBookModalContent'; import AddNewBookModalContent from './AddNewBookModalContent';
@@ -13,7 +14,8 @@ function createMapStateToProps() {
(state) => state.search, (state) => state.search,
(state) => state.settings.metadataProfiles, (state) => state.settings.metadataProfiles,
createDimensionsSelector(), createDimensionsSelector(),
(isExistingAuthor, searchState, metadataProfiles, dimensions) => { createSystemStatusSelector(),
(isExistingAuthor, searchState, metadataProfiles, dimensions, systemStatus) => {
const { const {
isAdding, isAdding,
addError, addError,
@@ -33,6 +35,7 @@ function createMapStateToProps() {
isSmallScreen: dimensions.isSmallScreen, isSmallScreen: dimensions.isSmallScreen,
validationErrors, validationErrors,
validationWarnings, validationWarnings,
isWindows: systemStatus.isWindows,
...settings ...settings
}; };
} }
@@ -203,6 +203,7 @@ class AddNewBookSearchResult extends Component {
disambiguation={disambiguation} disambiguation={disambiguation}
authorName={author.authorName} authorName={author.authorName}
overview={overview} overview={overview}
folder={author.folder}
images={images} images={images}
onModalClose={this.onAddBookModalClose} onModalClose={this.onAddBookModalClose}
/> />
@@ -39,7 +39,9 @@ class AddAuthorOptionsForm extends Component {
includeNoneMetadataProfile, includeNoneMetadataProfile,
includeSpecificBookMonitor, includeSpecificBookMonitor,
showMetadataProfile, showMetadataProfile,
folder,
tags, tags,
isWindows,
onInputChange, onInputChange,
...otherProps ...otherProps
} = this.props; } = this.props;
@@ -54,6 +56,15 @@ class AddAuthorOptionsForm extends Component {
<FormInputGroup <FormInputGroup
type={inputTypes.ROOT_FOLDER_SELECT} type={inputTypes.ROOT_FOLDER_SELECT}
name="rootFolderPath" name="rootFolderPath"
valueOptions={{
authorFolder: folder,
isWindows
}}
selectedValueOptions={{
authorFolder: folder,
isWindows
}}
helpText={translate('AddNewAuthorRootFolderHelpText', { folder })}
onChange={onInputChange} onChange={onInputChange}
{...rootFolderPath} {...rootFolderPath}
/> />
@@ -179,8 +190,14 @@ AddAuthorOptionsForm.propTypes = {
showMetadataProfile: PropTypes.bool.isRequired, showMetadataProfile: PropTypes.bool.isRequired,
includeNoneMetadataProfile: PropTypes.bool.isRequired, includeNoneMetadataProfile: PropTypes.bool.isRequired,
includeSpecificBookMonitor: PropTypes.bool.isRequired, includeSpecificBookMonitor: PropTypes.bool.isRequired,
folder: PropTypes.string.isRequired,
tags: PropTypes.object.isRequired, tags: PropTypes.object.isRequired,
isWindows: PropTypes.bool.isRequired,
onInputChange: PropTypes.func.isRequired onInputChange: PropTypes.func.isRequired
}; };
AddAuthorOptionsForm.defaultProps = {
includeSpecificBookMonitor: false
};
export default AddAuthorOptionsForm; export default AddAuthorOptionsForm;
@@ -10,11 +10,11 @@ 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 Column from 'Components/Table/Column';
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 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,
@@ -35,7 +35,7 @@ type OnSelectedChangeCallback = React.ComponentProps<
typeof ManageDownloadClientsModalRow typeof ManageDownloadClientsModalRow
>['onSelectedChange']; >['onSelectedChange'];
const COLUMNS = [ const COLUMNS: Column[] = [
{ {
name: 'name', name: 'name',
label: () => translate('Name'), label: () => translate('Name'),
@@ -82,8 +82,6 @@ const COLUMNS = [
interface ManageDownloadClientsModalContentProps { interface ManageDownloadClientsModalContentProps {
onModalClose(): void; onModalClose(): void;
sortKey?: string;
sortDirection?: SortDirection;
} }
function ManageDownloadClientsModalContent( function ManageDownloadClientsModalContent(
@@ -76,7 +76,7 @@ function EditImportListExclusionModalContent(props) {
<FormGroup> <FormGroup>
<FormLabel> <FormLabel>
{translate('MusicbrainzId')} {translate('ForeignId')}
</FormLabel> </FormLabel>
<FormInputGroup <FormInputGroup
@@ -10,11 +10,11 @@ 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 Column from 'Components/Table/Column';
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 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,
@@ -35,7 +35,7 @@ type OnSelectedChangeCallback = React.ComponentProps<
typeof ManageIndexersModalRow typeof ManageIndexersModalRow
>['onSelectedChange']; >['onSelectedChange'];
const COLUMNS = [ const COLUMNS: Column[] = [
{ {
name: 'name', name: 'name',
label: () => translate('Name'), label: () => translate('Name'),
@@ -82,8 +82,6 @@ const COLUMNS = [
interface ManageIndexersModalContentProps { interface ManageIndexersModalContentProps {
onModalClose(): void; onModalClose(): void;
sortKey?: string;
sortDirection?: SortDirection;
} }
function ManageIndexersModalContent(props: ManageIndexersModalContentProps) { function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
@@ -0,0 +1,18 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { createQualityProfileSelectorForHook } from 'Store/Selectors/createQualityProfileSelector';
import translate from 'Utilities/String/translate';
interface QualityProfileNameProps {
qualityProfileId: number;
}
function QualityProfileName({ qualityProfileId }: QualityProfileNameProps) {
const qualityProfile = useSelector(
createQualityProfileSelectorForHook(qualityProfileId)
);
return <span>{qualityProfile?.name ?? translate('Unknown')}</span>;
}
export default QualityProfileName;
@@ -1,31 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createQualityProfileSelector from 'Store/Selectors/createQualityProfileSelector';
function createMapStateToProps() {
return createSelector(
createQualityProfileSelector(),
(qualityProfile) => {
return {
name: qualityProfile.name
};
}
);
}
function QualityProfileNameConnector({ name, ...otherProps }) {
return (
<span>
{name}
</span>
);
}
QualityProfileNameConnector.propTypes = {
qualityProfileId: PropTypes.number.isRequired,
name: PropTypes.string.isRequired
};
export default connect(createMapStateToProps)(QualityProfileNameConnector);
@@ -45,6 +45,12 @@ export const defaultState = {
isSortable: true, isSortable: true,
isVisible: true isVisible: true
}, },
{
name: 'books.lastSearchTime',
label: 'Last Searched',
isSortable: true,
isVisible: false
},
{ {
name: 'actions', name: 'actions',
columnLabel: 'Actions', columnLabel: 'Actions',
@@ -108,6 +114,12 @@ export const defaultState = {
isSortable: true, isSortable: true,
isVisible: true isVisible: true
}, },
{
name: 'books.lastSearchTime',
label: 'Last Searched',
isSortable: true,
isVisible: false
},
{ {
name: 'actions', name: 'actions',
columnLabel: 'Actions', columnLabel: 'Actions',
@@ -27,6 +27,12 @@ export default function translate(
key: string, key: string,
tokens: Record<string, string | number | boolean> = {} tokens: Record<string, string | number | boolean> = {}
) { ) {
const { isProduction = true } = window.Readarr;
if (!isProduction && !(key in translations)) {
console.warn(`Missing translation for key: ${key}`);
}
const translation = translations[key] || key; const translation = translations[key] || key;
tokens.appName = 'Readarr'; tokens.appName = 'Readarr';
@@ -131,13 +131,15 @@ class CutoffUnmetConnector extends Component {
onSearchSelectedPress = (selected) => { onSearchSelectedPress = (selected) => {
this.props.executeCommand({ this.props.executeCommand({
name: commandNames.BOOK_SEARCH, name: commandNames.BOOK_SEARCH,
bookIds: selected bookIds: selected,
commandFinished: this.repopulate
}); });
}; };
onSearchAllCutoffUnmetPress = () => { onSearchAllCutoffUnmetPress = () => {
this.props.executeCommand({ this.props.executeCommand({
name: commandNames.CUTOFF_UNMET_BOOK_SEARCH name: commandNames.CUTOFF_UNMET_BOOK_SEARCH,
commandFinished: this.repopulate
}); });
}; };
@@ -16,6 +16,7 @@ function CutoffUnmetRow(props) {
releaseDate, releaseDate,
titleSlug, titleSlug,
title, title,
lastSearchTime,
disambiguation, disambiguation,
isSelected, isSelected,
columns, columns,
@@ -68,6 +69,15 @@ function CutoffUnmetRow(props) {
); );
} }
if (name === 'books.lastSearchTime') {
return (
<RelativeDateCellConnector
key={name}
date={lastSearchTime}
/>
);
}
if (name === 'releaseDate') { if (name === 'releaseDate') {
return ( return (
<RelativeDateCellConnector <RelativeDateCellConnector
@@ -105,6 +115,7 @@ CutoffUnmetRow.propTypes = {
releaseDate: PropTypes.string.isRequired, releaseDate: PropTypes.string.isRequired,
titleSlug: PropTypes.string.isRequired, titleSlug: PropTypes.string.isRequired,
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
lastSearchTime: PropTypes.string,
disambiguation: PropTypes.string, disambiguation: PropTypes.string,
isSelected: PropTypes.bool, isSelected: PropTypes.bool,
columns: PropTypes.arrayOf(PropTypes.object).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired,
@@ -121,13 +121,15 @@ class MissingConnector extends Component {
onSearchSelectedPress = (selected) => { onSearchSelectedPress = (selected) => {
this.props.executeCommand({ this.props.executeCommand({
name: commandNames.BOOK_SEARCH, name: commandNames.BOOK_SEARCH,
bookIds: selected bookIds: selected,
commandFinished: this.repopulate
}); });
}; };
onSearchAllMissingPress = () => { onSearchAllMissingPress = () => {
this.props.executeCommand({ this.props.executeCommand({
name: commandNames.MISSING_BOOK_SEARCH name: commandNames.MISSING_BOOK_SEARCH,
commandFinished: this.repopulate
}); });
}; };
+11
View File
@@ -16,6 +16,7 @@ function MissingRow(props) {
releaseDate, releaseDate,
titleSlug, titleSlug,
title, title,
lastSearchTime,
disambiguation, disambiguation,
isSelected, isSelected,
columns, columns,
@@ -77,6 +78,15 @@ function MissingRow(props) {
); );
} }
if (name === 'books.lastSearchTime') {
return (
<RelativeDateCellConnector
key={name}
date={lastSearchTime}
/>
);
}
if (name === 'actions') { if (name === 'actions') {
return ( return (
<BookSearchCellConnector <BookSearchCellConnector
@@ -104,6 +114,7 @@ MissingRow.propTypes = {
releaseDate: PropTypes.string.isRequired, releaseDate: PropTypes.string.isRequired,
titleSlug: PropTypes.string.isRequired, titleSlug: PropTypes.string.isRequired,
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
lastSearchTime: PropTypes.string,
disambiguation: PropTypes.string, disambiguation: PropTypes.string,
isSelected: PropTypes.bool, isSelected: PropTypes.bool,
columns: PropTypes.arrayOf(PropTypes.object).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired,
+1
View File
@@ -7,5 +7,6 @@ interface Window {
theme: string; theme: string;
urlBase: string; urlBase: string;
version: string; version: string;
isProduction: boolean;
}; };
} }
+12 -12
View File
@@ -25,10 +25,10 @@
"defaults" "defaults"
], ],
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "6.6.0", "@fortawesome/fontawesome-free": "6.7.1",
"@fortawesome/fontawesome-svg-core": "6.6.0", "@fortawesome/fontawesome-svg-core": "6.7.1",
"@fortawesome/free-regular-svg-icons": "6.6.0", "@fortawesome/free-regular-svg-icons": "6.7.1",
"@fortawesome/free-solid-svg-icons": "6.6.0", "@fortawesome/free-solid-svg-icons": "6.7.1",
"@fortawesome/react-fontawesome": "0.2.2", "@fortawesome/react-fontawesome": "0.2.2",
"@microsoft/signalr": "6.0.25", "@microsoft/signalr": "6.0.25",
"@sentry/browser": "7.119.1", "@sentry/browser": "7.119.1",
@@ -86,13 +86,13 @@
"typescript": "5.1.6" "typescript": "5.1.6"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.25.8", "@babel/core": "7.26.0",
"@babel/eslint-parser": "7.25.8", "@babel/eslint-parser": "7.25.9",
"@babel/plugin-proposal-export-default-from": "7.25.8", "@babel/plugin-proposal-export-default-from": "7.25.9",
"@babel/plugin-syntax-dynamic-import": "7.8.3", "@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/preset-env": "7.25.8", "@babel/preset-env": "7.26.0",
"@babel/preset-react": "7.25.7", "@babel/preset-react": "7.26.3",
"@babel/preset-typescript": "7.25.7", "@babel/preset-typescript": "7.26.0",
"@types/lodash": "4.14.195", "@types/lodash": "4.14.195",
"@types/react-lazyload": "3.2.3", "@types/react-lazyload": "3.2.3",
"@types/redux-actions": "2.6.5", "@types/redux-actions": "2.6.5",
@@ -102,7 +102,7 @@
"babel-loader": "9.2.1", "babel-loader": "9.2.1",
"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.38.1", "core-js": "3.39.0",
"css-loader": "6.8.1", "css-loader": "6.8.1",
"css-modules-typescript-loader": "4.0.1", "css-modules-typescript-loader": "4.0.1",
"eslint": "8.57.1", "eslint": "8.57.1",
@@ -142,7 +142,7 @@
"worker-loader": "3.0.8" "worker-loader": "3.0.8"
}, },
"volta": { "volta": {
"node": "16.17.0", "node": "20.11.1",
"yarn": "1.22.19" "yarn": "1.22.19"
} }
} }
+29
View File
@@ -99,6 +99,35 @@
<RootNamespace Condition="'$(ReadarrProject)'=='true'">$(MSBuildProjectName.Replace('Readarr','NzbDrone'))</RootNamespace> <RootNamespace Condition="'$(ReadarrProject)'=='true'">$(MSBuildProjectName.Replace('Readarr','NzbDrone'))</RootNamespace>
</PropertyGroup> </PropertyGroup>
<ItemGroup Condition="'$(TestProject)'!='true'">
<!-- Annotates .NET assemblies with repository information including SHA -->
<!-- Sentry uses this to link directly to GitHub at the exact version/file/line -->
<!-- This is built-in on .NET 8 and can be removed once the project is updated -->
<PackageReference Include="Microsoft.SourceLink.GitHub" />
</ItemGroup>
<!-- Sentry specific configuration: Only in Release mode -->
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<!-- https://docs.sentry.io/platforms/dotnet/configuration/msbuild/ -->
<!-- OrgSlug, ProjectSlug and AuthToken are required.
They can be set below, via argument to 'msbuild -p:' or environment variable -->
<SentryOrg></SentryOrg>
<SentryProject></SentryProject>
<SentryUrl></SentryUrl> <!-- If empty, assumed to be sentry.io -->
<SentryAuthToken></SentryAuthToken> <!-- Use env var instead: SENTRY_AUTH_TOKEN -->
<!-- Upload PDBs to Sentry, enabling stack traces with line numbers and file paths
without the need to deploy the application with PDBs -->
<SentryUploadSymbols>true</SentryUploadSymbols>
<!-- Source Link settings -->
<!-- https://github.com/dotnet/sourcelink/blob/main/docs/README.md#publishrepositoryurl -->
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<!-- Embeds all source code in the respective PDB. This can make it a bit bigger but since it'll be uploaded
to Sentry and not distributed to run on the server, it helps debug crashes while making releases smaller -->
<EmbedAllSources>true</EmbedAllSources>
</PropertyGroup>
<!-- Standard testing packages --> <!-- Standard testing packages -->
<ItemGroup Condition="'$(TestProject)'=='true'"> <ItemGroup Condition="'$(TestProject)'=='true'">
<PackageReference Include="Microsoft.NET.Test.Sdk" /> <PackageReference Include="Microsoft.NET.Test.Sdk" />
+14 -11
View File
@@ -9,7 +9,8 @@
<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.4.2" /> <PackageVersion Include="IPAddressRange" Version="6.1.0" />
<PackageVersion Include="Polly" Version="8.5.2" />
<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" />
@@ -17,14 +18,16 @@
<PackageVersion Include="Ical.Net" Version="4.3.1" /> <PackageVersion Include="Ical.Net" Version="4.3.1" />
<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="4.8.0" />
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.35" /> <PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.35" />
<PackageVersion Include="Microsoft.Data.SqlClient" Version="2.1.7" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="6.0.2" /> <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="6.0.2" />
<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.2" /> <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.10.0" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
<PackageVersion Include="Microsoft.Win32.Registry" Version="5.0.0" /> <PackageVersion Include="Microsoft.Win32.Registry" Version="5.0.0" />
<PackageVersion Include="Mono.Posix.NETStandard" Version="5.20.1.34-servarr22" /> <PackageVersion Include="Mono.Posix.NETStandard" Version="5.20.1.34-servarr22" />
<PackageVersion Include="Moq" Version="4.17.2" /> <PackageVersion Include="Moq" Version="4.17.2" />
@@ -34,28 +37,28 @@
<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.8" /> <PackageVersion Include="Npgsql" Version="7.0.10" />
<PackageVersion Include="NUnit3TestAdapter" Version="4.2.1" /> <PackageVersion Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageVersion Include="NUnit" Version="3.14.0" /> <PackageVersion Include="NUnit" Version="3.14.0" />
<PackageVersion Include="NunitXml.TestLogger" Version="3.0.117" /> <PackageVersion Include="NunitXml.TestLogger" Version="3.0.117" />
<PackageVersion Include="PdfSharpCore" Version="1.3.32" /> <PackageVersion Include="PdfSharpCore" Version="1.3.65" />
<PackageVersion Include="RestSharp.Serializers.SystemTextJson" Version="106.15.0" /> <PackageVersion Include="RestSharp.Serializers.SystemTextJson" Version="106.15.0" />
<PackageVersion Include="RestSharp" Version="106.15.0" /> <PackageVersion Include="RestSharp" Version="106.15.0" />
<PackageVersion Include="Selenium.Support" Version="3.141.0" /> <PackageVersion Include="Selenium.Support" Version="3.141.0" />
<PackageVersion Include="Selenium.WebDriver.ChromeDriver" Version="91.0.4472.10100" /> <PackageVersion Include="Selenium.WebDriver.ChromeDriver" Version="134.0.6998.16500" />
<PackageVersion Include="Sentry" Version="3.31.0" /> <PackageVersion Include="Sentry" Version="4.0.2" />
<PackageVersion Include="SharpZipLib" Version="1.4.2" /> <PackageVersion Include="SharpZipLib" Version="1.4.2" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.5" /> <PackageVersion Include="SixLabors.ImageSharp" Version="3.1.7" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.1.118" /> <PackageVersion Include="StyleCop.Analyzers" Version="1.1.118" />
<PackageVersion Include="Swashbuckle.AspNetCore.Annotations" Version="6.6.2" /> <PackageVersion Include="Swashbuckle.AspNetCore.Annotations" Version="6.6.2" />
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.6.2" /> <PackageVersion Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.6.2" />
<PackageVersion Include="System.Buffers" Version="4.5.1" /> <PackageVersion Include="System.Buffers" Version="4.6.0" />
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="6.0.1" /> <PackageVersion Include="System.Configuration.ConfigurationManager" Version="6.0.1" />
<PackageVersion Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" /> <PackageVersion Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="17.0.24" /> <PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="17.0.24" />
<PackageVersion Include="System.IO.Abstractions" Version="17.0.24" /> <PackageVersion Include="System.IO.Abstractions" Version="17.0.24" />
<PackageVersion Include="System.IO.FileSystem.AccessControl" Version="5.0.0" /> <PackageVersion Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
<PackageVersion Include="System.Memory" Version="4.5.5" /> <PackageVersion Include="System.Memory" Version="4.6.2" />
<PackageVersion Include="System.Reflection.TypeExtensions" Version="4.7.0" /> <PackageVersion Include="System.Reflection.TypeExtensions" Version="4.7.0" />
<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" />
@@ -63,7 +66,7 @@
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="6.0.1" /> <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.10" /> <PackageVersion Include="System.Text.Json" Version="6.0.10" />
<PackageVersion Include="System.ValueTuple" Version="4.5.0" /> <PackageVersion Include="System.ValueTuple" Version="4.6.1" />
<PackageVersion Include="TagLibSharp-Lidarr" Version="2.2.0.19" /> <PackageVersion Include="TagLibSharp-Lidarr" Version="2.2.0.19" />
</ItemGroup> </ItemGroup>
</Project> </Project>
@@ -40,15 +40,16 @@ namespace NzbDrone.Automation.Test
var service = ChromeDriverService.CreateDefaultService(); var service = ChromeDriverService.CreateDefaultService();
// Timeout as windows automation tests seem to take alot longer to get going // Timeout as windows automation tests seem to take alot longer to get going
driver = new ChromeDriver(service, options, new TimeSpan(0, 3, 0)); driver = new ChromeDriver(service, options, TimeSpan.FromMinutes(3));
driver.Manage().Window.Size = new System.Drawing.Size(1920, 1080); driver.Manage().Window.Size = new System.Drawing.Size(1920, 1080);
driver.Manage().Window.FullScreen();
_runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger(), null); _runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger(), null);
_runner.KillAll(); _runner.KillAll();
_runner.Start(true); _runner.Start(true);
driver.Url = "http://localhost:8787"; driver.Navigate().GoToUrl("http://localhost:8787");
var page = new PageBase(driver); var page = new PageBase(driver);
page.WaitForNoSpinner(); page.WaitForNoSpinner();
@@ -68,7 +69,7 @@ namespace NzbDrone.Automation.Test
{ {
try try
{ {
var image = ((ITakesScreenshot)driver).GetScreenshot(); var image = (driver as ITakesScreenshot).GetScreenshot();
image.SaveAsFile($"./{name}_test_screenshot.png", ScreenshotImageFormat.Png); image.SaveAsFile($"./{name}_test_screenshot.png", ScreenshotImageFormat.Png);
} }
catch (Exception ex) catch (Exception ex)
@@ -1,19 +1,17 @@
using System; using System;
using System.Threading; using System.Threading;
using OpenQA.Selenium; using OpenQA.Selenium;
using OpenQA.Selenium.Remote;
using OpenQA.Selenium.Support.UI; using OpenQA.Selenium.Support.UI;
namespace NzbDrone.Automation.Test.PageModel namespace NzbDrone.Automation.Test.PageModel
{ {
public class PageBase public class PageBase
{ {
private readonly RemoteWebDriver _driver; private readonly IWebDriver _driver;
public PageBase(RemoteWebDriver driver) public PageBase(IWebDriver driver)
{ {
_driver = driver; _driver = driver;
driver.Manage().Window.Maximize();
} }
public IWebElement FindByClass(string className, int timeout = 5) public IWebElement FindByClass(string className, int timeout = 5)
@@ -21,9 +21,28 @@ namespace NzbDrone.Common.Test.ExtensionTests
[TestCase("1.2.3.4")] [TestCase("1.2.3.4")]
[TestCase("172.55.0.1")] [TestCase("172.55.0.1")]
[TestCase("192.55.0.1")] [TestCase("192.55.0.1")]
[TestCase("100.64.0.1")]
[TestCase("100.127.255.254")]
public void should_return_false_for_public_ip_address(string ipAddress) public void should_return_false_for_public_ip_address(string ipAddress)
{ {
IPAddress.Parse(ipAddress).IsLocalAddress().Should().BeFalse(); IPAddress.Parse(ipAddress).IsLocalAddress().Should().BeFalse();
} }
[TestCase("100.64.0.1")]
[TestCase("100.127.255.254")]
[TestCase("100.100.100.100")]
public void should_return_true_for_cgnat_ip_address(string ipAddress)
{
IPAddress.Parse(ipAddress).IsCgnatIpAddress().Should().BeTrue();
}
[TestCase("1.2.3.4")]
[TestCase("192.168.5.1")]
[TestCase("100.63.255.255")]
[TestCase("100.128.0.0")]
public void should_return_false_for_non_cgnat_ip_address(string ipAddress)
{
IPAddress.Parse(ipAddress).IsCgnatIpAddress().Should().BeFalse();
}
} }
} }
@@ -4,6 +4,7 @@ using System.Linq;
using FluentAssertions; using FluentAssertions;
using NLog; using NLog;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Instrumentation.Sentry; using NzbDrone.Common.Instrumentation.Sentry;
using NzbDrone.Test.Common; using NzbDrone.Test.Common;
@@ -27,7 +28,7 @@ namespace NzbDrone.Common.Test.InstrumentationTests
[SetUp] [SetUp]
public void Setup() public void Setup()
{ {
_subject = new SentryTarget("https://aaaaaaaaaaaaaaaaaaaaaaaaaa@sentry.io/111111"); _subject = new SentryTarget("https://aaaaaaaaaaaaaaaaaaaaaaaaaa@sentry.io/111111", Mocker.GetMock<IAppFolderInfo>().Object);
} }
private LogEventInfo GivenLogEvent(LogLevel level, Exception ex, string message) private LogEventInfo GivenLogEvent(LogLevel level, Exception ex, string message)
+10 -9
View File
@@ -42,17 +42,18 @@ namespace NzbDrone.Common
public void CreateZip(string path, IEnumerable<string> files) public void CreateZip(string path, IEnumerable<string> files)
{ {
using (var zipFile = ZipFile.Create(path)) _logger.Debug("Creating archive {0}", path);
using var zipFile = ZipFile.Create(path);
zipFile.BeginUpdate();
foreach (var file in files)
{ {
zipFile.BeginUpdate(); zipFile.Add(file, Path.GetFileName(file));
foreach (var file in files)
{
zipFile.Add(file, Path.GetFileName(file));
}
zipFile.CommitUpdate();
} }
zipFile.CommitUpdate();
} }
private void ExtractZip(string compressedFile, string destination) private void ExtractZip(string compressedFile, string destination)
+20 -2
View File
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.IO.Abstractions; using System.IO.Abstractions;
using System.Linq; using System.Linq;
using System.Threading;
using NLog; using NLog;
using NzbDrone.Common.EnsureThat; using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
@@ -306,9 +307,26 @@ namespace NzbDrone.Common.Disk
{ {
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs); Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
var files = GetFiles(path, recursive); var files = GetFiles(path, recursive).ToList();
files.ToList().ForEach(RemoveReadOnly); files.ForEach(RemoveReadOnly);
var attempts = 0;
while (attempts < 3 && files.Any())
{
EmptyFolder(path);
if (GetFiles(path, recursive).Any())
{
// Wait for IO operations to complete after emptying the folder since they aren't always
// instantly removed and it can lead to false positives that files are still present.
Thread.Sleep(3000);
}
attempts++;
files = GetFiles(path, recursive).ToList();
}
_fileSystem.Directory.Delete(path, recursive); _fileSystem.Directory.Delete(path, recursive);
} }
@@ -342,10 +342,11 @@ namespace NzbDrone.Common.Disk
var isCifs = targetDriveFormat == "cifs"; var isCifs = targetDriveFormat == "cifs";
var isBtrfs = sourceDriveFormat == "btrfs" && targetDriveFormat == "btrfs"; var isBtrfs = sourceDriveFormat == "btrfs" && targetDriveFormat == "btrfs";
var isZfs = sourceDriveFormat == "zfs" && targetDriveFormat == "zfs";
if (mode.HasFlag(TransferMode.Copy)) if (mode.HasFlag(TransferMode.Copy))
{ {
if (isBtrfs) if (isBtrfs || isZfs)
{ {
if (_diskProvider.TryCreateRefLink(sourcePath, targetPath)) if (_diskProvider.TryCreateRefLink(sourcePath, targetPath))
{ {
@@ -359,7 +360,7 @@ namespace NzbDrone.Common.Disk
if (mode.HasFlag(TransferMode.Move)) if (mode.HasFlag(TransferMode.Move))
{ {
if (isBtrfs) if (isBtrfs || isZfs)
{ {
if (isSameMount && _diskProvider.TryRenameFile(sourcePath, targetPath)) if (isSameMount && _diskProvider.TryRenameFile(sourcePath, targetPath))
{ {
@@ -39,18 +39,24 @@ namespace NzbDrone.Common.Extensions
private static bool IsLocalIPv4(byte[] ipv4Bytes) private static bool IsLocalIPv4(byte[] ipv4Bytes)
{ {
// Link local (no IP assigned by DHCP): 169.254.0.0 to 169.254.255.255 (169.254.0.0/16) // Link local (no IP assigned by DHCP): 169.254.0.0 to 169.254.255.255 (169.254.0.0/16)
bool IsLinkLocal() => ipv4Bytes[0] == 169 && ipv4Bytes[1] == 254; var isLinkLocal = ipv4Bytes[0] == 169 && ipv4Bytes[1] == 254;
// Class A private range: 10.0.0.0 10.255.255.255 (10.0.0.0/8) // Class A private range: 10.0.0.0 10.255.255.255 (10.0.0.0/8)
bool IsClassA() => ipv4Bytes[0] == 10; var isClassA = ipv4Bytes[0] == 10;
// Class B private range: 172.16.0.0 172.31.255.255 (172.16.0.0/12) // Class B private range: 172.16.0.0 172.31.255.255 (172.16.0.0/12)
bool IsClassB() => ipv4Bytes[0] == 172 && ipv4Bytes[1] >= 16 && ipv4Bytes[1] <= 31; var isClassB = ipv4Bytes[0] == 172 && ipv4Bytes[1] >= 16 && ipv4Bytes[1] <= 31;
// Class C private range: 192.168.0.0 192.168.255.255 (192.168.0.0/16) // Class C private range: 192.168.0.0 192.168.255.255 (192.168.0.0/16)
bool IsClassC() => ipv4Bytes[0] == 192 && ipv4Bytes[1] == 168; var isClassC = ipv4Bytes[0] == 192 && ipv4Bytes[1] == 168;
return IsLinkLocal() || IsClassA() || IsClassC() || IsClassB(); return isLinkLocal || isClassA || isClassC || isClassB;
}
public static bool IsCgnatIpAddress(this IPAddress ipAddress)
{
var bytes = ipAddress.GetAddressBytes();
return bytes.Length == 4 && bytes[0] == 100 && bytes[1] >= 64 && bytes[1] <= 127;
} }
} }
} }
@@ -108,6 +108,15 @@ namespace NzbDrone.Common.Extensions
return Directory.GetParent(cleanPath)?.FullName; return Directory.GetParent(cleanPath)?.FullName;
} }
public static string GetCleanPath(this string path)
{
var cleanPath = OsInfo.IsWindows
? PARENT_PATH_END_SLASH_REGEX.Replace(path, "")
: path.TrimEnd(Path.DirectorySeparatorChar);
return cleanPath;
}
public static bool IsParentPath(this string parentPath, string childPath) public static bool IsParentPath(this string parentPath, string childPath)
{ {
if (parentPath != "/" && !parentPath.EndsWith(":\\")) if (parentPath != "/" && !parentPath.EndsWith(":\\"))
@@ -1,3 +1,4 @@
using System;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
namespace NzbDrone.Common.Http.Proxy namespace NzbDrone.Common.Http.Proxy
@@ -29,7 +30,8 @@ namespace NzbDrone.Common.Http.Proxy
{ {
if (!string.IsNullOrWhiteSpace(BypassFilter)) if (!string.IsNullOrWhiteSpace(BypassFilter))
{ {
var hostlist = BypassFilter.Split(','); var hostlist = BypassFilter.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
for (var i = 0; i < hostlist.Length; i++) for (var i = 0; i < hostlist.Length; i++)
{ {
if (hostlist[i].StartsWith("*")) if (hostlist[i].StartsWith("*"))
@@ -41,7 +43,7 @@ namespace NzbDrone.Common.Http.Proxy
return hostlist; return hostlist;
} }
return new string[] { }; return Array.Empty<string>();
} }
} }
@@ -4,27 +4,27 @@ namespace NzbDrone.Common.Instrumentation.Extensions
{ {
public static class LoggerExtensions public static class LoggerExtensions
{ {
[MessageTemplateFormatMethod("message")]
public static void ProgressInfo(this Logger logger, string message, params object[] args) public static void ProgressInfo(this Logger logger, string message, params object[] args)
{ {
var formattedMessage = string.Format(message, args); LogProgressMessage(logger, LogLevel.Info, message, args);
LogProgressMessage(logger, LogLevel.Info, formattedMessage);
} }
[MessageTemplateFormatMethod("message")]
public static void ProgressDebug(this Logger logger, string message, params object[] args) public static void ProgressDebug(this Logger logger, string message, params object[] args)
{ {
var formattedMessage = string.Format(message, args); LogProgressMessage(logger, LogLevel.Debug, message, args);
LogProgressMessage(logger, LogLevel.Debug, formattedMessage);
} }
[MessageTemplateFormatMethod("message")]
public static void ProgressTrace(this Logger logger, string message, params object[] args) public static void ProgressTrace(this Logger logger, string message, params object[] args)
{ {
var formattedMessage = string.Format(message, args); LogProgressMessage(logger, LogLevel.Trace, message, args);
LogProgressMessage(logger, LogLevel.Trace, formattedMessage);
} }
private static void LogProgressMessage(Logger logger, LogLevel level, string message) private static void LogProgressMessage(Logger logger, LogLevel level, string message, object[] parameters)
{ {
var logEvent = new LogEventInfo(level, logger.Name, message); var logEvent = new LogEventInfo(level, logger.Name, null, message, parameters);
logEvent.Properties.Add("Status", ""); logEvent.Properties.Add("Status", "");
logger.Log(logEvent); logger.Log(logEvent);
@@ -41,7 +41,7 @@ namespace NzbDrone.Common.Instrumentation
RegisterDebugger(); RegisterDebugger();
} }
RegisterSentry(updateApp); RegisterSentry(updateApp, appFolderInfo);
if (updateApp) if (updateApp)
{ {
@@ -62,7 +62,7 @@ namespace NzbDrone.Common.Instrumentation
LogManager.ReconfigExistingLoggers(); LogManager.ReconfigExistingLoggers();
} }
private static void RegisterSentry(bool updateClient) private static void RegisterSentry(bool updateClient, IAppFolderInfo appFolderInfo)
{ {
string dsn; string dsn;
@@ -77,7 +77,7 @@ namespace NzbDrone.Common.Instrumentation
: "https://31e00a6c63ea42c8b5fe70358526a30d@sentry.servarr.com/4"; : "https://31e00a6c63ea42c8b5fe70358526a30d@sentry.servarr.com/4";
} }
var target = new SentryTarget(dsn) var target = new SentryTarget(dsn, appFolderInfo)
{ {
Name = "sentryTarget", Name = "sentryTarget",
Layout = "${message}" Layout = "${message}"
@@ -9,6 +9,7 @@ using NLog;
using NLog.Common; using NLog.Common;
using NLog.Targets; using NLog.Targets;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using Sentry; using Sentry;
namespace NzbDrone.Common.Instrumentation.Sentry namespace NzbDrone.Common.Instrumentation.Sentry
@@ -99,7 +100,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
public bool FilterEvents { get; set; } public bool FilterEvents { get; set; }
public bool SentryEnabled { get; set; } public bool SentryEnabled { get; set; }
public SentryTarget(string dsn) public SentryTarget(string dsn, IAppFolderInfo appFolderInfo)
{ {
_sdk = SentrySdk.Init(o => _sdk = SentrySdk.Init(o =>
{ {
@@ -107,9 +108,33 @@ namespace NzbDrone.Common.Instrumentation.Sentry
o.AttachStacktrace = true; o.AttachStacktrace = true;
o.MaxBreadcrumbs = 200; o.MaxBreadcrumbs = 200;
o.Release = $"{BuildInfo.AppName}@{BuildInfo.Release}"; o.Release = $"{BuildInfo.AppName}@{BuildInfo.Release}";
o.BeforeSend = x => SentryCleanser.CleanseEvent(x); o.SetBeforeSend(x => SentryCleanser.CleanseEvent(x));
o.BeforeBreadcrumb = x => SentryCleanser.CleanseBreadcrumb(x); o.SetBeforeBreadcrumb(x => SentryCleanser.CleanseBreadcrumb(x));
o.Environment = BuildInfo.Branch; o.Environment = BuildInfo.Branch;
// Crash free run statistics (sends a ping for healthy and for crashes sessions)
o.AutoSessionTracking = false;
// Caches files in the event device is offline
// Sentry creates a 'sentry' sub directory, no need to concat here
o.CacheDirectoryPath = appFolderInfo.GetAppDataPath();
// default environment is production
if (!RuntimeInfo.IsProduction)
{
if (RuntimeInfo.IsDevelopment)
{
o.Environment = "development";
}
else if (RuntimeInfo.IsTesting)
{
o.Environment = "testing";
}
else
{
o.Environment = "other";
}
}
}); });
InitializeScope(); InitializeScope();
@@ -127,7 +152,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
{ {
SentrySdk.ConfigureScope(scope => SentrySdk.ConfigureScope(scope =>
{ {
scope.User = new User scope.User = new SentryUser
{ {
Id = HashUtil.AnonymousToken() Id = HashUtil.AnonymousToken()
}; };
@@ -169,9 +194,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
private void OnError(Exception ex) private void OnError(Exception ex)
{ {
var webException = ex as WebException; if (ex is WebException webException)
if (webException != null)
{ {
var response = webException.Response as HttpWebResponse; var response = webException.Response as HttpWebResponse;
var statusCode = response?.StatusCode; var statusCode = response?.StatusCode;
@@ -290,13 +313,21 @@ namespace NzbDrone.Common.Instrumentation.Sentry
} }
} }
var level = LoggingLevelMap[logEvent.Level];
var sentryEvent = new SentryEvent(logEvent.Exception) var sentryEvent = new SentryEvent(logEvent.Exception)
{ {
Level = LoggingLevelMap[logEvent.Level], Level = level,
Logger = logEvent.LoggerName, Logger = logEvent.LoggerName,
Message = logEvent.FormattedMessage Message = logEvent.FormattedMessage
}; };
if (level is SentryLevel.Fatal && logEvent.Exception is not null)
{
// Usages of 'fatal' here indicates the process will crash. In Sentry this is represented with
// the 'unhandled' exception flag
logEvent.Exception.SetSentryMechanism("Logger.Fatal", "Logger.Fatal was called", false);
}
sentryEvent.SetExtras(extras); sentryEvent.SetExtras(extras);
sentryEvent.SetFingerprint(fingerPrint); sentryEvent.SetFingerprint(fingerPrint);
@@ -6,4 +6,5 @@ public class AuthOptions
public bool? Enabled { get; set; } public bool? Enabled { get; set; }
public string Method { get; set; } public string Method { get; set; }
public string Required { get; set; } public string Required { get; set; }
public bool? TrustCgnatIpAddresses { get; set; }
} }
@@ -6,6 +6,7 @@ using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text;
using NLog; using NLog;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Model; using NzbDrone.Common.Model;
@@ -117,7 +118,9 @@ namespace NzbDrone.Common.Processes
UseShellExecute = false, UseShellExecute = false,
RedirectStandardError = true, RedirectStandardError = true,
RedirectStandardOutput = true, RedirectStandardOutput = true,
RedirectStandardInput = true RedirectStandardInput = true,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8
}; };
if (environmentVariables != null) if (environmentVariables != null)
@@ -313,7 +316,7 @@ namespace NzbDrone.Common.Processes
processInfo = new ProcessInfo(); processInfo = new ProcessInfo();
processInfo.Id = process.Id; processInfo.Id = process.Id;
processInfo.Name = process.ProcessName; processInfo.Name = process.ProcessName;
processInfo.StartPath = process.MainModule.FileName; processInfo.StartPath = process.MainModule?.FileName;
if (process.Id != GetCurrentProcessId() && process.HasExited) if (process.Id != GetCurrentProcessId() && process.HasExited)
{ {
@@ -6,6 +6,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="DryIoc.dll" /> <PackageReference Include="DryIoc.dll" />
<PackageReference Include="IPAddressRange" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" /> <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" />
<PackageReference Include="NLog.Extensions.Logging" /> <PackageReference Include="NLog.Extensions.Logging" />
@@ -17,7 +17,7 @@ namespace NzbDrone.Common.Reflection
public static List<Type> ImplementationsOf<T>(this Assembly assembly) public static List<Type> ImplementationsOf<T>(this Assembly assembly)
{ {
return assembly.GetTypes().Where(c => typeof(T).IsAssignableFrom(c)).ToList(); return assembly.GetExportedTypes().Where(c => typeof(T).IsAssignableFrom(c)).ToList();
} }
public static bool IsSimpleType(this Type type) public static bool IsSimpleType(this Type type)
@@ -68,7 +68,7 @@ namespace NzbDrone.Common.Reflection
public static Type FindTypeByName(this Assembly assembly, string name) public static Type FindTypeByName(this Assembly assembly, string name)
{ {
return assembly.GetTypes().SingleOrDefault(c => c.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)); return assembly.GetExportedTypes().SingleOrDefault(c => c.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase));
} }
public static bool HasAttribute<TAttribute>(this Type type) public static bool HasAttribute<TAttribute>(this Type type)
@@ -200,17 +200,9 @@ namespace NzbDrone.Core.Test.Download
var seriesTags = new HashSet<int> { 2 }; var seriesTags = new HashSet<int> { 2 };
var clientTags = new HashSet<int> { 1 }; var clientTags = new HashSet<int> { 1 };
WithTorrentClient(0, clientTags);
WithTorrentClient(0, clientTags);
WithTorrentClient(0, clientTags);
WithTorrentClient(0, clientTags); WithTorrentClient(0, clientTags);
var client1 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags); Assert.Throws<DownloadClientUnavailableException>(() => Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags));
var client2 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags);
var client3 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags);
var client4 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags);
Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags).Should().BeNull();
} }
[Test] [Test]
@@ -312,11 +312,12 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DelugeTests
[Test] [Test]
public void should_return_status_with_outputdirs() public void should_return_status_with_outputdirs()
{ {
var configItems = new Dictionary<string, object>(); var configItems = new Dictionary<string, object>
{
configItems.Add("download_location", @"C:\Downloads\Downloading\deluge".AsOsAgnostic()); { "download_location", @"C:\Downloads\Downloading\deluge".AsOsAgnostic() },
configItems.Add("move_completed_path", @"C:\Downloads\Finished\deluge".AsOsAgnostic()); { "move_completed_path", @"C:\Downloads\Finished\deluge".AsOsAgnostic() },
configItems.Add("move_completed", true); { "move_completed", true }
};
Mocker.GetMock<IDelugeProxy>() Mocker.GetMock<IDelugeProxy>()
.Setup(v => v.GetConfig(It.IsAny<DelugeSettings>())) .Setup(v => v.GetConfig(It.IsAny<DelugeSettings>()))
@@ -328,5 +329,18 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DelugeTests
result.OutputRootFolders.Should().NotBeNull(); result.OutputRootFolders.Should().NotBeNull();
result.OutputRootFolders.First().Should().Be(@"C:\Downloads\Finished\deluge".AsOsAgnostic()); result.OutputRootFolders.First().Should().Be(@"C:\Downloads\Finished\deluge".AsOsAgnostic());
} }
[Test]
public void should_return_status_with_outputdirs_for_directories_in_settings()
{
Subject.Definition.Settings.As<DelugeSettings>().DownloadDirectory = @"D:\Downloads\Downloading\deluge".AsOsAgnostic();
Subject.Definition.Settings.As<DelugeSettings>().CompletedDirectory = @"D:\Downloads\Finished\deluge".AsOsAgnostic();
var result = Subject.GetStatus();
result.IsLocalhost.Should().BeTrue();
result.OutputRootFolders.Should().NotBeNull();
result.OutputRootFolders.First().Should().Be(@"D:\Downloads\Finished\deluge".AsOsAgnostic());
}
} }
} }
@@ -178,8 +178,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
VerifyWarning(item); VerifyWarning(item);
} }
[Test] [TestCase("pausedDL")]
public void paused_item_should_have_required_properties() [TestCase("stoppedDL")]
public void paused_item_should_have_required_properties(string state)
{ {
var torrent = new QBittorrentTorrent var torrent = new QBittorrentTorrent
{ {
@@ -188,7 +189,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
Size = 1000, Size = 1000,
Progress = 0.7, Progress = 0.7,
Eta = 8640000, Eta = 8640000,
State = "pausedDL", State = state,
Label = "", Label = "",
SavePath = "" SavePath = ""
}; };
@@ -200,6 +201,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
} }
[TestCase("pausedUP")] [TestCase("pausedUP")]
[TestCase("stoppedUP")]
[TestCase("queuedUP")] [TestCase("queuedUP")]
[TestCase("uploading")] [TestCase("uploading")]
[TestCase("stalledUP")] [TestCase("stalledUP")]
@@ -397,8 +399,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
result.OutputPath.FullPath.Should().Be(Path.Combine(torrent.SavePath, "Droned.S01.12")); result.OutputPath.FullPath.Should().Be(Path.Combine(torrent.SavePath, "Droned.S01.12"));
} }
[Test] [TestCase("pausedUP")]
public void api_261_should_use_content_path() [TestCase("stoppedUP")]
public void api_261_should_use_content_path(string state)
{ {
var torrent = new QBittorrentTorrent var torrent = new QBittorrentTorrent
{ {
@@ -407,7 +410,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
Size = 1000, Size = 1000,
Progress = 0.7, Progress = 0.7,
Eta = 8640000, Eta = 8640000,
State = "pausedUP", State = state,
Label = "", Label = "",
SavePath = @"C:\Torrents".AsOsAgnostic(), SavePath = @"C:\Torrents".AsOsAgnostic(),
ContentPath = @"C:\Torrents\Droned.S01.12".AsOsAgnostic() ContentPath = @"C:\Torrents\Droned.S01.12".AsOsAgnostic()
@@ -684,44 +687,96 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
item.CanMoveFiles.Should().BeFalse(); item.CanMoveFiles.Should().BeFalse();
} }
[Test] [TestCase("pausedUP")]
public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_is_not_set() [TestCase("stoppedUP")]
public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_is_not_set(string state)
{ {
GivenGlobalSeedLimits(-1); GivenGlobalSeedLimits(-1);
GivenCompletedTorrent("pausedUP", ratio: 1.0f); GivenCompletedTorrent(state, ratio: 1.0f);
var item = Subject.GetItems().Single(); var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeFalse(); item.CanBeRemoved.Should().BeFalse();
item.CanMoveFiles.Should().BeFalse(); item.CanMoveFiles.Should().BeFalse();
} }
[Test] [TestCase("pausedUP")]
public void should_be_removable_and_should_allow_move_files_if_max_ratio_reached_and_paused() [TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_max_ratio_reached_and_paused(string state)
{ {
GivenGlobalSeedLimits(1.0f); GivenGlobalSeedLimits(1.0f);
GivenCompletedTorrent("pausedUP", ratio: 1.0f); GivenCompletedTorrent(state, ratio: 1.0f);
var item = Subject.GetItems().Single(); var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue(); item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue(); item.CanMoveFiles.Should().BeTrue();
} }
[Test] [TestCase("pausedUP")]
public void should_be_removable_and_should_allow_move_files_if_overridden_max_ratio_reached_and_paused() [TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_max_ratio_reached_after_rounding_and_paused(string state)
{
GivenGlobalSeedLimits(1.0f);
GivenCompletedTorrent(state, ratio: 1.1006066990976857f);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_just_under_max_ratio_reached_after_rounding_and_paused(string state)
{
GivenGlobalSeedLimits(1.0f);
GivenCompletedTorrent(state, ratio: 0.9999f);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_overridden_max_ratio_reached_and_paused(string state)
{ {
GivenGlobalSeedLimits(2.0f); GivenGlobalSeedLimits(2.0f);
GivenCompletedTorrent("pausedUP", ratio: 1.0f, ratioLimit: 0.8f); GivenCompletedTorrent(state, ratio: 1.0f, ratioLimit: 0.8f);
var item = Subject.GetItems().Single(); var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue(); item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue(); item.CanMoveFiles.Should().BeTrue();
} }
[Test] [TestCase("pausedUP")]
public void should_not_be_removable_if_overridden_max_ratio_not_reached_and_paused() [TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_overridden_max_ratio_reached_after_rounding_and_paused(string state)
{
GivenGlobalSeedLimits(2.0f);
GivenCompletedTorrent(state, ratio: 1.1006066990976857f, ratioLimit: 1.1f);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_just_under_overridden_max_ratio_reached_after_rounding_and_paused(string state)
{
GivenGlobalSeedLimits(2.0f);
GivenCompletedTorrent(state, ratio: 0.9999f, ratioLimit: 1.0f);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_not_be_removable_if_overridden_max_ratio_not_reached_and_paused(string state)
{ {
GivenGlobalSeedLimits(0.2f); GivenGlobalSeedLimits(0.2f);
GivenCompletedTorrent("pausedUP", ratio: 0.5f, ratioLimit: 0.8f); GivenCompletedTorrent(state, ratio: 0.5f, ratioLimit: 0.8f);
var item = Subject.GetItems().Single(); var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeFalse(); item.CanBeRemoved.Should().BeFalse();
@@ -739,33 +794,36 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
item.CanMoveFiles.Should().BeFalse(); item.CanMoveFiles.Should().BeFalse();
} }
[Test] [TestCase("pausedUP")]
public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_and_paused() [TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_and_paused(string state)
{ {
GivenGlobalSeedLimits(-1, 20); GivenGlobalSeedLimits(-1, 20);
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 20); GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 20);
var item = Subject.GetItems().Single(); var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue(); item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue(); item.CanMoveFiles.Should().BeTrue();
} }
[Test] [TestCase("pausedUP")]
public void should_be_removable_and_should_allow_move_files_if_overridden_max_seedingtime_reached_and_paused() [TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_overridden_max_seedingtime_reached_and_paused(string state)
{ {
GivenGlobalSeedLimits(-1, 40); GivenGlobalSeedLimits(-1, 40);
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 20, seedingTimeLimit: 10); GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 20, seedingTimeLimit: 10);
var item = Subject.GetItems().Single(); var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue(); item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue(); item.CanMoveFiles.Should().BeTrue();
} }
[Test] [TestCase("pausedUP")]
public void should_not_be_removable_if_overridden_max_seedingtime_not_reached_and_paused() [TestCase("stoppedUP")]
public void should_not_be_removable_if_overridden_max_seedingtime_not_reached_and_paused(string state)
{ {
GivenGlobalSeedLimits(-1, 20); GivenGlobalSeedLimits(-1, 20);
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 30, seedingTimeLimit: 40); GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 30, seedingTimeLimit: 40);
var item = Subject.GetItems().Single(); var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeFalse(); item.CanBeRemoved.Should().BeFalse();
@@ -783,66 +841,72 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
item.CanMoveFiles.Should().BeFalse(); item.CanMoveFiles.Should().BeFalse();
} }
[Test] [TestCase("pausedUP")]
public void should_be_removable_and_should_allow_move_files_if_max_inactive_seedingtime_reached_and_paused() [TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_max_inactive_seedingtime_reached_and_paused(string state)
{ {
GivenGlobalSeedLimits(-1, maxInactiveSeedingTime: 20); GivenGlobalSeedLimits(-1, maxInactiveSeedingTime: 20);
GivenCompletedTorrent("pausedUP", ratio: 2.0f, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(25)).ToUnixTimeSeconds()); GivenCompletedTorrent(state, ratio: 2.0f, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(25)).ToUnixTimeSeconds());
var item = Subject.GetItems().Single(); var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue(); item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue(); item.CanMoveFiles.Should().BeTrue();
} }
[Test] [TestCase("pausedUP")]
public void should_be_removable_and_should_allow_move_files_if_overridden_max_inactive_seedingtime_reached_and_paused() [TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_overridden_max_inactive_seedingtime_reached_and_paused(string state)
{ {
GivenGlobalSeedLimits(-1, maxInactiveSeedingTime: 40); GivenGlobalSeedLimits(-1, maxInactiveSeedingTime: 40);
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 20, inactiveSeedingTimeLimit: 10, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(15)).ToUnixTimeSeconds()); GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 20, inactiveSeedingTimeLimit: 10, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(15)).ToUnixTimeSeconds());
var item = Subject.GetItems().Single(); var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue(); item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue(); item.CanMoveFiles.Should().BeTrue();
} }
[Test] [TestCase("pausedUP")]
public void should_not_be_removable_if_overridden_max_inactive_seedingtime_not_reached_and_paused() [TestCase("stoppedUP")]
public void should_not_be_removable_if_overridden_max_inactive_seedingtime_not_reached_and_paused(string state)
{ {
GivenGlobalSeedLimits(-1, maxInactiveSeedingTime: 20); GivenGlobalSeedLimits(-1, maxInactiveSeedingTime: 20);
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 30, inactiveSeedingTimeLimit: 40, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(30)).ToUnixTimeSeconds()); GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 30, inactiveSeedingTimeLimit: 40, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(30)).ToUnixTimeSeconds());
var item = Subject.GetItems().Single(); var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeFalse(); item.CanBeRemoved.Should().BeFalse();
item.CanMoveFiles.Should().BeFalse(); item.CanMoveFiles.Should().BeFalse();
} }
[Test] [TestCase("pausedUP")]
public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_but_ratio_not_and_paused() [TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_but_ratio_not_and_paused(string state)
{ {
GivenGlobalSeedLimits(2.0f, 20); GivenGlobalSeedLimits(2.0f, 20);
GivenCompletedTorrent("pausedUP", ratio: 1.0f, seedingTime: 30); GivenCompletedTorrent(state, ratio: 1.0f, seedingTime: 30);
var item = Subject.GetItems().Single(); var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue(); item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue(); item.CanMoveFiles.Should().BeTrue();
} }
[Test] [TestCase("pausedUP")]
public void should_be_removable_and_should_allow_move_files_if_max_inactive_seedingtime_reached_but_ratio_not_and_paused() [TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_max_inactive_seedingtime_reached_but_ratio_not_and_paused(string state)
{ {
GivenGlobalSeedLimits(2.0f, maxInactiveSeedingTime: 20); GivenGlobalSeedLimits(2.0f, maxInactiveSeedingTime: 20);
GivenCompletedTorrent("pausedUP", ratio: 1.0f, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(25)).ToUnixTimeSeconds()); GivenCompletedTorrent(state, ratio: 1.0f, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(25)).ToUnixTimeSeconds());
var item = Subject.GetItems().Single(); var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue(); item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue(); item.CanMoveFiles.Should().BeTrue();
} }
[Test] [TestCase("pausedUP")]
public void should_not_fetch_details_twice() [TestCase("stoppedUP")]
public void should_not_fetch_details_twice(string state)
{ {
GivenGlobalSeedLimits(-1, 30); GivenGlobalSeedLimits(-1, 30);
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 20); GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 20);
var item = Subject.GetItems().Single(); var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeFalse(); item.CanBeRemoved.Should().BeFalse();
@@ -854,8 +918,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
.Verify(p => p.GetTorrentProperties(It.IsAny<string>(), It.IsAny<QBittorrentSettings>()), Times.Once()); .Verify(p => p.GetTorrentProperties(It.IsAny<string>(), It.IsAny<QBittorrentSettings>()), Times.Once());
} }
[Test] [TestCase("pausedUP")]
public void should_get_category_from_the_category_if_set() [TestCase("stoppedUP")]
public void should_get_category_from_the_category_if_set(string state)
{ {
const string category = "music-readarr"; const string category = "music-readarr";
GivenGlobalSeedLimits(1.0f); GivenGlobalSeedLimits(1.0f);
@@ -867,7 +932,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
Size = 1000, Size = 1000,
Progress = 1.0, Progress = 1.0,
Eta = 8640000, Eta = 8640000,
State = "pausedUP", State = state,
Category = category, Category = category,
SavePath = "", SavePath = "",
Ratio = 1.0f Ratio = 1.0f
@@ -879,8 +944,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
item.Category.Should().Be(category); item.Category.Should().Be(category);
} }
[Test] [TestCase("pausedUP")]
public void should_get_category_from_the_label_if_the_category_is_not_available() [TestCase("stoppedUP")]
public void should_get_category_from_the_label_if_the_category_is_not_available(string state)
{ {
const string category = "music-readarr"; const string category = "music-readarr";
GivenGlobalSeedLimits(1.0f); GivenGlobalSeedLimits(1.0f);
@@ -892,7 +958,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
Size = 1000, Size = 1000,
Progress = 1.0, Progress = 1.0,
Eta = 8640000, Eta = 8640000,
State = "pausedUP", State = state,
Label = category, Label = category,
SavePath = "", SavePath = "",
Ratio = 1.0f Ratio = 1.0f
@@ -478,6 +478,37 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests
downloadClientInfo.RemovesCompletedDownloads.Should().BeTrue(); downloadClientInfo.RemovesCompletedDownloads.Should().BeTrue();
} }
[TestCase("all", 0)]
[TestCase("days-archive", 15)]
[TestCase("days-delete", 15)]
public void should_set_history_removes_completed_downloads_false_for_separate_properties(string option, int number)
{
_config.Misc.history_retention_option = option;
_config.Misc.history_retention_number = number;
var downloadClientInfo = Subject.GetStatus();
downloadClientInfo.RemovesCompletedDownloads.Should().BeFalse();
}
[TestCase("number-archive", 10)]
[TestCase("number-delete", 10)]
[TestCase("number-archive", 0)]
[TestCase("number-delete", 0)]
[TestCase("days-archive", 3)]
[TestCase("days-delete", 3)]
[TestCase("all-archive", 0)]
[TestCase("all-delete", 0)]
public void should_set_history_removes_completed_downloads_true_for_separate_properties(string option, int number)
{
_config.Misc.history_retention_option = option;
_config.Misc.history_retention_number = number;
var downloadClientInfo = Subject.GetStatus();
downloadClientInfo.RemovesCompletedDownloads.Should().BeTrue();
}
[TestCase(@"Y:\nzbget\root", @"completed\downloads", @"vv", @"Y:\nzbget\root\completed\downloads", @"Y:\nzbget\root\completed\downloads\vv")] [TestCase(@"Y:\nzbget\root", @"completed\downloads", @"vv", @"Y:\nzbget\root\completed\downloads", @"Y:\nzbget\root\completed\downloads\vv")]
[TestCase(@"Y:\nzbget\root", @"completed", @"vv", @"Y:\nzbget\root\completed", @"Y:\nzbget\root\completed\vv")] [TestCase(@"Y:\nzbget\root", @"completed", @"vv", @"Y:\nzbget\root\completed", @"Y:\nzbget\root\completed\vv")]
[TestCase(@"/nzbget/root", @"completed/downloads", @"vv", @"/nzbget/root/completed/downloads", @"/nzbget/root/completed/downloads/vv")] [TestCase(@"/nzbget/root", @"completed/downloads", @"vv", @"/nzbget/root/completed/downloads", @"/nzbget/root/completed/downloads/vv")]
@@ -12,7 +12,7 @@ namespace NzbDrone.Core.Test.Http
{ {
private HttpProxySettings GetProxySettings() private HttpProxySettings GetProxySettings()
{ {
return new HttpProxySettings(ProxyType.Socks5, "localhost", 8080, "*.httpbin.org,google.com", true, null, null); return new HttpProxySettings(ProxyType.Socks5, "localhost", 8080, "*.httpbin.org,google.com,172.16.0.0/12", true, null, null);
} }
[Test] [Test]
@@ -23,6 +23,7 @@ namespace NzbDrone.Core.Test.Http
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://eu.httpbin.org/get")).Should().BeTrue(); Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://eu.httpbin.org/get")).Should().BeTrue();
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://google.com/get")).Should().BeTrue(); Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://google.com/get")).Should().BeTrue();
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://localhost:8654/get")).Should().BeTrue(); Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://localhost:8654/get")).Should().BeTrue();
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://172.21.0.1:8989/api/v3/indexer/schema")).Should().BeTrue();
} }
[Test] [Test]
@@ -31,6 +32,7 @@ namespace NzbDrone.Core.Test.Http
var settings = GetProxySettings(); var settings = GetProxySettings();
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://bing.com/get")).Should().BeFalse(); Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://bing.com/get")).Should().BeFalse();
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://172.3.0.1:8989/api/v3/indexer/schema")).Should().BeFalse();
} }
} }
} }
@@ -13,7 +13,7 @@ using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.MetadataSource.Goodreads namespace NzbDrone.Core.Test.MetadataSource.Goodreads
{ {
[TestFixture] [TestFixture]
[Ignore("Waiting for metadata to be back again", Until = "2024-12-15 00:00:00Z")] [Ignore("Waiting for metadata to be back again", Until = "2026-01-15 00:00:00Z")]
public class BookInfoProxyFixture : CoreTest<BookInfoProxy> public class BookInfoProxyFixture : CoreTest<BookInfoProxy>
{ {
private MetadataProfile _metadataProfile; private MetadataProfile _metadataProfile;
@@ -15,7 +15,7 @@ using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.MetadataSource.Goodreads namespace NzbDrone.Core.Test.MetadataSource.Goodreads
{ {
[TestFixture] [TestFixture]
[Ignore("Waiting for metadata to be back again", Until = "2024-12-15 00:00:00Z")] [Ignore("Waiting for metadata to be back again", Until = "2026-01-15 00:00:00Z")]
public class BookInfoProxySearchFixture : CoreTest<BookInfoProxy> public class BookInfoProxySearchFixture : CoreTest<BookInfoProxy>
{ {
[SetUp] [SetUp]
@@ -40,7 +40,7 @@ namespace NzbDrone.Core.Test.MetadataSource.Goodreads
[TestCase("Harry Potter and the sorcerer's stone a detailed summary", 72245296)] [TestCase("Harry Potter and the sorcerer's stone a detailed summary", 72245296)]
[TestCase("B0192CTMYG", 61209488)] [TestCase("B0192CTMYG", 61209488)]
[TestCase("9780439554930", 48517161)] [TestCase("9780439554930", 3)]
public void successful_book_search(string title, int expected) public void successful_book_search(string title, int expected)
{ {
var result = Subject.Search(title); var result = Subject.Search(title);
@@ -21,14 +21,14 @@ namespace NzbDrone.Core.Test.UpdateTests
public void no_update_when_version_higher() public void no_update_when_version_higher()
{ {
UseRealHttp(); UseRealHttp();
Subject.GetLatestUpdate("nightly", new Version(10, 0)).Should().BeNull(); Subject.GetLatestUpdate("develop", new Version(10, 0)).Should().BeNull();
} }
[Test] [Test]
public void finds_update_when_version_lower() public void finds_update_when_version_lower()
{ {
UseRealHttp(); UseRealHttp();
Subject.GetLatestUpdate("nightly", new Version(0, 1)).Should().NotBeNull(); Subject.GetLatestUpdate("develop", new Version(0, 1)).Should().NotBeNull();
} }
[Test] [Test]
@@ -42,10 +42,9 @@ namespace NzbDrone.Core.Test.UpdateTests
[Test] [Test]
public void should_get_recent_updates() public void should_get_recent_updates()
{ {
const string branch = "nightly"; const string branch = "develop";
UseRealHttp(); UseRealHttp();
var recent = Subject.GetRecentUpdates(branch, new Version(0, 1), null); var recent = Subject.GetRecentUpdates(branch, new Version(0, 1), null);
var recentWithChanges = recent.Where(c => c.Changes != null);
recent.Should().NotBeEmpty(); recent.Should().NotBeEmpty();
recent.Should().OnlyContain(c => c.Hash.IsNotNullOrWhiteSpace()); recent.Should().OnlyContain(c => c.Hash.IsNotNullOrWhiteSpace());
+9 -2
View File
@@ -66,12 +66,19 @@ namespace NzbDrone.Core.Backup
{ {
_logger.ProgressInfo("Starting Backup"); _logger.ProgressInfo("Starting Backup");
var backupFolder = GetBackupFolder(backupType);
_diskProvider.EnsureFolder(_backupTempFolder); _diskProvider.EnsureFolder(_backupTempFolder);
_diskProvider.EnsureFolder(GetBackupFolder(backupType)); _diskProvider.EnsureFolder(backupFolder);
if (!_diskProvider.FolderWritable(backupFolder))
{
throw new UnauthorizedAccessException($"Backup folder {backupFolder} is not writable");
}
var dateNow = DateTime.Now; var dateNow = DateTime.Now;
var backupFilename = $"readarr_backup_v{BuildInfo.Version}_{dateNow:yyyy.MM.dd_HH.mm.ss}.zip"; var backupFilename = $"readarr_backup_v{BuildInfo.Version}_{dateNow:yyyy.MM.dd_HH.mm.ss}.zip";
var backupPath = Path.Combine(GetBackupFolder(backupType), backupFilename); var backupPath = Path.Combine(backupFolder, backupFilename);
Cleanup(); Cleanup();
@@ -102,9 +102,9 @@ namespace NzbDrone.Core.Books
_logger.Error("ReadarrId {0} was not found, it may have been removed from Goodreads.", newAuthor.Metadata.Value.ForeignAuthorId); _logger.Error("ReadarrId {0} was not found, it may have been removed from Goodreads.", newAuthor.Metadata.Value.ForeignAuthorId);
throw new ValidationException(new List<ValidationFailure> throw new ValidationException(new List<ValidationFailure>
{ {
new ValidationFailure("MusicbrainzId", "An author with this ID was not found", newAuthor.Metadata.Value.ForeignAuthorId) new ("ForeignAuthorId", "An author with this ID was not found", newAuthor.Metadata.Value.ForeignAuthorId)
}); });
} }
author.ApplyChanges(newAuthor); author.ApplyChanges(newAuthor);
@@ -53,6 +53,7 @@ namespace NzbDrone.Core.Configuration
string SyslogServer { get; } string SyslogServer { get; }
int SyslogPort { get; } int SyslogPort { get; }
string SyslogLevel { get; } string SyslogLevel { get; }
string Theme { get; }
string PostgresHost { get; } string PostgresHost { get; }
int PostgresPort { get; } int PostgresPort { get; }
string PostgresUser { get; } string PostgresUser { get; }
@@ -60,7 +61,7 @@ namespace NzbDrone.Core.Configuration
string PostgresMainDb { get; } string PostgresMainDb { get; }
string PostgresLogDb { get; } string PostgresLogDb { get; }
string PostgresCacheDb { get; } string PostgresCacheDb { get; }
string Theme { get; } bool TrustCgnatIpAddresses { get; }
} }
public class ConfigFileProvider : IConfigFileProvider public class ConfigFileProvider : IConfigFileProvider
@@ -253,7 +254,21 @@ namespace NzbDrone.Core.Configuration
} }
public string UiFolder => BuildInfo.IsDebug ? Path.Combine("..", "UI") : "UI"; public string UiFolder => BuildInfo.IsDebug ? Path.Combine("..", "UI") : "UI";
public string InstanceName => _appOptions.InstanceName ?? GetValue("InstanceName", BuildInfo.AppName);
public string InstanceName
{
get
{
var instanceName = _appOptions.InstanceName ?? GetValue("InstanceName", BuildInfo.AppName);
if (instanceName.Contains(BuildInfo.AppName, StringComparison.OrdinalIgnoreCase))
{
return instanceName;
}
return BuildInfo.AppName;
}
}
public bool UpdateAutomatically => _updateOptions.Automatically ?? GetValueBoolean("UpdateAutomatically", OsInfo.IsWindows, false); public bool UpdateAutomatically => _updateOptions.Automatically ?? GetValueBoolean("UpdateAutomatically", OsInfo.IsWindows, false);
@@ -462,5 +477,7 @@ namespace NzbDrone.Core.Configuration
{ {
SetValue("ApiKey", GenerateApiKey()); SetValue("ApiKey", GenerateApiKey());
} }
public bool TrustCgnatIpAddresses => _authOptions.TrustCgnatIpAddresses ?? GetValueBoolean("TrustCgnatIpAddresses", false, persist: false);
} }
} }
@@ -404,6 +404,12 @@ namespace NzbDrone.Core.Configuration
public string ApplicationUrl => GetValue("ApplicationUrl", string.Empty); public string ApplicationUrl => GetValue("ApplicationUrl", string.Empty);
public bool TrustCgnatIpAddresses
{
get { return GetValueBoolean("TrustCgnatIpAddresses", false); }
set { SetValue("TrustCgnatIpAddresses", value); }
}
private string GetValue(string key) private string GetValue(string key)
{ {
return GetValue(key, string.Empty); return GetValue(key, string.Empty);
@@ -264,7 +264,7 @@ namespace NzbDrone.Core.Datastore
protected void Delete(SqlBuilder builder) protected void Delete(SqlBuilder builder)
{ {
var sql = builder.AddDeleteTemplate(typeof(TModel)).LogQuery(); var sql = builder.AddDeleteTemplate(typeof(TModel));
using (var conn = _database.OpenConnection()) using (var conn = _database.OpenConnection())
{ {
+1
View File
@@ -40,6 +40,7 @@ namespace NzbDrone.Core.Datastore
Environment.SetEnvironmentVariable("No_Expand", "true"); Environment.SetEnvironmentVariable("No_Expand", "true");
Environment.SetEnvironmentVariable("No_SQLiteXmlConfigFile", "true"); Environment.SetEnvironmentVariable("No_SQLiteXmlConfigFile", "true");
Environment.SetEnvironmentVariable("No_PreLoadSQLite", "true"); Environment.SetEnvironmentVariable("No_PreLoadSQLite", "true");
Environment.SetEnvironmentVariable("No_SQLiteFunctions", "true");
} }
public DbFactory(IMigrationController migrationController, public DbFactory(IMigrationController migrationController,
@@ -213,9 +213,18 @@ namespace NzbDrone.Core.Download.Clients.Deluge
{ {
var config = _proxy.GetConfig(Settings); var config = _proxy.GetConfig(Settings);
var label = _proxy.GetLabelOptions(Settings); var label = _proxy.GetLabelOptions(Settings);
OsPath destDir; OsPath destDir;
if (label != null && label.ApplyMoveCompleted && label.MoveCompleted) if (Settings.CompletedDirectory.IsNotNullOrWhiteSpace())
{
destDir = new OsPath(Settings.CompletedDirectory);
}
else if (Settings.DownloadDirectory.IsNotNullOrWhiteSpace())
{
destDir = new OsPath(Settings.DownloadDirectory);
}
else if (label is { ApplyMoveCompleted: true, MoveCompleted: true })
{ {
// if label exists and a label completed path exists and is enabled use it instead of global // if label exists and a label completed path exists and is enabled use it instead of global
destDir = new OsPath(label.MoveCompletedPath); destDir = new OsPath(label.MoveCompletedPath);
@@ -231,7 +240,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
var status = new DownloadClientInfo var status = new DownloadClientInfo
{ {
IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost" IsLocalhost = Settings.Host is "127.0.0.1" or "localhost"
}; };
if (!destDir.IsEmpty) if (!destDir.IsEmpty)
@@ -239,7 +239,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
// Avoid removing torrents that haven't reached the global max ratio. // Avoid removing torrents that haven't reached the global max ratio.
// Removal also requires the torrent to be paused, in case a higher max ratio was set on the torrent itself (which is not exposed by the api). // Removal also requires the torrent to be paused, in case a higher max ratio was set on the torrent itself (which is not exposed by the api).
item.CanMoveFiles = item.CanBeRemoved = torrent.State == "pausedUP" && HasReachedSeedLimit(torrent, config); item.CanMoveFiles = item.CanBeRemoved = torrent.State is "pausedUP" or "stoppedUP" && HasReachedSeedLimit(torrent, config);
switch (torrent.State) switch (torrent.State)
{ {
@@ -248,7 +248,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
item.Message = "qBittorrent is reporting an error"; item.Message = "qBittorrent is reporting an error";
break; break;
case "pausedDL": // torrent is paused and has NOT finished downloading case "stoppedDL": // torrent is stopped and has NOT finished downloading
case "pausedDL": // torrent is paused and has NOT finished downloading (qBittorrent < 5)
item.Status = DownloadItemStatus.Paused; item.Status = DownloadItemStatus.Paused;
break; break;
@@ -259,7 +260,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
item.Status = DownloadItemStatus.Queued; item.Status = DownloadItemStatus.Queued;
break; break;
case "pausedUP": // torrent is paused and has finished downloading case "pausedUP": // torrent is paused and has finished downloading (qBittorent < 5)
case "stoppedUP": // torrent is stopped and has finished downloading
case "uploading": // torrent is being seeded and data is being transferred case "uploading": // torrent is being seeded and data is being transferred
case "stalledUP": // torrent is being seeded, but no connection were made case "stalledUP": // torrent is being seeded, but no connection were made
case "queuedUP": // queuing is enabled and torrent is queued for upload case "queuedUP": // queuing is enabled and torrent is queued for upload
@@ -618,14 +620,14 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
{ {
if (torrent.RatioLimit >= 0) if (torrent.RatioLimit >= 0)
{ {
if (torrent.Ratio >= torrent.RatioLimit) if (torrent.RatioLimit - torrent.Ratio <= 0.001f)
{ {
return true; return true;
} }
} }
else if (torrent.RatioLimit == -2 && config.MaxRatioEnabled) else if (torrent.RatioLimit == -2 && config.MaxRatioEnabled)
{ {
if (Math.Round(torrent.Ratio, 2) >= config.MaxRatio) if (config.MaxRatio - torrent.Ratio <= 0.001f)
{ {
return true; return true;
} }
@@ -26,8 +26,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
Dictionary<string, QBittorrentLabel> GetLabels(QBittorrentSettings settings); Dictionary<string, QBittorrentLabel> GetLabels(QBittorrentSettings settings);
void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings); void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings);
void MoveTorrentToTopInQueue(string hash, QBittorrentSettings settings); void MoveTorrentToTopInQueue(string hash, QBittorrentSettings settings);
void PauseTorrent(string hash, QBittorrentSettings settings);
void ResumeTorrent(string hash, QBittorrentSettings settings);
void SetForceStart(string hash, bool enabled, QBittorrentSettings settings); void SetForceStart(string hash, bool enabled, QBittorrentSettings settings);
} }
@@ -148,7 +148,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
{ {
request.AddFormParameter("paused", false); request.AddFormParameter("paused", false);
} }
else if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause) else if ((QBittorrentState)settings.InitialState == QBittorrentState.Stop)
{ {
request.AddFormParameter("paused", true); request.AddFormParameter("paused", true);
} }
@@ -178,7 +178,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
{ {
request.AddFormParameter("paused", false); request.AddFormParameter("paused", false);
} }
else if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause) else if ((QBittorrentState)settings.InitialState == QBittorrentState.Stop)
{ {
request.AddFormParameter("paused", true); request.AddFormParameter("paused", true);
} }
@@ -214,7 +214,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
catch (DownloadClientException ex) catch (DownloadClientException ex)
{ {
// if setCategory fails due to method not being found, then try older setLabel command for qBittorrent < v.3.3.5 // if setCategory fails due to method not being found, then try older setLabel command for qBittorrent < v.3.3.5
if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.NotFound) if (ex.InnerException is HttpException httpException && httpException.Response.StatusCode == HttpStatusCode.NotFound)
{ {
var setLabelRequest = BuildRequest(settings).Resource("/command/setLabel") var setLabelRequest = BuildRequest(settings).Resource("/command/setLabel")
.Post() .Post()
@@ -257,7 +257,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
catch (DownloadClientException ex) catch (DownloadClientException ex)
{ {
// qBittorrent rejects all Prio commands with 403: Forbidden if Options -> BitTorrent -> Torrent Queueing is not enabled // qBittorrent rejects all Prio commands with 403: Forbidden if Options -> BitTorrent -> Torrent Queueing is not enabled
if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.Forbidden) if (ex.InnerException is HttpException httpException && httpException.Response.StatusCode == HttpStatusCode.Forbidden)
{ {
return; return;
} }
@@ -266,22 +266,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
} }
} }
public void PauseTorrent(string hash, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/command/pause")
.Post()
.AddFormParameter("hash", hash);
ProcessRequest(request, settings);
}
public void ResumeTorrent(string hash, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/command/resume")
.Post()
.AddFormParameter("hash", hash);
ProcessRequest(request, settings);
}
public void SetForceStart(string hash, bool enabled, QBittorrentSettings settings) public void SetForceStart(string hash, bool enabled, QBittorrentSettings settings)
{ {
var request = BuildRequest(settings).Resource("/command/setForceStart") var request = BuildRequest(settings).Resource("/command/setForceStart")
@@ -246,14 +246,20 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
request.AddFormParameter("category", settings.MusicCategory); request.AddFormParameter("category", settings.MusicCategory);
} }
// Note: ForceStart is handled by separate api call // Avoid extraneous API version check if initial state is ForceStart
if ((QBittorrentState)settings.InitialState == QBittorrentState.Start) if ((QBittorrentState)settings.InitialState is QBittorrentState.Start or QBittorrentState.Stop)
{ {
request.AddFormParameter("paused", false); var stoppedParameterName = GetApiVersion(settings) >= new Version(2, 11, 0) ? "stopped" : "paused";
}
else if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause) // Note: ForceStart is handled by separate api call
{ if ((QBittorrentState)settings.InitialState == QBittorrentState.Start)
request.AddFormParameter("paused", true); {
request.AddFormParameter(stoppedParameterName, false);
}
else if ((QBittorrentState)settings.InitialState == QBittorrentState.Stop)
{
request.AddFormParameter(stoppedParameterName, true);
}
} }
if (settings.SequentialOrder) if (settings.SequentialOrder)
@@ -291,7 +297,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
catch (DownloadClientException ex) catch (DownloadClientException ex)
{ {
// setShareLimits was added in api v2.0.1 so catch it case of the unlikely event that someone has api v2.0 // setShareLimits was added in api v2.0.1 so catch it case of the unlikely event that someone has api v2.0
if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.NotFound) if (ex.InnerException is HttpException httpException && httpException.Response.StatusCode == HttpStatusCode.NotFound)
{ {
return; return;
} }
@@ -313,7 +319,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
catch (DownloadClientException ex) catch (DownloadClientException ex)
{ {
// qBittorrent rejects all Prio commands with 409: Conflict if Options -> BitTorrent -> Torrent Queueing is not enabled // qBittorrent rejects all Prio commands with 409: Conflict if Options -> BitTorrent -> Torrent Queueing is not enabled
if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.Conflict) if (ex.InnerException is HttpException httpException && httpException.Response.StatusCode == HttpStatusCode.Conflict)
{ {
return; return;
} }
@@ -322,22 +328,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
} }
} }
public void PauseTorrent(string hash, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/api/v2/torrents/pause")
.Post()
.AddFormParameter("hashes", hash);
ProcessRequest(request, settings);
}
public void ResumeTorrent(string hash, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/api/v2/torrents/resume")
.Post()
.AddFormParameter("hashes", hash);
ProcessRequest(request, settings);
}
public void SetForceStart(string hash, bool enabled, QBittorrentSettings settings) public void SetForceStart(string hash, bool enabled, QBittorrentSettings settings)
{ {
var request = BuildRequest(settings).Resource("/api/v2/torrents/setForceStart") var request = BuildRequest(settings).Resource("/api/v2/torrents/setForceStart")
@@ -1,9 +1,16 @@
using NzbDrone.Core.Annotations;
namespace NzbDrone.Core.Download.Clients.QBittorrent namespace NzbDrone.Core.Download.Clients.QBittorrent
{ {
public enum QBittorrentState public enum QBittorrentState
{ {
[FieldOption(Label = "Started")]
Start = 0, Start = 0,
[FieldOption(Label = "Force Started")]
ForceStart = 1, ForceStart = 1,
Pause = 2
[FieldOption(Label = "Stopped")]
Stop = 2
} }
} }
@@ -263,20 +263,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
status.OutputRootFolders = new List<OsPath> { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, category.FullPath) }; status.OutputRootFolders = new List<OsPath> { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, category.FullPath) };
} }
if (config.Misc.history_retention.IsNullOrWhiteSpace()) status.RemovesCompletedDownloads = RemovesCompletedDownloads(config);
{
status.RemovesCompletedDownloads = false;
}
else if (config.Misc.history_retention.EndsWith("d"))
{
int.TryParse(config.Misc.history_retention.AsSpan(0, config.Misc.history_retention.Length - 1),
out var daysRetention);
status.RemovesCompletedDownloads = daysRetention < 14;
}
else
{
status.RemovesCompletedDownloads = config.Misc.history_retention != "0";
}
return status; return status;
} }
@@ -518,6 +505,43 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
return categories.Contains(category); return categories.Contains(category);
} }
private bool RemovesCompletedDownloads(SabnzbdConfig config)
{
var retention = config.Misc.history_retention;
var option = config.Misc.history_retention_option;
var number = config.Misc.history_retention_number;
switch (option)
{
case "all":
return false;
case "number-archive":
case "number-delete":
return true;
case "days-archive":
case "days-delete":
return number < 14;
case "all-archive":
case "all-delete":
return true;
}
// TODO: Remove these checks once support for SABnzbd < 4.3 is removed
if (retention.IsNullOrWhiteSpace())
{
return false;
}
if (retention.EndsWith("d"))
{
int.TryParse(config.Misc.history_retention.AsSpan(0, config.Misc.history_retention.Length - 1),
out var daysRetention);
return daysRetention < 14;
}
return retention != "0";
}
private bool ValidatePath(DownloadClientItem downloadClientItem) private bool ValidatePath(DownloadClientItem downloadClientItem)
{ {
var downloadItemOutputPath = downloadClientItem.OutputPath; var downloadItemOutputPath = downloadClientItem.OutputPath;
@@ -30,6 +30,8 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
public bool enable_date_sorting { get; set; } public bool enable_date_sorting { get; set; }
public bool pre_check { get; set; } public bool pre_check { get; set; }
public string history_retention { get; set; } public string history_retention { get; set; }
public string history_retention_option { get; set; }
public int history_retention_number { get; set; }
} }
public class SabnzbdCategory public class SabnzbdCategory
@@ -4,6 +4,7 @@ using System.Net;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using NLog; using NLog;
using NzbDrone.Common.Cache; using NzbDrone.Common.Cache;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer; using NzbDrone.Common.Serializer;
@@ -208,7 +209,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
private void AuthenticateClient(HttpRequestBuilder requestBuilder, TransmissionSettings settings, bool reauthenticate = false) private void AuthenticateClient(HttpRequestBuilder requestBuilder, TransmissionSettings settings, bool reauthenticate = false)
{ {
var authKey = string.Format("{0}:{1}", requestBuilder.BaseUrl, settings.Password); var authKey = $"{requestBuilder.BaseUrl}:{settings.Password}";
var sessionId = _authSessionIDCache.Find(authKey); var sessionId = _authSessionIDCache.Find(authKey);
@@ -220,24 +221,26 @@ namespace NzbDrone.Core.Download.Clients.Transmission
authLoginRequest.SuppressHttpError = true; authLoginRequest.SuppressHttpError = true;
var response = _httpClient.Execute(authLoginRequest); var response = _httpClient.Execute(authLoginRequest);
if (response.StatusCode == HttpStatusCode.MovedPermanently)
{
var url = response.Headers.GetSingleValue("Location");
throw new DownloadClientException("Remote site redirected to " + url); switch (response.StatusCode)
}
else if (response.StatusCode == HttpStatusCode.Conflict)
{ {
sessionId = response.Headers.GetSingleValue("X-Transmission-Session-Id"); case HttpStatusCode.MovedPermanently:
var url = response.Headers.GetSingleValue("Location");
if (sessionId == null) throw new DownloadClientException("Remote site redirected to " + url);
{ case HttpStatusCode.Forbidden:
throw new DownloadClientException("Remote host did not return a Session Id."); throw new DownloadClientException($"Failed to authenticate with Transmission. It may be necessary to add {BuildInfo.AppName}'s IP address to RPC whitelist.");
} case HttpStatusCode.Conflict:
} sessionId = response.Headers.GetSingleValue("X-Transmission-Session-Id");
else
{ if (sessionId == null)
throw new DownloadClientAuthenticationException("Failed to authenticate with Transmission."); {
throw new DownloadClientException("Remote host did not return a Session Id.");
}
break;
default:
throw new DownloadClientAuthenticationException("Failed to authenticate with Transmission.");
} }
_logger.Debug("Transmission authentication succeeded."); _logger.Debug("Transmission authentication succeeded.");
@@ -41,18 +41,23 @@ namespace NzbDrone.Core.Download
var blockedProviders = new HashSet<int>(_downloadClientStatusService.GetBlockedProviders().Select(v => v.ProviderId)); var blockedProviders = new HashSet<int>(_downloadClientStatusService.GetBlockedProviders().Select(v => v.ProviderId));
var availableProviders = _downloadClientFactory.GetAvailableProviders().Where(v => v.Protocol == downloadProtocol).ToList(); var availableProviders = _downloadClientFactory.GetAvailableProviders().Where(v => v.Protocol == downloadProtocol).ToList();
if (tags != null) if (!availableProviders.Any())
{
return null;
}
if (tags is { Count: > 0 })
{ {
var matchingTagsClients = availableProviders.Where(i => i.Definition.Tags.Intersect(tags).Any()).ToList(); var matchingTagsClients = availableProviders.Where(i => i.Definition.Tags.Intersect(tags).Any()).ToList();
availableProviders = matchingTagsClients.Count > 0 ? availableProviders = matchingTagsClients.Count > 0 ?
matchingTagsClients : matchingTagsClients :
availableProviders.Where(i => i.Definition.Tags.Empty()).ToList(); availableProviders.Where(i => i.Definition.Tags.Empty()).ToList();
}
if (!availableProviders.Any()) if (!availableProviders.Any())
{ {
return null; throw new DownloadClientUnavailableException("No download client was found without tags or a matching author tag. Please check your settings.");
}
} }
if (indexerId > 0) if (indexerId > 0)
@@ -4,24 +4,24 @@ namespace NzbDrone.Core.Exceptions
{ {
public class AuthorNotFoundException : NzbDroneException public class AuthorNotFoundException : NzbDroneException
{ {
public string MusicBrainzId { get; set; } public string ForeignAuthorId { get; set; }
public AuthorNotFoundException(string musicbrainzId) public AuthorNotFoundException(string foreignAuthorId)
: base(string.Format("Author with id {0} was not found, it may have been removed from the metadata server.", musicbrainzId)) : base($"Author with id {foreignAuthorId} was not found, it may have been removed from the metadata server.")
{ {
MusicBrainzId = musicbrainzId; ForeignAuthorId = foreignAuthorId;
} }
public AuthorNotFoundException(string musicbrainzId, string message, params object[] args) public AuthorNotFoundException(string foreignAuthorId, string message, params object[] args)
: base(message, args) : base(message, args)
{ {
MusicBrainzId = musicbrainzId; ForeignAuthorId = foreignAuthorId;
} }
public AuthorNotFoundException(string musicbrainzId, string message) public AuthorNotFoundException(string foreignAuthorId, string message)
: base(message) : base(message)
{ {
MusicBrainzId = musicbrainzId; ForeignAuthorId = foreignAuthorId;
} }
} }
} }

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