Compare commits

..

108 Commits

Author SHA1 Message Date
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
Bogdan
b83a760873 Bump frontend packages 2024-10-20 13:22:14 +03:00
Bogdan
22ab50f76d Bump dotnet to 6.0.35 2024-10-20 13:22:14 +03:00
Mark McDowall
66758ca006 New: Show update settings on all platforms
(cherry picked from commit c023fc700896c7f0751c4ac63c4e1a89d6e1a9bb)
2024-10-20 11:43:51 +03:00
Mark McDowall
e7d7bc79f4 New: Allow major version updates to be installed
(cherry picked from commit 0e95ba2021b23cc65bce0a0620dd48e355250dab)
2024-10-20 11:43:51 +03:00
Bogdan
cfccb4f9c3 Bump version to 0.4.2 2024-10-20 08:07:10 +03:00
Mark McDowall
9312f17041 New: Use 307 redirect for requests missing URL Base
(cherry picked from commit 39074b0b1d040969f86d787c2346d5ed5a9f72dc)
2024-10-08 02:20:37 +03:00
Bogdan
8192c22910 Bump macOS runner version to 13 2024-10-06 16:30:37 +03:00
ManiMatter
0b1d6b677a Add '.temp*' to .gitignore (#3778) 2024-10-02 22:53:34 +03:00
Bogdan
d666df0189 Bump version to 0.4.1 2024-09-29 08:19:59 +03:00
Bogdan
10d8f345c1 Display naming example errors when all fields are empty
(cherry picked from commit 768af433d1655c587a9eee9b100f306ba4345f88)
2024-09-28 05:18:41 +03:00
Robin Dadswell
fb720b8714 Fixed: Telegram log message including token
(cherry picked from commit a7cb264cc8013d9a56aee7d5e41acfd76cde5f96)
2024-09-28 05:18:29 +03:00
Servarr
e8131b5791 Automated API Docs update 2024-09-25 10:34:30 +03:00
Bogdan
4f793f6b93 Remove $ from Discord delete notifications 2024-09-25 10:28:00 +03:00
Bogdan
4215c21c94 Add package needed for RemoveDiacritics 2024-09-23 05:46:26 +03:00
Paul DiLoreto
6913789adc New: Use instance name in forms authentication cookie name (#3761)
(cherry picked from commit 97ebaf279650082c6baee9563ef179921c5ed25a)
(cherry picked from commit faf9173b3b4a298e3afa9a186e66ba6764ac055e)
(cherry picked from commit 75fae9262c6ca003d24df9fcf035d75b1e90f994)

---------

Co-authored-by: Mark McDowall <mark@mcdowall.ca>
2024-09-23 05:45:01 +03:00
Mark McDowall
09e0c40792 Fixed: Limit redirects after login to local paths
(cherry picked from commit 14005d8d1054eafaba808337a109d5812f3e79e6)
2024-09-23 05:41:21 +03:00
Mark McDowall
baff805551 New: Return downloading magnets from Transmission
(cherry picked from commit 11a9dcb3890eaf99602900f37e64007f2fbf9b8e)
2024-09-23 05:40:25 +03:00
Bogdan
c885fe43cd Fix disabled style for monitor toggle button
(cherry picked from commit dde28cbd7e16b85f78d38c8dde7cf6bbb6119bb3)
2024-09-23 05:39:36 +03:00
Treycos
464a777722 Updated code action fixall value for VSCode
(cherry picked from commit 8af4246ff9baee4c291550102769a1186f65dc29)
2024-09-23 05:39:19 +03:00
momo
89e5999c85 Fix description for API key as query parameter
(cherry picked from commit 30c36fdc3baa686102ff124833c7963fc786f251)
2024-09-23 05:36:05 +03:00
Bogdan
b6fa332550 Ignore metadata tests temporarily once again 2024-09-23 05:35:27 +03:00
Bogdan
05f262dc0a Don't persist value for SslCertHash when checking for existence
(cherry picked from commit 98c4cbdd13dc49ad30e91343897b8bd006002489)
2024-09-07 16:26:28 -05:00
Bogdan
699b765ee9 Remove provider status on provider deletion
(cherry picked from commit f45713bff815b2a49a5cdad4afe62a53bbdf6a6e)
2024-09-07 16:26:11 -05:00
Mark McDowall
84beba2383 Don't hash files in development builds
(cherry picked from commit bc7799139e52b92956eb595fb87f44d7dda9a320)
2024-09-07 13:46:26 -05:00
Mark McDowall
62eceb9148 New: Default file log level changed to debug
(cherry picked from commit 9b528eb82914a05cfc3b67d4d6146ce51e86f68d)
2024-09-07 13:45:57 -05:00
Servarr
f46070d4b0 Translations update from Servarr Weblate (#3578)
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Ano10 <arnaudthommeray+github@ik.me>
Co-authored-by: Dream <seth.gecko.rr@gmail.com>
Co-authored-by: Gabriel Markowski <gmarkowski62@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Jason54 <jason54700.jg@gmail.com>
Co-authored-by: Kerk en IT <info@kerkenit.nl>
Co-authored-by: MattiaPell <mattiapellegrini16@gmail.com>
Co-authored-by: Nota Inutilis <hugo@notainutilis.fr>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: iMohmmedSA <i.mohmmed.i+1@gmail.com>
2024-09-07 13:44:59 -05:00
176 changed files with 6153 additions and 4595 deletions

1
.gitignore vendored
View File

@@ -120,6 +120,7 @@ _artifacts
_rawPackage/
_dotTrace*
_tests/
_temp*
*.Result.xml
coverage*.xml
coverage*.json

View File

@@ -9,18 +9,18 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '0.4.0'
majorVersion: '0.4.13'
minorVersion: $[counter('minorVersion', 1)]
readarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(readarrVersion)'
sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.424'
dotnetVersion: '6.0.427'
nodeVersion: '20.X'
innoVersion: '6.2.0'
windowsImage: 'windows-2022'
linuxImage: 'ubuntu-20.04'
macImage: 'macOS-12'
macImage: 'macOS-13'
trigger:
branches:
@@ -1102,19 +1102,19 @@ stages:
vmImage: ${{ variables.windowsImage }}
steps:
- checkout: self # Need history for Sonar analysis
- task: SonarCloudPrepare@2
- task: SonarCloudPrepare@3
env:
SONAR_SCANNER_OPTS: ''
inputs:
SonarCloud: 'SonarCloud'
organization: 'readarr'
scannerMode: 'CLI'
scannerMode: 'cli'
configMode: 'manual'
cliProjectKey: 'readarrui'
cliProjectName: 'ReadarrUI'
cliProjectVersion: '$(readarrVersion)'
cliSources: './frontend'
- task: SonarCloudAnalyze@2
- task: SonarCloudAnalyze@3
- job: Api_Docs
displayName: API Docs
@@ -1190,12 +1190,12 @@ stages:
submodules: true
- powershell: Set-Service SCardSvr -StartupType Manual
displayName: Enable Windows Test Service
- task: SonarCloudPrepare@2
- task: SonarCloudPrepare@3
condition: eq(variables['System.PullRequest.IsFork'], 'False')
inputs:
SonarCloud: 'SonarCloud'
organization: 'readarr'
scannerMode: 'MSBuild'
scannerMode: 'dotnet'
projectKey: 'Readarr_Readarr'
projectName: 'Readarr'
projectVersion: '$(readarrVersion)'
@@ -1208,10 +1208,10 @@ stages:
./build.sh --backend -f net6.0 -r win-x64
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
displayName: Coverage Unit Tests
- task: SonarCloudAnalyze@2
- task: SonarCloudAnalyze@3
condition: eq(variables['System.PullRequest.IsFork'], 'False')
displayName: Publish SonarCloud Results
- task: reportgenerator@5
- task: reportgenerator@5.3.11
displayName: Generate Coverage Report
inputs:
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'

View File

@@ -9,7 +9,7 @@
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll": true
"source.fixAll": "explicit"
},
"typescript.preferences.quoteStyle": "single",

View File

@@ -26,6 +26,7 @@ module.exports = (env) => {
const config = {
mode: isProduction ? 'production' : 'development',
devtool: isProduction ? 'source-map' : 'eval-source-map',
target: 'web',
stats: {
children: false
@@ -67,7 +68,7 @@ module.exports = (env) => {
output: {
path: distFolder,
publicPath: '/',
filename: '[name]-[contenthash].js',
filename: isProduction ? '[name]-[contenthash].js' : '[name].js',
sourceMapFilename: '[file].map'
},
@@ -92,7 +93,7 @@ module.exports = (env) => {
new MiniCssExtractPlugin({
filename: 'Content/styles.css',
chunkFilename: 'Content/[id]-[chunkhash].css'
chunkFilename: isProduction ? 'Content/[id]-[chunkhash].css' : 'Content/[id].css'
}),
new HtmlWebpackPlugin({
@@ -181,7 +182,7 @@ module.exports = (env) => {
loose: true,
debug: false,
useBuiltIns: 'entry',
corejs: 3
corejs: '3.39'
}
]
]
@@ -202,7 +203,7 @@ module.exports = (env) => {
options: {
importLoaders: 1,
modules: {
localIdentName: '[name]/[local]/[hash:base64:5]'
localIdentName: isProduction ? '[name]/[local]/[hash:base64:5]' : '[name]/[local]'
}
}
},

View File

@@ -165,7 +165,8 @@ function HistoryDetails(props) {
if (eventType === 'downloadFailed') {
const {
message
message,
indexer
} = data;
return (
@@ -177,11 +178,21 @@ function HistoryDetails(props) {
/>
{
!!message &&
indexer ?
<DescriptionListItem
title={translate('Indexer')}
data={indexer}
/> :
null
}
{
message ?
<DescriptionListItem
title={translate('Message')}
data={message}
/>
/> :
null
}
</DescriptionList>
);

View File

@@ -32,7 +32,7 @@ import LogsTableConnector from 'System/Events/LogsTableConnector';
import Logs from 'System/Logs/Logs';
import Status from 'System/Status/Status';
import Tasks from 'System/Tasks/Tasks';
import UpdatesConnector from 'System/Updates/UpdatesConnector';
import Updates from 'System/Updates/Updates';
import UnmappedFilesTableConnector from 'UnmappedFiles/UnmappedFilesTableConnector';
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
@@ -247,7 +247,7 @@ function AppRoutes(props) {
<Route
path="/system/updates"
component={UpdatesConnector}
component={Updates}
/>
<Route

View File

@@ -1,6 +1,7 @@
import AuthorsAppState from './AuthorsAppState';
import CommandAppState from './CommandAppState';
import SettingsAppState from './SettingsAppState';
import SystemAppState from './SystemAppState';
import TagsAppState from './TagsAppState';
interface FilterBuilderPropOption {
@@ -35,10 +36,24 @@ export interface CustomFilter {
filers: PropertyFilter[];
}
export interface AppSectionState {
isConnected: boolean;
isReconnecting: boolean;
version: string;
prevVersion?: string;
dimensions: {
isSmallScreen: boolean;
width: number;
height: number;
};
}
interface AppState {
app: AppSectionState;
authors: AuthorsAppState;
commands: CommandAppState;
settings: SettingsAppState;
system: SystemAppState;
tags: TagsAppState;
}

View File

@@ -1,5 +1,6 @@
import AppSectionState, {
AppSectionDeleteState,
AppSectionItemState,
AppSectionSaveState,
} from 'App/State/AppSectionState';
import DownloadClient from 'typings/DownloadClient';
@@ -7,13 +8,16 @@ import ImportList from 'typings/ImportList';
import Indexer from 'typings/Indexer';
import IndexerFlag from 'typings/IndexerFlag';
import Notification from 'typings/Notification';
import { UiSettings } from 'typings/UiSettings';
import General from 'typings/Settings/General';
import UiSettings from 'typings/Settings/UiSettings';
export interface DownloadClientAppState
extends AppSectionState<DownloadClient>,
AppSectionDeleteState,
AppSectionSaveState {}
export type GeneralAppState = AppSectionItemState<General>;
export interface ImportListAppState
extends AppSectionState<ImportList>,
AppSectionDeleteState,
@@ -33,11 +37,12 @@ export type UiSettingsAppState = AppSectionState<UiSettings>;
interface SettingsAppState {
downloadClients: DownloadClientAppState;
general: GeneralAppState;
importLists: ImportListAppState;
indexerFlags: IndexerFlagSettingsAppState;
indexers: IndexerAppState;
notifications: NotificationAppState;
uiSettings: UiSettingsAppState;
ui: UiSettingsAppState;
}
export default SettingsAppState;

View File

@@ -0,0 +1,13 @@
import SystemStatus from 'typings/SystemStatus';
import Update from 'typings/Update';
import AppSectionState, { AppSectionItemState } from './AppSectionState';
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
export type UpdateAppState = AppSectionState<Update>;
interface SystemAppState {
updates: UpdateAppState;
status: SystemStatusAppState;
}
export default SystemAppState;

View File

@@ -1,5 +1,7 @@
import ModelBase from 'App/ModelBase';
export type AuthorStatus = 'continuing' | 'ended';
interface Author extends ModelBase {
added: string;
genres: string[];
@@ -10,6 +12,7 @@ interface Author extends ModelBase {
metadataProfileId: number;
rootFolderPath: string;
sortName: string;
status: AuthorStatus;
tags: number[];
authorName: string;
isSaving?: boolean;

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;
}

View File

@@ -7,6 +7,7 @@ import AuthorHistoryTable from 'Author/History/AuthorHistoryTable';
import MonitoringOptionsModal from 'Author/MonitoringOptions/MonitoringOptionsModal';
import BookEditorFooter from 'Book/Editor/BookEditorFooter';
import BookFileEditorTable from 'BookFile/Editor/BookFileEditorTable';
import Alert from 'Components/Alert';
import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link';
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 PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
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 InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable';
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
@@ -412,22 +413,25 @@ class AuthorDetails extends Component {
<div className={styles.contentContainer}>
{
!isPopulated && !booksError && !bookFilesError &&
<LoadingIndicator />
!isPopulated && !booksError && !bookFilesError ?
<LoadingIndicator /> :
null
}
{
!isFetching && booksError &&
<div>
!isFetching && booksError ?
<Alert kind={kinds.DANGER}>
{translate('LoadingBooksFailed')}
</div>
</Alert> :
null
}
{
!isFetching && bookFilesError &&
<div>
!isFetching && bookFilesError ?
<Alert kind={kinds.DANGER}>
{translate('LoadingBookFilesFailed')}
</div>
</Alert> :
null
}
{

View File

@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import TextTruncate from 'react-text-truncate';
import AuthorPoster from 'Author/AuthorPoster';
import { getAuthorStatusDetails } from 'Author/AuthorStatus';
import HeartRating from 'Components/HeartRating';
import Icon from 'Components/Icon';
import Label from 'Components/Label';
@@ -11,7 +12,7 @@ import MonitorToggleButton from 'Components/MonitorToggleButton';
import Popover from 'Components/Tooltip/Popover';
import Tooltip from 'Components/Tooltip/Tooltip';
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 formatBytes from 'Utilities/Number/formatBytes';
import stripHtml from 'Utilities/String/stripHtml';
@@ -87,11 +88,11 @@ class AuthorDetailsHeader extends Component {
titleWidth
} = this.state;
const statusDetails = getAuthorStatusDetails(status);
const fanartUrl = getFanartUrl(images);
const marqueeWidth = titleWidth - (isSmallScreen ? 85 : 160);
const continuing = status === 'continuing';
let bookFilesCountMessage = translate('BookFilesCountMessage');
if (bookFileCount === 1) {
@@ -213,7 +214,7 @@ class AuthorDetailsHeader extends Component {
<span className={styles.qualityProfileName}>
{
<QualityProfileNameConnector
<QualityProfileName
qualityProfileId={qualityProfileId}
/>
}
@@ -236,16 +237,16 @@ class AuthorDetailsHeader extends Component {
<Label
className={styles.detailsLabel}
title={continuing ? translate('ContinuingMoreBooksAreExpected') : translate('ContinuingNoAdditionalBooksAreExpected')}
title={statusDetails.message}
size={sizes.LARGE}
>
<Icon
name={continuing ? icons.AUTHOR_CONTINUING : icons.AUTHOR_ENDED}
name={statusDetails.icon}
size={17}
/>
<span className={styles.qualityProfileName}>
{continuing ? 'Continuing' : 'Deceased'}
{statusDetails.title}
</span>
</Label>

View File

@@ -5,6 +5,7 @@ import dimensions from 'Styles/Variables/dimensions';
import formatDateTime from 'Utilities/Date/formatDateTime';
import getRelativeDate from 'Utilities/Date/getRelativeDate';
import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
import AuthorIndexOverviewInfoRow from './AuthorIndexOverviewInfoRow';
import styles from './AuthorIndexOverviewInfo.css';
@@ -76,9 +77,9 @@ function getInfoRowProps(row, props) {
};
}
if (name === 'qualityProfileId') {
if (name === 'qualityProfileId' && !!props.qualityProfile?.name) {
return {
title: 'Quality Profile',
title: translate('QualityProfile'),
iconName: icons.PROFILE,
label: props.qualityProfile.name
};

View File

@@ -235,12 +235,12 @@ class AuthorIndexPoster extends Component {
</div>
}
{
showQualityProfile &&
<div className={styles.title}>
{qualityProfile.name}
</div>
}
{showQualityProfile && !!qualityProfile?.name ? (
<div className={styles.title} title={translate('QualityProfile')}>
{qualityProfile.name}
</div>
) : null}
{
nextAiring &&
<div className={styles.nextAiring}>

View File

@@ -209,7 +209,7 @@ class AuthorIndexRow extends Component {
key={name}
className={styles[name]}
>
{qualityProfile.name}
{qualityProfile?.name ?? ''}
</VirtualTableRowCell>
);
}
@@ -220,7 +220,7 @@ class AuthorIndexRow extends Component {
key={name}
className={styles[name]}
>
{metadataProfile.name}
{metadataProfile?.name ?? ''}
</VirtualTableRowCell>
);
}

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React from 'react';
import { getAuthorStatusDetails } from 'Author/AuthorStatus';
import Icon from 'Components/Icon';
import VirtualTableRowCell from 'Components/Table/Cells/TableRowCell';
import { icons } from 'Helpers/Props';
@@ -15,6 +16,8 @@ function AuthorStatusCell(props) {
...otherProps
} = props;
const statusDetails = getAuthorStatusDetails(status);
return (
<Component
className={className}
@@ -28,8 +31,8 @@ function AuthorStatusCell(props) {
<Icon
className={styles.statusIcon}
name={status === 'ended' ? icons.AUTHOR_ENDED : icons.AUTHOR_CONTINUING}
title={status === 'ended' ? translate('StatusEndedDeceased') : translate('StatusEndedContinuing')}
name={statusDetails.icon}
title={`${statusDetails.title}: ${statusDetails.message}`}
/>
</Component>
);

View File

@@ -5,6 +5,7 @@ import dimensions from 'Styles/Variables/dimensions';
import formatDateTime from 'Utilities/Date/formatDateTime';
import getRelativeDate from 'Utilities/Date/getRelativeDate';
import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
import BookIndexOverviewInfoRow from './BookIndexOverviewInfoRow';
import styles from './BookIndexOverviewInfo.css';
@@ -71,9 +72,9 @@ function getInfoRowProps(row, props) {
};
}
if (name === 'qualityProfileId') {
if (name === 'qualityProfileId' && !!props.qualityProfile?.name) {
return {
title: 'Quality Profile',
title: translate('QualityProfile'),
iconName: icons.PROFILE,
label: props.qualityProfile.name
};

View File

@@ -250,12 +250,12 @@ class BookIndexPoster extends Component {
</div>
}
{
showQualityProfile &&
<div className={styles.title}>
{qualityProfile.name}
</div>
}
{showQualityProfile && !!qualityProfile?.name ? (
<div className={styles.title} title={translate('QualityProfile')}>
{qualityProfile.name}
</div>
) : null}
{
nextAiring &&
<div className={styles.nextAiring}>

View File

@@ -195,7 +195,7 @@ class BookIndexRow extends Component {
key={name}
className={styles[name]}
>
{qualityProfile.name}
{qualityProfile?.name ?? ''}
</VirtualTableRowCell>
);
}

View File

@@ -1,12 +1,11 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import AuthorNameLink from 'Author/AuthorNameLink';
import { getAuthorStatusDetails } from 'Author/AuthorStatus';
import Icon from 'Components/Icon';
import MonitorToggleButton from 'Components/MonitorToggleButton';
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import BookshelfBook from './BookshelfBook';
import styles from './BookshelfRow.css';
@@ -30,6 +29,8 @@ class BookshelfRow extends Component {
onBookMonitoredPress
} = this.props;
const statusDetails = getAuthorStatusDetails(status);
return (
<>
<VirtualTableSelectCell
@@ -52,8 +53,8 @@ class BookshelfRow extends Component {
<VirtualTableRowCell className={styles.status}>
<Icon
className={styles.statusIcon}
name={status === 'ended' ? icons.AUTHOR_ENDED : icons.AUTHOR_CONTINUING}
title={status === 'ended' ? translate('StatusEndedEnded') : translate('StatusEndedContinuing')}
name={statusDetails.icon}
title={statusDetails.title}
/>
</VirtualTableRowCell>

View File

@@ -20,6 +20,8 @@ import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
import TextInput from './TextInput';
import styles from './EnhancedSelectInput.css';
const MINIMUM_DISTANCE_FROM_EDGE = 10;
function isArrowKey(keyCode) {
return keyCode === keyCodes.UP_ARROW || keyCode === keyCodes.DOWN_ARROW;
}
@@ -137,18 +139,9 @@ class EnhancedSelectInput extends Component {
// Listeners
onComputeMaxHeight = (data) => {
const {
top,
bottom
} = data.offsets.reference;
const windowHeight = window.innerHeight;
if ((/^botton/).test(data.placement)) {
data.styles.maxHeight = windowHeight - bottom;
} else {
data.styles.maxHeight = top;
}
data.styles.maxHeight = windowHeight - MINIMUM_DISTANCE_FROM_EDGE;
return data;
};
@@ -457,6 +450,10 @@ class EnhancedSelectInput extends Component {
order: 851,
enabled: true,
fn: this.onComputeMaxHeight
},
preventOverflow: {
enabled: true,
boundariesElement: 'viewport'
}
}}
>

View File

@@ -0,0 +1,54 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { align, kinds, sizes } from 'Helpers/Props';
import Link from './Link';
import styles from './Button.css';
class Button extends Component {
//
// Render
render() {
const {
className,
buttonGroupPosition,
kind,
size,
children,
...otherProps
} = this.props;
return (
<Link
className={classNames(
className,
styles[kind],
styles[size],
buttonGroupPosition && styles[buttonGroupPosition]
)}
{...otherProps}
>
{children}
</Link>
);
}
}
Button.propTypes = {
className: PropTypes.string.isRequired,
buttonGroupPosition: PropTypes.oneOf(align.all),
kind: PropTypes.oneOf(kinds.all),
size: PropTypes.oneOf(sizes.all),
children: PropTypes.node
};
Button.defaultProps = {
className: styles.button,
kind: kinds.DEFAULT,
size: sizes.MEDIUM
};
export default Button;

View File

@@ -1,35 +0,0 @@
import classNames from 'classnames';
import React from 'react';
import { align, kinds, sizes } from 'Helpers/Props';
import Link, { LinkProps } from './Link';
import styles from './Button.css';
export interface ButtonProps extends Omit<LinkProps, 'children' | 'size'> {
buttonGroupPosition?: Extract<
(typeof align.all)[number],
keyof typeof styles
>;
kind?: Extract<(typeof kinds.all)[number], keyof typeof styles>;
size?: Extract<(typeof sizes.all)[number], keyof typeof styles>;
children: Required<LinkProps['children']>;
}
export default function Button({
className = styles.button,
buttonGroupPosition,
kind = kinds.DEFAULT,
size = sizes.MEDIUM,
...otherProps
}: ButtonProps) {
return (
<Link
className={classNames(
className,
styles[kind],
styles[size],
buttonGroupPosition && styles[buttonGroupPosition]
)}
{...otherProps}
/>
);
}

View File

@@ -83,13 +83,6 @@
}
@media only screen and (max-width: $breakpointMedium) {
.modal.small,
.modal.medium {
width: 90%;
}
}
@media only screen and (max-width: $breakpointSmall) {
.modalContainer {
position: fixed;
}

View File

@@ -3,9 +3,9 @@
padding: 0;
font-size: inherit;
}
.isDisabled {
color: var(--disabledColor);
cursor: not-allowed;
&.isDisabled {
color: var(--disabledColor);
cursor: not-allowed;
}
}

View File

@@ -4,7 +4,7 @@
line-height: 1.52857143;
}
@media only screen and (max-width: $breakpointSmall) {
@media only screen and (max-width: $breakpointMedium) {
.cell {
white-space: nowrap;
}

View File

@@ -7,7 +7,7 @@
white-space: nowrap;
}
@media only screen and (max-width: $breakpointSmall) {
@media only screen and (max-width: $breakpointMedium) {
.cell {
white-space: nowrap;
}

View File

@@ -10,7 +10,7 @@
border-collapse: collapse;
}
@media only screen and (max-width: $breakpointSmall) {
@media only screen and (max-width: $breakpointMedium) {
.tableContainer {
min-width: 100%;
width: fit-content;

View File

@@ -9,7 +9,7 @@
margin-left: 10px;
}
@media only screen and (max-width: $breakpointSmall) {
@media only screen and (max-width: $breakpointMedium) {
.headerCell {
white-space: nowrap;
}

View File

@@ -60,7 +60,7 @@
height: 25px;
}
@media only screen and (max-width: $breakpointSmall) {
@media only screen and (max-width: $breakpointMedium) {
.pager {
flex-wrap: wrap;
}

View File

@@ -9,7 +9,7 @@
margin-left: 10px;
}
@media only screen and (max-width: $breakpointSmall) {
@media only screen and (max-width: $breakpointMedium) {
.headerCell {
white-space: nowrap;
}

View File

@@ -10,11 +10,11 @@ import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import Column from 'Components/Table/Column';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import useSelectState from 'Helpers/Hooks/useSelectState';
import { kinds } from 'Helpers/Props';
import SortDirection from 'Helpers/Props/SortDirection';
import {
bulkDeleteDownloadClients,
bulkEditDownloadClients,
@@ -35,7 +35,7 @@ type OnSelectedChangeCallback = React.ComponentProps<
typeof ManageDownloadClientsModalRow
>['onSelectedChange'];
const COLUMNS = [
const COLUMNS: Column[] = [
{
name: 'name',
label: () => translate('Name'),
@@ -82,8 +82,6 @@ const COLUMNS = [
interface ManageDownloadClientsModalContentProps {
onModalClose(): void;
sortKey?: string;
sortDirection?: SortDirection;
}
function ManageDownloadClientsModalContent(

View File

@@ -18,7 +18,6 @@ function UpdateSettings(props) {
const {
advancedSettings,
settings,
isWindows,
packageUpdateMechanism,
onInputChange
} = props;
@@ -44,10 +43,10 @@ function UpdateSettings(props) {
value: titleCase(packageUpdateMechanism)
});
} else {
updateOptions.push({ key: 'builtIn', value: 'Built-In' });
updateOptions.push({ key: 'builtIn', value: translate('BuiltIn') });
}
updateOptions.push({ key: 'script', value: 'Script' });
updateOptions.push({ key: 'script', value: translate('Script') });
return (
<FieldSet legend={translate('Updates')}>
@@ -60,8 +59,8 @@ function UpdateSettings(props) {
<FormInputGroup
type={inputTypes.AUTO_COMPLETE}
name="branch"
helpText={usingExternalUpdateMechanism ? translate('UsingExternalUpdateMechanismBranchUsedByExternalUpdateMechanism') : translate('UsingExternalUpdateMechanismBranchToUseToUpdateReadarr')}
helpLink="https://wiki.servarr.com/readarr/faq#how-do-I-update-my-readarr"
helpText={usingExternalUpdateMechanism ? translate('BranchUpdateMechanism') : translate('BranchUpdate')}
helpLink="https://wiki.servarr.com/readarr/settings#updates"
{...branch}
values={branchValues}
onChange={onInputChange}
@@ -69,62 +68,59 @@ function UpdateSettings(props) {
/>
</FormGroup>
{
!isWindows &&
<div>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('Automatic')}</FormLabel>
<div>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('Automatic')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="updateAutomatically"
helpText={translate('UpdateAutomaticallyHelpText')}
helpTextWarning={updateMechanism.value === 'docker' ? translate('AutomaticUpdatesDisabledDocker', { appName: 'Readarr' }) : undefined}
onChange={onInputChange}
{...updateAutomatically}
/>
</FormGroup>
<FormInputGroup
type={inputTypes.CHECK}
name="updateAutomatically"
helpText={translate('UpdateAutomaticallyHelpText')}
helpTextWarning={updateMechanism.value === 'docker' ? translate('AutomaticUpdatesDisabledDocker') : undefined}
onChange={onInputChange}
{...updateAutomatically}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('Mechanism')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="updateMechanism"
values={updateOptions}
helpText={translate('UpdateMechanismHelpText')}
helpLink="https://wiki.servarr.com/readarr/settings#updates"
onChange={onInputChange}
{...updateMechanism}
/>
</FormGroup>
{
updateMechanism.value === 'script' &&
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('Mechanism')}</FormLabel>
<FormLabel>{translate('ScriptPath')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="updateMechanism"
values={updateOptions}
helpText={translate('UpdateMechanismHelpText')}
helpLink="https://wiki.servarr.com/readarr/faq#how-do-i-update-my-readarr"
type={inputTypes.TEXT}
name="updateScriptPath"
helpText={translate('UpdateScriptPathHelpText')}
onChange={onInputChange}
{...updateMechanism}
{...updateScriptPath}
/>
</FormGroup>
{
updateMechanism.value === 'script' &&
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('ScriptPath')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="updateScriptPath"
helpText={translate('UpdateScriptPathHelpText')}
onChange={onInputChange}
{...updateScriptPath}
/>
</FormGroup>
}
</div>
}
}
</div>
</FieldSet>
);
}

View File

@@ -76,7 +76,7 @@ function EditImportListExclusionModalContent(props) {
<FormGroup>
<FormLabel>
{translate('MusicbrainzId')}
{translate('ForeignId')}
</FormLabel>
<FormInputGroup

View File

@@ -10,11 +10,11 @@ import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import Column from 'Components/Table/Column';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import useSelectState from 'Helpers/Hooks/useSelectState';
import { kinds } from 'Helpers/Props';
import SortDirection from 'Helpers/Props/SortDirection';
import {
bulkDeleteIndexers,
bulkEditIndexers,
@@ -35,7 +35,7 @@ type OnSelectedChangeCallback = React.ComponentProps<
typeof ManageIndexersModalRow
>['onSelectedChange'];
const COLUMNS = [
const COLUMNS: Column[] = [
{
name: 'name',
label: () => translate('Name'),
@@ -82,8 +82,6 @@ const COLUMNS = [
interface ManageIndexersModalContentProps {
onModalClose(): void;
sortKey?: string;
sortDirection?: SortDirection;
}
function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {

View File

@@ -1,4 +1,3 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
@@ -15,11 +14,11 @@ function createMapStateToProps() {
(state) => state.settings.advancedSettings,
(state) => state.settings.namingExamples,
createSettingsSectionSelector(SECTION),
(advancedSettings, examples, sectionSettings) => {
(advancedSettings, namingExamples, sectionSettings) => {
return {
advancedSettings,
examples: examples.item,
examplesPopulated: !_.isEmpty(examples.item),
examples: namingExamples.item,
examplesPopulated: namingExamples.isPopulated,
...sectionSettings
};
}

View File

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

View File

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

View File

@@ -45,6 +45,12 @@ export const defaultState = {
isSortable: true,
isVisible: true
},
{
name: 'books.lastSearchTime',
label: 'Last Searched',
isSortable: true,
isVisible: false
},
{
name: 'actions',
columnLabel: 'Actions',
@@ -108,6 +114,12 @@ export const defaultState = {
isSortable: true,
isVisible: true
},
{
name: 'books.lastSearchTime',
label: 'Last Searched',
isSortable: true,
isVisible: false
},
{
name: 'actions',
columnLabel: 'Actions',

View File

@@ -1,50 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
import styles from './UpdateChanges.css';
class UpdateChanges extends Component {
//
// Render
render() {
const {
title,
changes
} = this.props;
if (changes.length === 0) {
return null;
}
return (
<div>
<div className={styles.title}>{title}</div>
<ul>
{
changes.map((change, index) => {
const checkChange = change.replace(/#\d{4,5}\b/g, (match, contents) => {
return `[${match}](https://github.com/Readarr/Readarr/issues/${match.substring(1)})`;
});
return (
<li key={index}>
<InlineMarkdown data={checkChange} />
</li>
);
})
}
</ul>
</div>
);
}
}
UpdateChanges.propTypes = {
title: PropTypes.string.isRequired,
changes: PropTypes.arrayOf(PropTypes.string)
};
export default UpdateChanges;

View File

@@ -0,0 +1,43 @@
import React from 'react';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
import styles from './UpdateChanges.css';
interface UpdateChangesProps {
title: string;
changes: string[];
}
function UpdateChanges(props: UpdateChangesProps) {
const { title, changes } = props;
if (changes.length === 0) {
return null;
}
const uniqueChanges = [...new Set(changes)];
return (
<div>
<div className={styles.title}>{title}</div>
<ul>
{uniqueChanges.map((change, index) => {
const checkChange = change.replace(
/#\d{4,5}\b/g,
(match) =>
`[${match}](https://github.com/Readarr/Readarr/issues/${match.substring(
1
)})`
);
return (
<li key={index}>
<InlineMarkdown data={checkChange} />
</li>
);
})}
</ul>
</div>
);
}
export default UpdateChanges;

View File

@@ -1,252 +0,0 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component, Fragment } from 'react';
import Alert from 'Components/Alert';
import Icon from 'Components/Icon';
import Label from 'Components/Label';
import SpinnerButton from 'Components/Link/SpinnerButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import { icons, kinds } from 'Helpers/Props';
import formatDate from 'Utilities/Date/formatDate';
import formatDateTime from 'Utilities/Date/formatDateTime';
import translate from 'Utilities/String/translate';
import UpdateChanges from './UpdateChanges';
import styles from './Updates.css';
class Updates extends Component {
//
// Render
render() {
const {
currentVersion,
isFetching,
isPopulated,
updatesError,
generalSettingsError,
items,
isInstallingUpdate,
updateMechanism,
isDocker,
updateMechanismMessage,
shortDateFormat,
longDateFormat,
timeFormat,
onInstallLatestPress
} = this.props;
const hasError = !!(updatesError || generalSettingsError);
const hasUpdates = isPopulated && !hasError && items.length > 0;
const noUpdates = isPopulated && !hasError && !items.length;
const hasUpdateToInstall = hasUpdates && _.some(items, { installable: true, latest: true });
const noUpdateToInstall = hasUpdates && !hasUpdateToInstall;
const externalUpdaterPrefix = 'Unable to update Readarr directly,';
const externalUpdaterMessages = {
external: 'Readarr is configured to use an external update mechanism',
apt: 'use apt to install the update',
docker: 'update the docker container to receive the update'
};
return (
<PageContent title={translate('Updates')}>
<PageContentBody>
{
!isPopulated && !hasError &&
<LoadingIndicator />
}
{
noUpdates &&
<Alert kind={kinds.INFO}>
{translate('NoUpdatesAreAvailable')}
</Alert>
}
{
hasUpdateToInstall &&
<div className={styles.messageContainer}>
{
(updateMechanism === 'builtIn' || updateMechanism === 'script') && !isDocker ?
<SpinnerButton
className={styles.updateAvailable}
kind={kinds.PRIMARY}
isSpinning={isInstallingUpdate}
onPress={onInstallLatestPress}
>
Install Latest
</SpinnerButton> :
<Fragment>
<Icon
name={icons.WARNING}
kind={kinds.WARNING}
size={30}
/>
<div className={styles.message}>
{externalUpdaterPrefix} <InlineMarkdown data={updateMechanismMessage || externalUpdaterMessages[updateMechanism] || externalUpdaterMessages.external} />
</div>
</Fragment>
}
{
isFetching &&
<LoadingIndicator
className={styles.loading}
size={20}
/>
}
</div>
}
{
noUpdateToInstall &&
<div className={styles.messageContainer}>
<Icon
className={styles.upToDateIcon}
name={icons.CHECK_CIRCLE}
size={30}
/>
<div className={styles.message}>
The latest version of Readarr is already installed
</div>
{
isFetching &&
<LoadingIndicator
className={styles.loading}
size={20}
/>
}
</div>
}
{
hasUpdates &&
<div>
{
items.map((update) => {
const hasChanges = !!update.changes;
return (
<div
key={update.version}
className={styles.update}
>
<div className={styles.info}>
<div className={styles.version}>{update.version}</div>
<div className={styles.space}>&mdash;</div>
<div
className={styles.date}
title={formatDateTime(update.releaseDate, longDateFormat, timeFormat)}
>
{formatDate(update.releaseDate, shortDateFormat)}
</div>
{
update.branch === 'master' ?
null :
<Label
className={styles.label}
>
{update.branch}
</Label>
}
{
update.version === currentVersion ?
<Label
className={styles.label}
kind={kinds.SUCCESS}
title={formatDateTime(update.installedOn, longDateFormat, timeFormat)}
>
Currently Installed
</Label> :
null
}
{
update.version !== currentVersion && update.installedOn ?
<Label
className={styles.label}
kind={kinds.INVERSE}
title={formatDateTime(update.installedOn, longDateFormat, timeFormat)}
>
Previously Installed
</Label> :
null
}
</div>
{
!hasChanges &&
<div>
{translate('MaintenanceRelease')}
</div>
}
{
hasChanges &&
<div className={styles.changes}>
<UpdateChanges
title={translate('New')}
changes={update.changes.new}
/>
<UpdateChanges
title={translate('Fixed')}
changes={update.changes.fixed}
/>
</div>
}
</div>
);
})
}
</div>
}
{
!!updatesError &&
<div>
Failed to fetch updates
</div>
}
{
!!generalSettingsError &&
<div>
Failed to update settings
</div>
}
</PageContentBody>
</PageContent>
);
}
}
Updates.propTypes = {
currentVersion: PropTypes.string.isRequired,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
updatesError: PropTypes.object,
generalSettingsError: PropTypes.object,
items: PropTypes.array.isRequired,
isInstallingUpdate: PropTypes.bool.isRequired,
isDocker: PropTypes.bool.isRequired,
updateMechanism: PropTypes.string,
updateMechanismMessage: PropTypes.string,
shortDateFormat: PropTypes.string.isRequired,
longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
onInstallLatestPress: PropTypes.func.isRequired
};
export default Updates;

View File

@@ -0,0 +1,303 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import * as commandNames from 'Commands/commandNames';
import Alert from 'Components/Alert';
import Icon from 'Components/Icon';
import Label from 'Components/Label';
import SpinnerButton from 'Components/Link/SpinnerButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import { icons, kinds } from 'Helpers/Props';
import { executeCommand } from 'Store/Actions/commandActions';
import { fetchGeneralSettings } from 'Store/Actions/settingsActions';
import { fetchUpdates } from 'Store/Actions/systemActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import { UpdateMechanism } from 'typings/Settings/General';
import formatDate from 'Utilities/Date/formatDate';
import formatDateTime from 'Utilities/Date/formatDateTime';
import translate from 'Utilities/String/translate';
import UpdateChanges from './UpdateChanges';
import styles from './Updates.css';
const VERSION_REGEX = /\d+\.\d+\.\d+\.\d+/i;
function createUpdatesSelector() {
return createSelector(
(state: AppState) => state.system.updates,
(state: AppState) => state.settings.general,
(updates, generalSettings) => {
const { error: updatesError, items } = updates;
const isFetching = updates.isFetching || generalSettings.isFetching;
const isPopulated = updates.isPopulated && generalSettings.isPopulated;
return {
isFetching,
isPopulated,
updatesError,
generalSettingsError: generalSettings.error,
items,
updateMechanism: generalSettings.item.updateMechanism,
};
}
);
}
function Updates() {
const currentVersion = useSelector((state: AppState) => state.app.version);
const { packageUpdateMechanismMessage } = useSelector(
createSystemStatusSelector()
);
const { shortDateFormat, longDateFormat, timeFormat } = useSelector(
createUISettingsSelector()
);
const isInstallingUpdate = useSelector(
createCommandExecutingSelector(commandNames.APPLICATION_UPDATE)
);
const {
isFetching,
isPopulated,
updatesError,
generalSettingsError,
items,
updateMechanism,
} = useSelector(createUpdatesSelector());
const dispatch = useDispatch();
const [isMajorUpdateModalOpen, setIsMajorUpdateModalOpen] = useState(false);
const hasError = !!(updatesError || generalSettingsError);
const hasUpdates = isPopulated && !hasError && items.length > 0;
const noUpdates = isPopulated && !hasError && !items.length;
const externalUpdaterPrefix = translate('UpdateAppDirectlyLoadError');
const externalUpdaterMessages: Partial<Record<UpdateMechanism, string>> = {
external: translate('ExternalUpdater'),
apt: translate('AptUpdater'),
docker: translate('DockerUpdater'),
};
const { isMajorUpdate, hasUpdateToInstall } = useMemo(() => {
const majorVersion = parseInt(
currentVersion.match(VERSION_REGEX)?.[0] ?? '0'
);
const latestVersion = items[0]?.version;
const latestMajorVersion = parseInt(
latestVersion?.match(VERSION_REGEX)?.[0] ?? '0'
);
return {
isMajorUpdate: latestMajorVersion > majorVersion,
hasUpdateToInstall: items.some(
(update) => update.installable && update.latest
),
};
}, [currentVersion, items]);
const noUpdateToInstall = hasUpdates && !hasUpdateToInstall;
const handleInstallLatestPress = useCallback(() => {
if (isMajorUpdate) {
setIsMajorUpdateModalOpen(true);
} else {
dispatch(executeCommand({ name: commandNames.APPLICATION_UPDATE }));
}
}, [isMajorUpdate, setIsMajorUpdateModalOpen, dispatch]);
const handleInstallLatestMajorVersionPress = useCallback(() => {
setIsMajorUpdateModalOpen(false);
dispatch(
executeCommand({
name: commandNames.APPLICATION_UPDATE,
installMajorUpdate: true,
})
);
}, [setIsMajorUpdateModalOpen, dispatch]);
const handleCancelMajorVersionPress = useCallback(() => {
setIsMajorUpdateModalOpen(false);
}, [setIsMajorUpdateModalOpen]);
useEffect(() => {
dispatch(fetchUpdates());
dispatch(fetchGeneralSettings());
}, [dispatch]);
return (
<PageContent title={translate('Updates')}>
<PageContentBody>
{isPopulated || hasError ? null : <LoadingIndicator />}
{noUpdates ? (
<Alert kind={kinds.INFO}>{translate('NoUpdatesAreAvailable')}</Alert>
) : null}
{hasUpdateToInstall ? (
<div className={styles.messageContainer}>
{updateMechanism === 'builtIn' || updateMechanism === 'script' ? (
<SpinnerButton
kind={kinds.PRIMARY}
isSpinning={isInstallingUpdate}
onPress={handleInstallLatestPress}
>
{translate('InstallLatest')}
</SpinnerButton>
) : (
<>
<Icon name={icons.WARNING} kind={kinds.WARNING} size={30} />
<div className={styles.message}>
{externalUpdaterPrefix}{' '}
<InlineMarkdown
data={
packageUpdateMechanismMessage ||
externalUpdaterMessages[updateMechanism] ||
externalUpdaterMessages.external
}
/>
</div>
</>
)}
{isFetching ? (
<LoadingIndicator className={styles.loading} size={20} />
) : null}
</div>
) : null}
{noUpdateToInstall && (
<div className={styles.messageContainer}>
<Icon
className={styles.upToDateIcon}
name={icons.CHECK_CIRCLE}
size={30}
/>
<div className={styles.message}>{translate('OnLatestVersion')}</div>
{isFetching && (
<LoadingIndicator className={styles.loading} size={20} />
)}
</div>
)}
{hasUpdates && (
<div>
{items.map((update) => {
return (
<div key={update.version} className={styles.update}>
<div className={styles.info}>
<div className={styles.version}>{update.version}</div>
<div className={styles.space}>&mdash;</div>
<div
className={styles.date}
title={formatDateTime(
update.releaseDate,
longDateFormat,
timeFormat
)}
>
{formatDate(update.releaseDate, shortDateFormat)}
</div>
{update.branch === 'master' ? null : (
<Label className={styles.label}>{update.branch}</Label>
)}
{update.version === currentVersion ? (
<Label
className={styles.label}
kind={kinds.SUCCESS}
title={formatDateTime(
update.installedOn,
longDateFormat,
timeFormat
)}
>
{translate('CurrentlyInstalled')}
</Label>
) : null}
{update.version !== currentVersion && update.installedOn ? (
<Label
className={styles.label}
kind={kinds.INVERSE}
title={formatDateTime(
update.installedOn,
longDateFormat,
timeFormat
)}
>
{translate('PreviouslyInstalled')}
</Label>
) : null}
</div>
{update.changes ? (
<div>
<UpdateChanges
title={translate('New')}
changes={update.changes.new}
/>
<UpdateChanges
title={translate('Fixed')}
changes={update.changes.fixed}
/>
</div>
) : (
<div>{translate('MaintenanceRelease')}</div>
)}
</div>
);
})}
</div>
)}
{updatesError ? (
<Alert kind={kinds.WARNING}>
{translate('FailedToFetchUpdates')}
</Alert>
) : null}
{generalSettingsError ? (
<Alert kind={kinds.DANGER}>
{translate('FailedToFetchSettings')}
</Alert>
) : null}
<ConfirmModal
isOpen={isMajorUpdateModalOpen}
kind={kinds.WARNING}
title={translate('InstallMajorVersionUpdate')}
message={
<div>
<div>{translate('InstallMajorVersionUpdateMessage')}</div>
<div>
<InlineMarkdown
data={translate('InstallMajorVersionUpdateMessageLink', {
domain: 'readarr.com',
url: 'https://readarr.com/#downloads',
})}
/>
</div>
</div>
}
confirmLabel={translate('Install')}
onConfirm={handleInstallLatestMajorVersionPress}
onCancel={handleCancelMajorVersionPress}
/>
</PageContentBody>
</PageContent>
);
}
export default Updates;

View File

@@ -131,13 +131,15 @@ class CutoffUnmetConnector extends Component {
onSearchSelectedPress = (selected) => {
this.props.executeCommand({
name: commandNames.BOOK_SEARCH,
bookIds: selected
bookIds: selected,
commandFinished: this.repopulate
});
};
onSearchAllCutoffUnmetPress = () => {
this.props.executeCommand({
name: commandNames.CUTOFF_UNMET_BOOK_SEARCH
name: commandNames.CUTOFF_UNMET_BOOK_SEARCH,
commandFinished: this.repopulate
});
};

View File

@@ -16,6 +16,7 @@ function CutoffUnmetRow(props) {
releaseDate,
titleSlug,
title,
lastSearchTime,
disambiguation,
isSelected,
columns,
@@ -68,6 +69,15 @@ function CutoffUnmetRow(props) {
);
}
if (name === 'books.lastSearchTime') {
return (
<RelativeDateCellConnector
key={name}
date={lastSearchTime}
/>
);
}
if (name === 'releaseDate') {
return (
<RelativeDateCellConnector
@@ -105,6 +115,7 @@ CutoffUnmetRow.propTypes = {
releaseDate: PropTypes.string.isRequired,
titleSlug: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
lastSearchTime: PropTypes.string,
disambiguation: PropTypes.string,
isSelected: PropTypes.bool,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,

View File

@@ -121,13 +121,15 @@ class MissingConnector extends Component {
onSearchSelectedPress = (selected) => {
this.props.executeCommand({
name: commandNames.BOOK_SEARCH,
bookIds: selected
bookIds: selected,
commandFinished: this.repopulate
});
};
onSearchAllMissingPress = () => {
this.props.executeCommand({
name: commandNames.MISSING_BOOK_SEARCH
name: commandNames.MISSING_BOOK_SEARCH,
commandFinished: this.repopulate
});
};

View File

@@ -16,6 +16,7 @@ function MissingRow(props) {
releaseDate,
titleSlug,
title,
lastSearchTime,
disambiguation,
isSelected,
columns,
@@ -77,6 +78,15 @@ function MissingRow(props) {
);
}
if (name === 'books.lastSearchTime') {
return (
<RelativeDateCellConnector
key={name}
date={lastSearchTime}
/>
);
}
if (name === 'actions') {
return (
<BookSearchCellConnector
@@ -104,6 +114,7 @@ MissingRow.propTypes = {
releaseDate: PropTypes.string.isRequired,
titleSlug: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
lastSearchTime: PropTypes.string,
disambiguation: PropTypes.string,
isSelected: PropTypes.bool,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,

View File

@@ -0,0 +1,45 @@
export type UpdateMechanism =
| 'builtIn'
| 'script'
| 'external'
| 'apt'
| 'docker';
export default interface General {
bindAddress: string;
port: number;
sslPort: number;
enableSsl: boolean;
launchBrowser: boolean;
authenticationMethod: string;
authenticationRequired: string;
analyticsEnabled: boolean;
username: string;
password: string;
passwordConfirmation: string;
logLevel: string;
consoleLogLevel: string;
branch: string;
apiKey: string;
sslCertPath: string;
sslCertPassword: string;
urlBase: string;
instanceName: string;
applicationUrl: string;
updateAutomatically: boolean;
updateMechanism: UpdateMechanism;
updateScriptPath: string;
proxyEnabled: boolean;
proxyType: string;
proxyHostname: string;
proxyPort: number;
proxyUsername: string;
proxyPassword: string;
proxyBypassFilter: string;
proxyBypassLocalAddresses: boolean;
certificateValidation: string;
backupFolder: string;
backupInterval: number;
backupRetention: number;
id: number;
}

View File

@@ -1,4 +1,5 @@
export interface UiSettings {
export default interface UiSettings {
theme: 'auto' | 'dark' | 'light';
showRelativeDates: boolean;
shortDateFormat: string;
longDateFormat: string;

View File

@@ -0,0 +1,32 @@
interface SystemStatus {
appData: string;
appName: string;
authentication: string;
branch: string;
buildTime: string;
instanceName: string;
isAdmin: boolean;
isDebug: boolean;
isDocker: boolean;
isLinux: boolean;
isNetCore: boolean;
isOsx: boolean;
isProduction: boolean;
isUserInteractive: boolean;
isWindows: boolean;
migrationVersion: number;
mode: string;
osName: string;
osVersion: string;
packageUpdateMechanism: string;
packageUpdateMechanismMessage: string;
runtimeName: string;
runtimeVersion: string;
sqliteVersion: string;
startTime: string;
startupPath: string;
urlBase: string;
version: string;
}
export default SystemStatus;

View File

@@ -0,0 +1,20 @@
export interface Changes {
new: string[];
fixed: string[];
}
interface Update {
version: string;
branch: string;
releaseDate: string;
fileName: string;
url: string;
installed: boolean;
installedOn: string;
installable: boolean;
latest: boolean;
changes: Changes | null;
hash: string;
}
export default Update;

View File

@@ -25,34 +25,33 @@
"defaults"
],
"dependencies": {
"@fortawesome/fontawesome-free": "6.4.0",
"@fortawesome/fontawesome-svg-core": "6.4.0",
"@fortawesome/free-regular-svg-icons": "6.4.0",
"@fortawesome/free-solid-svg-icons": "6.4.0",
"@fortawesome/react-fontawesome": "0.2.0",
"@fortawesome/fontawesome-free": "6.7.1",
"@fortawesome/fontawesome-svg-core": "6.7.1",
"@fortawesome/free-regular-svg-icons": "6.7.1",
"@fortawesome/free-solid-svg-icons": "6.7.1",
"@fortawesome/react-fontawesome": "0.2.2",
"@microsoft/signalr": "6.0.25",
"@sentry/browser": "7.51.2",
"@sentry/integrations": "7.51.2",
"@types/node": "18.19.31",
"@sentry/browser": "7.119.1",
"@sentry/integrations": "7.119.1",
"@types/node": "20.16.11",
"@types/react": "18.2.79",
"@types/react-dom": "18.2.25",
"ansi-colors": "4.1.3",
"classnames": "2.3.2",
"classnames": "2.5.1",
"clipboard": "2.0.11",
"connected-react-router": "6.9.3",
"element-class": "0.2.2",
"filesize": "10.0.7",
"filesize": "10.1.6",
"fuse.js": "6.6.2",
"history": "4.10.1",
"jdu": "1.0.0",
"jquery": "3.7.0",
"jquery": "3.7.1",
"lodash": "4.17.21",
"mobile-detect": "1.4.5",
"moment": "2.29.4",
"moment": "2.30.1",
"mousetrap": "1.6.5",
"normalize.css": "8.0.1",
"prop-types": "15.8.1",
"qs": "6.11.1",
"qs": "6.13.0",
"react": "17.0.2",
"react-addons-shallow-compare": "15.6.3",
"react-async-script": "1.2.0",
@@ -64,7 +63,7 @@
"react-dnd-touch-backend": "14.1.1",
"react-document-title": "2.0.3",
"react-dom": "17.0.2",
"react-focus-lock": "2.5.2",
"react-focus-lock": "2.9.4",
"react-google-recaptcha": "2.1.0",
"react-lazyload": "3.2.0",
"react-measure": "2.5.2",
@@ -73,80 +72,77 @@
"react-redux": "7.2.4",
"react-router": "5.2.0",
"react-router-dom": "5.2.0",
"react-slider": "1.3.1",
"react-tabs": "3.2.2",
"react-text-truncate": "0.18.0",
"react-slider": "1.3.3",
"react-tabs": "4.3.0",
"react-text-truncate": "0.19.0",
"react-virtualized": "9.21.1",
"redux": "4.1.0",
"redux": "4.2.1",
"redux-actions": "2.6.5",
"redux-batched-actions": "0.5.0",
"redux-localstorage": "0.4.1",
"redux-thunk": "2.3.0",
"redux-thunk": "2.4.2",
"reselect": "4.1.8",
"stacktrace-js": "2.0.2",
"typescript": "5.1.6"
},
"devDependencies": {
"@babel/core": "7.24.4",
"@babel/eslint-parser": "7.24.1",
"@babel/plugin-proposal-export-default-from": "7.24.1",
"@babel/core": "7.26.0",
"@babel/eslint-parser": "7.25.9",
"@babel/plugin-proposal-export-default-from": "7.25.9",
"@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/preset-env": "7.24.4",
"@babel/preset-react": "7.24.1",
"@babel/preset-typescript": "7.24.1",
"@babel/preset-env": "7.26.0",
"@babel/preset-react": "7.26.3",
"@babel/preset-typescript": "7.26.0",
"@types/lodash": "4.14.195",
"@types/react-lazyload": "3.2.0",
"@types/redux-actions": "2.6.2",
"@types/react-lazyload": "3.2.3",
"@types/redux-actions": "2.6.5",
"@typescript-eslint/eslint-plugin": "6.21.0",
"@typescript-eslint/parser": "6.21.0",
"autoprefixer": "10.4.14",
"babel-loader": "9.1.3",
"autoprefixer": "10.4.20",
"babel-loader": "9.2.1",
"babel-plugin-inline-classnames": "2.0.1",
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
"core-js": "3.37.0",
"core-js": "3.39.0",
"css-loader": "6.8.1",
"css-modules-typescript-loader": "4.0.1",
"eslint": "8.57.0",
"eslint": "8.57.1",
"eslint-config-prettier": "8.10.0",
"eslint-plugin-filenames": "1.3.2",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-json": "3.1.0",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-prettier": "4.2.1",
"eslint-plugin-react": "7.34.1",
"eslint-plugin-react-hooks": "4.6.0",
"eslint-plugin-simple-import-sort": "12.1.0",
"eslint-plugin-react": "7.37.1",
"eslint-plugin-react-hooks": "4.6.2",
"eslint-plugin-simple-import-sort": "12.1.1",
"file-loader": "6.2.0",
"filemanager-webpack-plugin": "8.0.0",
"fork-ts-checker-webpack-plugin": "8.0.0",
"html-webpack-plugin": "5.5.3",
"html-webpack-plugin": "5.6.0",
"loader-utils": "^3.2.1",
"mini-css-extract-plugin": "2.7.6",
"postcss": "8.4.38",
"mini-css-extract-plugin": "2.9.1",
"postcss": "8.4.47",
"postcss-color-function": "4.1.0",
"postcss-loader": "7.3.0",
"postcss-mixins": "9.0.4",
"postcss-nested": "6.0.1",
"postcss-nested": "6.2.0",
"postcss-simple-vars": "7.0.1",
"postcss-url": "10.1.3",
"prettier": "2.8.8",
"require-nocache": "1.0.0",
"rimraf": "4.4.1",
"run-sequence": "2.2.1",
"streamqueue": "1.1.2",
"style-loader": "3.3.3",
"rimraf": "6.0.1",
"style-loader": "3.3.4",
"stylelint": "15.10.3",
"stylelint-order": "6.0.3",
"terser-webpack-plugin": "5.3.9",
"ts-loader": "9.4.4",
"stylelint-order": "6.0.4",
"terser-webpack-plugin": "5.3.10",
"ts-loader": "9.5.1",
"typescript-plugin-css-modules": "5.0.1",
"url-loader": "4.1.1",
"webpack": "5.88.2",
"webpack": "5.95.0",
"webpack-cli": "5.1.4",
"webpack-livereload-plugin": "3.0.2",
"worker-loader": "3.0.8"
},
"volta": {
"node": "16.17.0",
"node": "20.11.1",
"yarn": "1.22.19"
}
}

View File

@@ -99,6 +99,35 @@
<RootNamespace Condition="'$(ReadarrProject)'=='true'">$(MSBuildProjectName.Replace('Readarr','NzbDrone'))</RootNamespace>
</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 -->
<ItemGroup Condition="'$(TestProject)'=='true'">
<PackageReference Include="Microsoft.NET.Test.Sdk" />

View File

@@ -4,26 +4,30 @@
<PackageVersion Include="AutoFixture" Version="4.17.0" />
<PackageVersion Include="coverlet.collector" Version="3.0.4-preview.27.ge7cb7c3b40" PrivateAssets="all" />
<PackageVersion Include="Dapper" Version="2.0.151" />
<PackageVersion Include="Diacritical.Net" Version="1.0.4" />
<PackageVersion Include="DryIoc.dll" Version="5.4.3" />
<PackageVersion Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0" />
<PackageVersion Include="Equ" Version="2.3.0" />
<PackageVersion Include="FluentAssertions" Version="5.10.3" />
<PackageVersion Include="Polly" Version="8.4.1" />
<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.SQLite" Version="3.3.2.9" />
<PackageVersion Include="Servarr.FluentMigrator.Runner.Postgres" Version="3.3.2.9" />
<PackageVersion Include="FluentValidation" Version="9.5.4" />
<PackageVersion Include="Ical.Net" Version="4.2.0" />
<PackageVersion Include="Ical.Net" Version="4.3.1" />
<PackageVersion Include="ImpromptuInterface" Version="7.0.1" />
<PackageVersion Include="LazyCache" Version="2.4.0" />
<PackageVersion Include="Mailkit" Version="3.6.0" />
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.32" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
<PackageVersion Include="Mailkit" Version="4.8.0" />
<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.Configuration" 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.Logging" Version="6.0.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="Mono.Posix.NETStandard" Version="5.20.1.34-servarr22" />
<PackageVersion Include="Moq" Version="4.17.2" />
@@ -33,36 +37,36 @@
<PackageVersion Include="NLog.Extensions.Logging" Version="5.2.3" />
<PackageVersion Include="NLog" Version="5.1.4" />
<PackageVersion Include="NLog.Targets.Syslog" Version="7.0.0" />
<PackageVersion Include="Npgsql" Version="7.0.7" />
<PackageVersion Include="Npgsql" Version="7.0.10" />
<PackageVersion Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageVersion Include="NUnit" Version="3.14.0" />
<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" Version="106.15.0" />
<PackageVersion Include="Selenium.Support" Version="3.141.0" />
<PackageVersion Include="Selenium.WebDriver.ChromeDriver" Version="91.0.4472.10100" />
<PackageVersion Include="Sentry" Version="3.31.0" />
<PackageVersion Include="Sentry" Version="4.0.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="Swashbuckle.AspNetCore.Annotations" 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.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" Version="17.0.24" />
<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.Resources.Extensions" Version="6.0.0" />
<PackageVersion Include="System.Runtime.Loader" Version="4.3.0" />
<PackageVersion Include="System.Security.Principal.Windows" Version="5.0.0" />
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="6.0.1" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="6.0.0" />
<PackageVersion Include="System.Text.Json" Version="6.0.9" />
<PackageVersion Include="System.ValueTuple" Version="4.5.0" />
<PackageVersion Include="System.Text.Json" Version="6.0.10" />
<PackageVersion Include="System.ValueTuple" Version="4.6.1" />
<PackageVersion Include="TagLibSharp-Lidarr" Version="2.2.0.19" />
</ItemGroup>
</Project>

View File

@@ -21,9 +21,28 @@ namespace NzbDrone.Common.Test.ExtensionTests
[TestCase("1.2.3.4")]
[TestCase("172.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)
{
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();
}
}
}

View File

@@ -89,6 +89,10 @@ namespace NzbDrone.Common.Test.InstrumentationTests
[TestCase(@"https://discord.com/api/webhooks/mySecret")]
[TestCase(@"https://discord.com/api/webhooks/mySecret/01233210")]
// Telegram
[TestCase(@"https://api.telegram.org/bot1234567890:mySecret/sendmessage: chat_id=123456&parse_mode=HTML&text=<text>")]
[TestCase(@"https://api.telegram.org/bot1234567890:mySecret/")]
public void should_clean_message(string message)
{
var cleansedMessage = CleanseLogMessage.Cleanse(message);

View File

@@ -4,6 +4,7 @@ using System.Linq;
using FluentAssertions;
using NLog;
using NUnit.Framework;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Instrumentation.Sentry;
using NzbDrone.Test.Common;
@@ -27,7 +28,7 @@ namespace NzbDrone.Common.Test.InstrumentationTests
[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)

View File

@@ -42,17 +42,18 @@ namespace NzbDrone.Common
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();
foreach (var file in files)
{
zipFile.Add(file, Path.GetFileName(file));
}
zipFile.CommitUpdate();
zipFile.Add(file, Path.GetFileName(file));
}
zipFile.CommitUpdate();
}
private void ExtractZip(string compressedFile, string destination)

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.IO.Abstractions;
using System.Linq;
using System.Threading;
using NLog;
using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.EnvironmentInfo;
@@ -306,9 +307,26 @@ namespace NzbDrone.Common.Disk
{
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);
}

View File

@@ -342,10 +342,11 @@ namespace NzbDrone.Common.Disk
var isCifs = targetDriveFormat == "cifs";
var isBtrfs = sourceDriveFormat == "btrfs" && targetDriveFormat == "btrfs";
var isZfs = sourceDriveFormat == "zfs" && targetDriveFormat == "zfs";
if (mode.HasFlag(TransferMode.Copy))
{
if (isBtrfs)
if (isBtrfs || isZfs)
{
if (_diskProvider.TryCreateRefLink(sourcePath, targetPath))
{
@@ -359,7 +360,7 @@ namespace NzbDrone.Common.Disk
if (mode.HasFlag(TransferMode.Move))
{
if (isBtrfs)
if (isBtrfs || isZfs)
{
if (isSameMount && _diskProvider.TryRenameFile(sourcePath, targetPath))
{

View File

@@ -39,18 +39,24 @@ namespace NzbDrone.Common.Extensions
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)
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)
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)
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)
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;
}
}
}

View File

@@ -1,3 +1,4 @@
using System;
using NzbDrone.Common.Extensions;
namespace NzbDrone.Common.Http.Proxy
@@ -29,7 +30,8 @@ namespace NzbDrone.Common.Http.Proxy
{
if (!string.IsNullOrWhiteSpace(BypassFilter))
{
var hostlist = BypassFilter.Split(',');
var hostlist = BypassFilter.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
for (var i = 0; i < hostlist.Length; i++)
{
if (hostlist[i].StartsWith("*"))
@@ -41,7 +43,7 @@ namespace NzbDrone.Common.Http.Proxy
return hostlist;
}
return new string[] { };
return Array.Empty<string>();
}
}

View File

@@ -54,7 +54,10 @@ namespace NzbDrone.Common.Instrumentation
new (@"api/v[0-9]/notification/readarr/(?<secret>[\w-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Discord
new (@"discord.com/api/webhooks/((?<secret>[\w-]+)/)?(?<secret>[\w-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase)
new (@"discord.com/api/webhooks/((?<secret>[\w-]+)/)?(?<secret>[\w-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Telegram
new (@"api.telegram.org/bot(?<id>[\d]+):(?<secret>[\w-]+)/", RegexOptions.Compiled | RegexOptions.IgnoreCase)
};
private static readonly Regex CleanseRemoteIPRegex = new (@"(?:Auth-\w+(?<!Failure|Unauthorized) ip|from) (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})", RegexOptions.Compiled);

View File

@@ -41,7 +41,7 @@ namespace NzbDrone.Common.Instrumentation
RegisterDebugger();
}
RegisterSentry(updateApp);
RegisterSentry(updateApp, appFolderInfo);
if (updateApp)
{
@@ -62,7 +62,7 @@ namespace NzbDrone.Common.Instrumentation
LogManager.ReconfigExistingLoggers();
}
private static void RegisterSentry(bool updateClient)
private static void RegisterSentry(bool updateClient, IAppFolderInfo appFolderInfo)
{
string dsn;
@@ -77,7 +77,7 @@ namespace NzbDrone.Common.Instrumentation
: "https://31e00a6c63ea42c8b5fe70358526a30d@sentry.servarr.com/4";
}
var target = new SentryTarget(dsn)
var target = new SentryTarget(dsn, appFolderInfo)
{
Name = "sentryTarget",
Layout = "${message}"

View File

@@ -9,6 +9,7 @@ using NLog;
using NLog.Common;
using NLog.Targets;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using Sentry;
namespace NzbDrone.Common.Instrumentation.Sentry
@@ -99,7 +100,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
public bool FilterEvents { get; set; }
public bool SentryEnabled { get; set; }
public SentryTarget(string dsn)
public SentryTarget(string dsn, IAppFolderInfo appFolderInfo)
{
_sdk = SentrySdk.Init(o =>
{
@@ -107,9 +108,33 @@ namespace NzbDrone.Common.Instrumentation.Sentry
o.AttachStacktrace = true;
o.MaxBreadcrumbs = 200;
o.Release = $"{BuildInfo.AppName}@{BuildInfo.Release}";
o.BeforeSend = x => SentryCleanser.CleanseEvent(x);
o.BeforeBreadcrumb = x => SentryCleanser.CleanseBreadcrumb(x);
o.SetBeforeSend(x => SentryCleanser.CleanseEvent(x));
o.SetBeforeBreadcrumb(x => SentryCleanser.CleanseBreadcrumb(x));
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();
@@ -127,7 +152,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
{
SentrySdk.ConfigureScope(scope =>
{
scope.User = new User
scope.User = new SentryUser
{
Id = HashUtil.AnonymousToken()
};
@@ -169,9 +194,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
private void OnError(Exception ex)
{
var webException = ex as WebException;
if (webException != null)
if (ex is WebException webException)
{
var response = webException.Response as HttpWebResponse;
var statusCode = response?.StatusCode;
@@ -290,13 +313,21 @@ namespace NzbDrone.Common.Instrumentation.Sentry
}
}
var level = LoggingLevelMap[logEvent.Level];
var sentryEvent = new SentryEvent(logEvent.Exception)
{
Level = LoggingLevelMap[logEvent.Level],
Level = level,
Logger = logEvent.LoggerName,
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.SetFingerprint(fingerPrint);

View File

@@ -6,4 +6,5 @@ public class AuthOptions
public bool? Enabled { get; set; }
public string Method { get; set; }
public string Required { get; set; }
public bool? TrustCgnatIpAddresses { get; set; }
}

View File

@@ -313,7 +313,7 @@ namespace NzbDrone.Common.Processes
processInfo = new ProcessInfo();
processInfo.Id = process.Id;
processInfo.Name = process.ProcessName;
processInfo.StartPath = process.MainModule.FileName;
processInfo.StartPath = process.MainModule?.FileName;
if (process.Id != GetCurrentProcessId() && process.HasExited)
{

View File

@@ -6,6 +6,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DryIoc.dll" />
<PackageReference Include="IPAddressRange" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" />
<PackageReference Include="NLog.Extensions.Logging" />

View File

@@ -200,17 +200,9 @@ namespace NzbDrone.Core.Test.Download
var seriesTags = new HashSet<int> { 2 };
var clientTags = new HashSet<int> { 1 };
WithTorrentClient(0, clientTags);
WithTorrentClient(0, clientTags);
WithTorrentClient(0, clientTags);
WithTorrentClient(0, clientTags);
var client1 = 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();
Assert.Throws<DownloadClientUnavailableException>(() => Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags));
}
[Test]

View File

@@ -312,11 +312,12 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DelugeTests
[Test]
public void should_return_status_with_outputdirs()
{
var configItems = new Dictionary<string, object>();
configItems.Add("download_location", @"C:\Downloads\Downloading\deluge".AsOsAgnostic());
configItems.Add("move_completed_path", @"C:\Downloads\Finished\deluge".AsOsAgnostic());
configItems.Add("move_completed", true);
var configItems = new Dictionary<string, object>
{
{ "download_location", @"C:\Downloads\Downloading\deluge".AsOsAgnostic() },
{ "move_completed_path", @"C:\Downloads\Finished\deluge".AsOsAgnostic() },
{ "move_completed", true }
};
Mocker.GetMock<IDelugeProxy>()
.Setup(v => v.GetConfig(It.IsAny<DelugeSettings>()))
@@ -328,5 +329,18 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DelugeTests
result.OutputRootFolders.Should().NotBeNull();
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());
}
}
}

View File

@@ -178,8 +178,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
VerifyWarning(item);
}
[Test]
public void paused_item_should_have_required_properties()
[TestCase("pausedDL")]
[TestCase("stoppedDL")]
public void paused_item_should_have_required_properties(string state)
{
var torrent = new QBittorrentTorrent
{
@@ -188,7 +189,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
Size = 1000,
Progress = 0.7,
Eta = 8640000,
State = "pausedDL",
State = state,
Label = "",
SavePath = ""
};
@@ -200,6 +201,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
}
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
[TestCase("queuedUP")]
[TestCase("uploading")]
[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"));
}
[Test]
public void api_261_should_use_content_path()
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void api_261_should_use_content_path(string state)
{
var torrent = new QBittorrentTorrent
{
@@ -407,7 +410,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
Size = 1000,
Progress = 0.7,
Eta = 8640000,
State = "pausedUP",
State = state,
Label = "",
SavePath = @"C:\Torrents".AsOsAgnostic(),
ContentPath = @"C:\Torrents\Droned.S01.12".AsOsAgnostic()
@@ -684,44 +687,96 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
item.CanMoveFiles.Should().BeFalse();
}
[Test]
public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_is_not_set()
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_is_not_set(string state)
{
GivenGlobalSeedLimits(-1);
GivenCompletedTorrent("pausedUP", ratio: 1.0f);
GivenCompletedTorrent(state, ratio: 1.0f);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeFalse();
item.CanMoveFiles.Should().BeFalse();
}
[Test]
public void should_be_removable_and_should_allow_move_files_if_max_ratio_reached_and_paused()
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_max_ratio_reached_and_paused(string state)
{
GivenGlobalSeedLimits(1.0f);
GivenCompletedTorrent("pausedUP", ratio: 1.0f);
GivenCompletedTorrent(state, ratio: 1.0f);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[Test]
public void should_be_removable_and_should_allow_move_files_if_overridden_max_ratio_reached_and_paused()
[TestCase("pausedUP")]
[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);
GivenCompletedTorrent("pausedUP", ratio: 1.0f, ratioLimit: 0.8f);
GivenCompletedTorrent(state, ratio: 1.0f, ratioLimit: 0.8f);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[Test]
public void should_not_be_removable_if_overridden_max_ratio_not_reached_and_paused()
[TestCase("pausedUP")]
[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);
GivenCompletedTorrent("pausedUP", ratio: 0.5f, ratioLimit: 0.8f);
GivenCompletedTorrent(state, ratio: 0.5f, ratioLimit: 0.8f);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeFalse();
@@ -739,33 +794,36 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
item.CanMoveFiles.Should().BeFalse();
}
[Test]
public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_and_paused()
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_and_paused(string state)
{
GivenGlobalSeedLimits(-1, 20);
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 20);
GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 20);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[Test]
public void should_be_removable_and_should_allow_move_files_if_overridden_max_seedingtime_reached_and_paused()
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_overridden_max_seedingtime_reached_and_paused(string state)
{
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();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[Test]
public void should_not_be_removable_if_overridden_max_seedingtime_not_reached_and_paused()
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_not_be_removable_if_overridden_max_seedingtime_not_reached_and_paused(string state)
{
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();
item.CanBeRemoved.Should().BeFalse();
@@ -783,66 +841,72 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
item.CanMoveFiles.Should().BeFalse();
}
[Test]
public void should_be_removable_and_should_allow_move_files_if_max_inactive_seedingtime_reached_and_paused()
[TestCase("pausedUP")]
[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);
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();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[Test]
public void should_be_removable_and_should_allow_move_files_if_overridden_max_inactive_seedingtime_reached_and_paused()
[TestCase("pausedUP")]
[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);
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();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[Test]
public void should_not_be_removable_if_overridden_max_inactive_seedingtime_not_reached_and_paused()
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_not_be_removable_if_overridden_max_inactive_seedingtime_not_reached_and_paused(string state)
{
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();
item.CanBeRemoved.Should().BeFalse();
item.CanMoveFiles.Should().BeFalse();
}
[Test]
public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_but_ratio_not_and_paused()
[TestCase("pausedUP")]
[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);
GivenCompletedTorrent("pausedUP", ratio: 1.0f, seedingTime: 30);
GivenCompletedTorrent(state, ratio: 1.0f, seedingTime: 30);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[Test]
public void should_be_removable_and_should_allow_move_files_if_max_inactive_seedingtime_reached_but_ratio_not_and_paused()
[TestCase("pausedUP")]
[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);
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();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[Test]
public void should_not_fetch_details_twice()
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_not_fetch_details_twice(string state)
{
GivenGlobalSeedLimits(-1, 30);
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 20);
GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 20);
var item = Subject.GetItems().Single();
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());
}
[Test]
public void should_get_category_from_the_category_if_set()
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_get_category_from_the_category_if_set(string state)
{
const string category = "music-readarr";
GivenGlobalSeedLimits(1.0f);
@@ -867,7 +932,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
Size = 1000,
Progress = 1.0,
Eta = 8640000,
State = "pausedUP",
State = state,
Category = category,
SavePath = "",
Ratio = 1.0f
@@ -879,8 +944,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
item.Category.Should().Be(category);
}
[Test]
public void should_get_category_from_the_label_if_the_category_is_not_available()
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_get_category_from_the_label_if_the_category_is_not_available(string state)
{
const string category = "music-readarr";
GivenGlobalSeedLimits(1.0f);
@@ -892,7 +958,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
Size = 1000,
Progress = 1.0,
Eta = 8640000,
State = "pausedUP",
State = state,
Label = category,
SavePath = "",
Ratio = 1.0f

View File

@@ -478,6 +478,37 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests
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", @"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")]

View File

@@ -49,10 +49,13 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests
}
[Test]
public void magnet_download_should_not_return_the_item()
public void magnet_download_should_be_returned_as_queued()
{
PrepareClientToReturnMagnetItem();
Subject.GetItems().Count().Should().Be(0);
var item = Subject.GetItems().Single();
item.Status.Should().Be(DownloadItemStatus.Queued);
}
[Test]

View File

@@ -60,7 +60,10 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.VuzeTests
public void magnet_download_should_not_return_the_item()
{
PrepareClientToReturnMagnetItem();
Subject.GetItems().Count().Should().Be(0);
var item = Subject.GetItems().Single();
item.Status.Should().Be(DownloadItemStatus.Queued);
}
[Test]

View File

@@ -7,6 +7,7 @@ using NzbDrone.Core.HealthCheck.Checks;
using NzbDrone.Core.Localization;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Update;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.HealthCheck.Checks
{
@@ -21,28 +22,10 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
.Returns("Some Warning Message");
}
[Test]
public void should_return_error_when_app_folder_is_write_protected()
{
WindowsOnly();
Mocker.GetMock<IAppFolderInfo>()
.Setup(s => s.StartUpFolder)
.Returns(@"C:\NzbDrone");
Mocker.GetMock<IDiskProvider>()
.Setup(c => c.FolderWritable(It.IsAny<string>()))
.Returns(false);
Subject.Check().ShouldBeError();
}
[Test]
public void should_return_error_when_app_folder_is_write_protected_and_update_automatically_is_enabled()
{
PosixOnly();
const string startupFolder = @"/opt/nzbdrone";
var startupFolder = @"C:\NzbDrone".AsOsAgnostic();
Mocker.GetMock<IConfigFileProvider>()
.Setup(s => s.UpdateAutomatically)
@@ -62,10 +45,8 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
[Test]
public void should_return_error_when_ui_folder_is_write_protected_and_update_automatically_is_enabled()
{
PosixOnly();
const string startupFolder = @"/opt/nzbdrone";
const string uiFolder = @"/opt/nzbdrone/UI";
var startupFolder = @"C:\NzbDrone".AsOsAgnostic();
var uiFolder = @"C:\NzbDrone\UI".AsOsAgnostic();
Mocker.GetMock<IConfigFileProvider>()
.Setup(s => s.UpdateAutomatically)
@@ -89,7 +70,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
[Test]
public void should_not_return_error_when_app_folder_is_write_protected_and_external_script_enabled()
{
PosixOnly();
var startupFolder = @"C:\NzbDrone".AsOsAgnostic();
Mocker.GetMock<IConfigFileProvider>()
.Setup(s => s.UpdateAutomatically)
@@ -101,7 +82,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
Mocker.GetMock<IAppFolderInfo>()
.Setup(s => s.StartUpFolder)
.Returns(@"/opt/nzbdrone");
.Returns(startupFolder);
Mocker.GetMock<IDiskProvider>()
.Verify(c => c.FolderWritable(It.IsAny<string>()), Times.Never());

View File

@@ -12,7 +12,7 @@ namespace NzbDrone.Core.Test.Http
{
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]
@@ -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://google.com/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]
@@ -31,6 +32,7 @@ namespace NzbDrone.Core.Test.Http
var settings = GetProxySettings();
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();
}
}
}

View File

@@ -13,7 +13,7 @@ using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.MetadataSource.Goodreads
{
[TestFixture]
[Ignore("Waiting for metadata to be back again", Until = "2024-08-15 00:00:00Z")]
[Ignore("Waiting for metadata to be back again", Until = "2025-05-15 00:00:00Z")]
public class BookInfoProxyFixture : CoreTest<BookInfoProxy>
{
private MetadataProfile _metadataProfile;

View File

@@ -15,7 +15,7 @@ using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.MetadataSource.Goodreads
{
[TestFixture]
[Ignore("Waiting for metadata to be back again", Until = "2024-08-15 00:00:00Z")]
[Ignore("Waiting for metadata to be back again", Until = "2025-05-15 00:00:00Z")]
public class BookInfoProxySearchFixture : CoreTest<BookInfoProxy>
{
[SetUp]

View File

@@ -40,7 +40,7 @@ namespace NzbDrone.Core.Test.MetadataSource.Goodreads
[TestCase("Harry Potter and the sorcerer's stone a detailed summary", 72245296)]
[TestCase("B0192CTMYG", 61209488)]
[TestCase("9780439554930", 48517161)]
[TestCase("9780439554930", 3)]
public void successful_book_search(string title, int expected)
{
var result = Subject.Search(title);

View File

@@ -21,14 +21,14 @@ namespace NzbDrone.Core.Test.UpdateTests
public void no_update_when_version_higher()
{
UseRealHttp();
Subject.GetLatestUpdate("nightly", new Version(10, 0)).Should().BeNull();
Subject.GetLatestUpdate("develop", new Version(10, 0)).Should().BeNull();
}
[Test]
public void finds_update_when_version_lower()
{
UseRealHttp();
Subject.GetLatestUpdate("nightly", new Version(0, 1)).Should().NotBeNull();
Subject.GetLatestUpdate("develop", new Version(0, 1)).Should().NotBeNull();
}
[Test]
@@ -42,10 +42,9 @@ namespace NzbDrone.Core.Test.UpdateTests
[Test]
public void should_get_recent_updates()
{
const string branch = "nightly";
const string branch = "develop";
UseRealHttp();
var recent = Subject.GetRecentUpdates(branch, new Version(0, 1), null);
var recentWithChanges = recent.Where(c => c.Changes != null);
recent.Should().NotBeEmpty();
recent.Should().OnlyContain(c => c.Hash.IsNotNullOrWhiteSpace());

View File

@@ -66,12 +66,19 @@ namespace NzbDrone.Core.Backup
{
_logger.ProgressInfo("Starting Backup");
var backupFolder = GetBackupFolder(backupType);
_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 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();

View File

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

View File

@@ -53,6 +53,7 @@ namespace NzbDrone.Core.Configuration
string SyslogServer { get; }
int SyslogPort { get; }
string SyslogLevel { get; }
string Theme { get; }
string PostgresHost { get; }
int PostgresPort { get; }
string PostgresUser { get; }
@@ -60,7 +61,7 @@ namespace NzbDrone.Core.Configuration
string PostgresMainDb { get; }
string PostgresLogDb { get; }
string PostgresCacheDb { get; }
string Theme { get; }
bool TrustCgnatIpAddresses { get; }
}
public class ConfigFileProvider : IConfigFileProvider
@@ -219,7 +220,7 @@ namespace NzbDrone.Core.Configuration
// TODO: Change back to "master" for the first stable release
public string Branch => _updateOptions.Branch ?? GetValue("Branch", "develop").ToLowerInvariant();
public string LogLevel => _logOptions.Level ?? GetValue("LogLevel", "info").ToLowerInvariant();
public string LogLevel => _logOptions.Level ?? GetValue("LogLevel", "debug").ToLowerInvariant();
public string ConsoleLogLevel => _logOptions.ConsoleLevel ?? GetValue("ConsoleLogLevel", string.Empty, persist: false);
public string PostgresHost => _postgresOptions?.Host ?? GetValue("PostgresHost", string.Empty, persist: false);
@@ -253,9 +254,23 @@ namespace NzbDrone.Core.Configuration
}
public string UiFolder => BuildInfo.IsDebug ? Path.Combine("..", "UI") : "UI";
public string InstanceName => _appOptions.InstanceName ?? GetValue("InstanceName", BuildInfo.AppName);
public bool UpdateAutomatically => _updateOptions.Automatically ?? GetValueBoolean("UpdateAutomatically", false, false);
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 UpdateMechanism UpdateMechanism =>
Enum.TryParse<UpdateMechanism>(_updateOptions.Mechanism, out var enumValue)
@@ -357,7 +372,7 @@ namespace NzbDrone.Core.Configuration
}
// If SSL is enabled and a cert hash is still in the config file or cert path is empty disable SSL
if (EnableSsl && (GetValue("SslCertHash", null).IsNotNullOrWhiteSpace() || SslCertPath.IsNullOrWhiteSpace()))
if (EnableSsl && (GetValue("SslCertHash", string.Empty, false).IsNotNullOrWhiteSpace() || SslCertPath.IsNullOrWhiteSpace()))
{
SetValue("EnableSsl", false);
}
@@ -462,5 +477,7 @@ namespace NzbDrone.Core.Configuration
{
SetValue("ApiKey", GenerateApiKey());
}
public bool TrustCgnatIpAddresses => _authOptions.TrustCgnatIpAddresses ?? GetValueBoolean("TrustCgnatIpAddresses", false, persist: false);
}
}

View File

@@ -404,6 +404,12 @@ namespace NzbDrone.Core.Configuration
public string ApplicationUrl => GetValue("ApplicationUrl", string.Empty);
public bool TrustCgnatIpAddresses
{
get { return GetValueBoolean("TrustCgnatIpAddresses", false); }
set { SetValue("TrustCgnatIpAddresses", value); }
}
private string GetValue(string key)
{
return GetValue(key, string.Empty);

View File

@@ -213,9 +213,18 @@ namespace NzbDrone.Core.Download.Clients.Deluge
{
var config = _proxy.GetConfig(Settings);
var label = _proxy.GetLabelOptions(Settings);
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
destDir = new OsPath(label.MoveCompletedPath);
@@ -231,7 +240,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
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)

View File

@@ -239,7 +239,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
// 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).
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)
{
@@ -248,7 +248,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
item.Message = "qBittorrent is reporting an error";
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;
break;
@@ -259,7 +260,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
item.Status = DownloadItemStatus.Queued;
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 "stalledUP": // torrent is being seeded, but no connection were made
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.Ratio >= torrent.RatioLimit)
if (torrent.RatioLimit - torrent.Ratio <= 0.001f)
{
return true;
}
}
else if (torrent.RatioLimit == -2 && config.MaxRatioEnabled)
{
if (Math.Round(torrent.Ratio, 2) >= config.MaxRatio)
if (config.MaxRatio - torrent.Ratio <= 0.001f)
{
return true;
}

View File

@@ -26,8 +26,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
Dictionary<string, QBittorrentLabel> GetLabels(QBittorrentSettings settings);
void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, 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);
}

View File

@@ -148,7 +148,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
{
request.AddFormParameter("paused", false);
}
else if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
else if ((QBittorrentState)settings.InitialState == QBittorrentState.Stop)
{
request.AddFormParameter("paused", true);
}
@@ -178,7 +178,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
{
request.AddFormParameter("paused", false);
}
else if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
else if ((QBittorrentState)settings.InitialState == QBittorrentState.Stop)
{
request.AddFormParameter("paused", true);
}
@@ -214,7 +214,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
catch (DownloadClientException ex)
{
// 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")
.Post()
@@ -257,7 +257,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
catch (DownloadClientException ex)
{
// 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;
}
@@ -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)
{
var request = BuildRequest(settings).Resource("/command/setForceStart")

View File

@@ -246,14 +246,20 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
request.AddFormParameter("category", settings.MusicCategory);
}
// Note: ForceStart is handled by separate api call
if ((QBittorrentState)settings.InitialState == QBittorrentState.Start)
// Avoid extraneous API version check if initial state is ForceStart
if ((QBittorrentState)settings.InitialState is QBittorrentState.Start or QBittorrentState.Stop)
{
request.AddFormParameter("paused", false);
}
else if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
{
request.AddFormParameter("paused", true);
var stoppedParameterName = GetApiVersion(settings) >= new Version(2, 11, 0) ? "stopped" : "paused";
// Note: ForceStart is handled by separate api call
if ((QBittorrentState)settings.InitialState == QBittorrentState.Start)
{
request.AddFormParameter(stoppedParameterName, false);
}
else if ((QBittorrentState)settings.InitialState == QBittorrentState.Stop)
{
request.AddFormParameter(stoppedParameterName, true);
}
}
if (settings.SequentialOrder)
@@ -291,7 +297,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
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
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;
}
@@ -313,7 +319,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
catch (DownloadClientException ex)
{
// 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;
}
@@ -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)
{
var request = BuildRequest(settings).Resource("/api/v2/torrents/setForceStart")

View File

@@ -1,9 +1,16 @@
using NzbDrone.Core.Annotations;
namespace NzbDrone.Core.Download.Clients.QBittorrent
{
public enum QBittorrentState
{
[FieldOption(Label = "Started")]
Start = 0,
[FieldOption(Label = "Force Started")]
ForceStart = 1,
Pause = 2
[FieldOption(Label = "Stopped")]
Stop = 2
}
}

View File

@@ -263,20 +263,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
status.OutputRootFolders = new List<OsPath> { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, category.FullPath) };
}
if (config.Misc.history_retention.IsNullOrWhiteSpace())
{
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";
}
status.RemovesCompletedDownloads = RemovesCompletedDownloads(config);
return status;
}
@@ -518,6 +505,43 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
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)
{
var downloadItemOutputPath = downloadClientItem.OutputPath;

View File

@@ -30,6 +30,8 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
public bool enable_date_sorting { get; set; }
public bool pre_check { 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

View File

@@ -41,12 +41,6 @@ namespace NzbDrone.Core.Download.Clients.Transmission
foreach (var torrent in torrents)
{
// If totalsize == 0 the torrent is a magnet downloading metadata
if (torrent.TotalSize == 0)
{
continue;
}
var outputPath = new OsPath(torrent.DownloadDir);
if (Settings.TvDirectory.IsNotNullOrWhiteSpace())
@@ -97,6 +91,10 @@ namespace NzbDrone.Core.Download.Clients.Transmission
item.Status = DownloadItemStatus.Warning;
item.Message = torrent.ErrorString;
}
else if (torrent.TotalSize == 0)
{
item.Status = DownloadItemStatus.Queued;
}
else if (torrent.LeftUntilDone == 0 && (torrent.Status == TransmissionTorrentStatus.Stopped ||
torrent.Status == TransmissionTorrentStatus.Seeding ||
torrent.Status == TransmissionTorrentStatus.SeedingWait))

View File

@@ -4,6 +4,7 @@ using System.Net;
using Newtonsoft.Json.Linq;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer;
@@ -208,7 +209,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
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);
@@ -220,24 +221,26 @@ namespace NzbDrone.Core.Download.Clients.Transmission
authLoginRequest.SuppressHttpError = true;
var response = _httpClient.Execute(authLoginRequest);
if (response.StatusCode == HttpStatusCode.MovedPermanently)
{
var url = response.Headers.GetSingleValue("Location");
throw new DownloadClientException("Remote site redirected to " + url);
}
else if (response.StatusCode == HttpStatusCode.Conflict)
switch (response.StatusCode)
{
sessionId = response.Headers.GetSingleValue("X-Transmission-Session-Id");
case HttpStatusCode.MovedPermanently:
var url = response.Headers.GetSingleValue("Location");
if (sessionId == null)
{
throw new DownloadClientException("Remote host did not return a Session Id.");
}
}
else
{
throw new DownloadClientAuthenticationException("Failed to authenticate with Transmission.");
throw new DownloadClientException("Remote site redirected to " + url);
case HttpStatusCode.Forbidden:
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");
if (sessionId == null)
{
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.");

View File

@@ -41,18 +41,23 @@ namespace NzbDrone.Core.Download
var blockedProviders = new HashSet<int>(_downloadClientStatusService.GetBlockedProviders().Select(v => v.ProviderId));
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();
availableProviders = matchingTagsClients.Count > 0 ?
matchingTagsClients :
availableProviders.Where(i => i.Definition.Tags.Empty()).ToList();
}
if (!availableProviders.Any())
{
return null;
if (!availableProviders.Any())
{
throw new DownloadClientUnavailableException("No download client was found without tags or a matching author tag. Please check your settings.");
}
}
if (indexerId > 0)

View File

@@ -4,24 +4,24 @@ namespace NzbDrone.Core.Exceptions
{
public class AuthorNotFoundException : NzbDroneException
{
public string MusicBrainzId { get; set; }
public string ForeignAuthorId { get; set; }
public AuthorNotFoundException(string musicbrainzId)
: base(string.Format("Author with id {0} was not found, it may have been removed from the metadata server.", musicbrainzId))
public AuthorNotFoundException(string foreignAuthorId)
: 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)
{
MusicBrainzId = musicbrainzId;
ForeignAuthorId = foreignAuthorId;
}
public AuthorNotFoundException(string musicbrainzId, string message)
public AuthorNotFoundException(string foreignAuthorId, string message)
: base(message)
{
MusicBrainzId = musicbrainzId;
ForeignAuthorId = foreignAuthorId;
}
}
}

View File

@@ -4,24 +4,24 @@ namespace NzbDrone.Core.Exceptions
{
public class BookNotFoundException : NzbDroneException
{
public string MusicBrainzId { get; set; }
public string ForeignBookId { get; set; }
public BookNotFoundException(string musicbrainzId)
: base(string.Format("Book with id {0} was not found, it may have been removed from metadata server.", musicbrainzId))
public BookNotFoundException(string foreignBookId)
: base($"Book with id {foreignBookId} was not found, it may have been removed from metadata server.")
{
MusicBrainzId = musicbrainzId;
ForeignBookId = foreignBookId;
}
public BookNotFoundException(string musicbrainzId, string message, params object[] args)
public BookNotFoundException(string foreignBookId, string message, params object[] args)
: base(message, args)
{
MusicBrainzId = musicbrainzId;
ForeignBookId = foreignBookId;
}
public BookNotFoundException(string musicbrainzId, string message)
public BookNotFoundException(string foreignBookId, string message)
: base(message)
{
MusicBrainzId = musicbrainzId;
ForeignBookId = foreignBookId;
}
}
}

View File

@@ -4,24 +4,24 @@ namespace NzbDrone.Core.Exceptions
{
public class EditionNotFoundException : NzbDroneException
{
public string MusicBrainzId { get; set; }
public string ForeignEditionId { get; set; }
public EditionNotFoundException(string musicbrainzId)
: base(string.Format("Edition with id {0} was not found, it may have been removed from metadata server.", musicbrainzId))
public EditionNotFoundException(string foreignEditionId)
: base($"Edition with id {foreignEditionId} was not found, it may have been removed from metadata server.")
{
MusicBrainzId = musicbrainzId;
ForeignEditionId = foreignEditionId;
}
public EditionNotFoundException(string musicbrainzId, string message, params object[] args)
public EditionNotFoundException(string foreignEditionId, string message, params object[] args)
: base(message, args)
{
MusicBrainzId = musicbrainzId;
ForeignEditionId = foreignEditionId;
}
public EditionNotFoundException(string musicbrainzId, string message)
public EditionNotFoundException(string foreignEditionId, string message)
: base(message)
{
MusicBrainzId = musicbrainzId;
ForeignEditionId = foreignEditionId;
}
}
}

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