Compare commits

...

151 Commits

Author SHA1 Message Date
Bogdan
2749479283 Fix possible enumerations in TrackGroupingService 2024-01-06 19:41:17 +02:00
Bogdan
4cbafa76d8 New: Custom formats in book history
(cherry picked from commit cd2ce34f10007efacd8631d3ce3ac4f4a6212966)

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

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Mario Rodriguez <mario2423@gmail.com>
Co-authored-by: Norbi <kovinor123@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translation: Servarr/Readarr
2024-01-01 14:11:59 +02:00
Mark McDowall
71cb9e1dd7 Fixed: Disable SSL on start if certificate path is not set
(cherry picked from commit 4e19fec123900b8ba1252b640f26f2a4983683ff)
2024-01-01 06:58:14 +02:00
Mark McDowall
ee5ed57fcc New: Add qBittorrent option for Content Layout
(cherry picked from commit 4b22200708ca120cfdcf9cb796be92183adb95d1)

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

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

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

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

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

Co-authored-by: Dimitri <dimitridroeck@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Koch Norbert <kochnorbert@icloud.com>
Co-authored-by: Nicola <nicola.neri@gmail.com>
Co-authored-by: SunStorm <me@sunstorm.rocks>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: chiral-lab <jan.eltner@googlemail.com>
Co-authored-by: chrizl <chrizl@gmail.com>
Co-authored-by: resi23 <x-resistant-x@gmx.de>
Co-authored-by: slammingdeath <sebastianbrudny97@gmail.com>
Co-authored-by: ube <ube@alienautopsy.net>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/sv/
Translation: Servarr/Readarr
2023-12-31 06:25:13 +02:00
Bogdan
5937c952af Fixed: Ignore empty tags when adding items to Flood
(cherry picked from commit 0a5200766ea80fc1c97bfa497cdfed31b9af687f)
2023-12-31 06:24:43 +02:00
Stevie Robinson
0f4bd3c472 New: Add sorting to Manage Indexer and Download Client modals
(cherry picked from commit 91053ca51ded804739f94ee936c1376a755dbe11)
2023-12-31 06:24:21 +02:00
Qstick
cf415e61de New: Bulk Delete for Unmapped Files
(cherry picked from commit 71c1edd47c5377bcdeeb68e9cededf122a6ce6b4)
2023-12-27 03:17:41 +02:00
Bogdan
9865e92cea Add error message for invalid Root Folder in Ebook Tag Service 2023-12-25 01:53:21 +02:00
Bogdan
1cf956a9d9 Don't use empty file path from Calibre 2023-12-25 01:53:21 +02:00
Bogdan
8989c55c8c Bump version to 0.3.14 2023-12-24 09:12:40 +02:00
Bogdan
dc83e0127e Fixed: Minor UI improvements to author and book details 2023-12-24 09:05:46 +02:00
Bogdan
34eb312426 Fixed: File Count on Books page 2023-12-24 07:22:10 +02:00
Bogdan
9d5cdebdb2 Fixed: Displaying Import List Exclusion actions on mobile 2023-12-24 05:31:34 +02:00
Bogdan
a0ab224acd Log payload for set fields request in Calibre 2023-12-24 02:08:31 +02:00
Weblate
05aa35a54d Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Aitzol Garmendia <aitzolgarmendia@gmail.com>
Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Menno Liefstingh <mennoliefstingh@gmail.com>
Co-authored-by: Pietro Ribeiro <xxb1exuv6@mozmail.com>
Co-authored-by: ROSERAT Ugo <roserat.ugo@gmail.com>
Co-authored-by: RicardoVelaC <ricardovelac@gmail.com>
Co-authored-by: SHUAI.W <x@ousui.org>
Co-authored-by: VisoTC <szlytlyt@outlook.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/lv/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translation: Servarr/Readarr
2023-12-23 03:22:45 +02:00
Mark McDowall
ca7f8775f5 Fixed: Increase width and truncate long names on Import List Exclusions
(cherry picked from commit 2d0541c03b761a0ec5e10711d6bd577e07141517)
2023-12-23 03:20:23 +02:00
Mark McDowall
2a01e9b445 Fixed: Don't grab propers/repacks when item in queue meets cutoff and propers/repacks are not downloaded automatically
(cherry picked from commit cf00fecbe410caf1a57d561e458f2e58921eef05)

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

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

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

Closes #3009
2023-12-22 03:32:28 +02:00
Weblate
ad47dc032d Multiple Translations updated by Weblate
ignore-downstream

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

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

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

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

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

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

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

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Hajiroxx <luypanda@163.com>
Co-authored-by: Jurriaan Den Toonder <jur.den.toonder@gmail.com>
Co-authored-by: RicardoVelaC <ricardovelac@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translation: Servarr/Readarr
2023-12-09 13:11:48 +02:00
Bogdan
fab7558bd4 Fixed: Don't write audio tags if there are no updates
(cherry picked from commit 1e147580729e24fbb6d8707d2a0ddfc8bd036d43)
2023-12-07 15:23:49 +02:00
Weblate
3dc86b3a01 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Augusto Poletti <augustpolet@gmail.com>
Co-authored-by: Dominika Matějková <dominika.matejkova@outlook.cz>
Co-authored-by: VisoTC <szlytlyt@outlook.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translation: Servarr/Readarr
2023-12-06 16:01:34 +02:00
Taloth Saldono
24ad6134e3 Small helper in UI to access Readarr API more easily
(cherry picked from commit 090cdc364ef335fbfea8cf540696af813f6ecea4)

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-03 13:51:48 +02:00
Weblate
4c73a619eb Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Appoxo <appoxo@appoxo.de>
Co-authored-by: David Molero <contact@dolvem.com>
Co-authored-by: Dimitri <dimitridroeck@gmail.com>
Co-authored-by: Patatra <patrice.chevreau@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Zalhera <tobias.bechen@gmail.com>
Co-authored-by: liimee <git.taaa@fedora.email>
Co-authored-by: resi23 <x-resistant-x@gmx.de>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/id/
Translation: Servarr/Readarr
2023-12-01 04:02:04 +02:00
bakerboy448
3ca798e983 Fixed: RootFolderWatchingService Logging 2023-12-01 04:01:32 +02:00
bmarinov
d9827fd6a6 Fixed: Filter unchanged files using UTC timestamps
(cherry picked from commit 9fc66e9b985a1eabd05f324ac631dfac39d2aebc)
2023-11-26 09:35:42 +02:00
Stevie Robinson
f4f03a853f New: Remove defunct Boxcar notifications
(cherry picked from commit c6ad2396bb98dc8eb1ad47bf5d066b227a47f8b5)

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

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

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

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

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

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

Closes #3039
2023-10-29 01:24:14 +03:00
Weblate
a21abe0838 Multiple Translations updated by Weblate
ignore-downstream

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translation: Servarr/Readarr
2023-08-23 04:57:55 +03:00
Bogdan
c31a797bd8 Revert "Switch to Parallel.ForEach for list processing with MaxParallelism"
This reverts commit ebb2b4eca3.
2023-08-22 06:03:14 +03:00
Qstick
ebb2b4eca3 Switch to Parallel.ForEach for list processing with MaxParallelism
(cherry picked from commit 0f93e04186f24abdb0cf0b3ba6a3505fda834e06)
2023-08-21 04:44:59 +03:00
Qstick
3ec5d9b9fe Use default MemoryAllocator for ImageSharp resizing
(cherry picked from commit c1a3a8249befde0a1b68e7845d5d2346066457a1)
2023-08-21 04:42:59 +03:00
Qstick
1ad84a7c44 Fixed: Ignore case when comparing torrent infohash
(cherry picked from commit 7986488c6d1687b0810b3bcac2c1dae725e770ac)
2023-08-20 16:10:26 -05:00
Bogdan
9d67c18254 Bump version to 0.3.4 2023-08-20 12:24:38 +03:00
310 changed files with 5986 additions and 2813 deletions

View File

@@ -1,5 +1,5 @@
name: Bug Report
description: 'Report a new bug, if you are not 100% certain this is a bug please go to our Reddit or Discord first'
description: 'Report a new bug, if you are not 100% certain this is a bug please go to our Discord first'
labels: ['Type: Bug', 'Status: Needs Triage']
body:
- type: checkboxes

View File

@@ -3,6 +3,3 @@ contact_links:
- name: Support via Discord
url: https://readarr.com/discord
about: Chat with users and devs on support and setup related topics.
- name: Support via Reddit
url: https://reddit.com/r/Readarr
about: Discuss and search thru support topics.

View File

@@ -15,8 +15,7 @@ jobs:
issue-comment: >
:wave: @{issue-author}, we use the issue tracker exclusively
for bug reports and feature requests. However, this issue appears
to be a support request. Please hop over onto our [Discord](https://readarr.com/discord)
or [Subreddit](https://reddit.com/r/readarr)
to be a support request. Please hop over onto our [Discord](https://readarr.com/discord).
close-issue: true
lock-issue: false
- uses: dessant/support-requests@v3

View File

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

View File

@@ -4,14 +4,14 @@ module.exports = {
plugins: [
// Stage 1
'@babel/plugin-proposal-export-default-from',
['@babel/plugin-proposal-optional-chaining', { loose }],
['@babel/plugin-proposal-nullish-coalescing-operator', { loose }],
['@babel/plugin-transform-optional-chaining', { loose }],
['@babel/plugin-transform-nullish-coalescing-operator', { loose }],
// Stage 2
'@babel/plugin-proposal-export-namespace-from',
'@babel/plugin-transform-export-namespace-from',
// Stage 3
['@babel/plugin-proposal-class-properties', { loose }],
['@babel/plugin-transform-class-properties', { loose }],
'@babel/plugin-syntax-dynamic-import'
],
env: {

View File

@@ -338,4 +338,8 @@ Queue.propTypes = {
onRemoveSelectedPress: PropTypes.func.isRequired
};
Queue.defaultProps = {
count: 0
};
export default Queue;

View File

@@ -7,13 +7,10 @@ function findImage(images, coverType) {
}
function getUrl(image, coverType, size) {
if (image) {
// Remove protocol
let url = image.url;
const imageUrl = image?.url;
url = url.replace(`${coverType}.jpg`, `${coverType}-${size}.jpg`);
return url;
if (imageUrl) {
return imageUrl.replace(`${coverType}.jpg`, `${coverType}-${size}.jpg`);
}
}

View File

@@ -44,6 +44,10 @@
margin-top: 20px;
}
.filterIcon {
float: right;
}
.authorNavigationButtons {
position: absolute;
right: 0;

View File

@@ -6,6 +6,7 @@ interface CssExports {
'authorUpButton': string;
'contentContainer': string;
'errorMessage': string;
'filterIcon': string;
'innerContentBody': string;
'metadataMessage': string;
'selectedTab': string;

View File

@@ -239,9 +239,14 @@ class AuthorDetails extends Component {
saveError,
isDeleting,
deleteError,
statistics
statistics = {}
} = this.props;
const {
bookFileCount = 0,
totalBookCount = 0
} = statistics;
const {
isOrganizeModalOpen,
isRetagModalOpen,
@@ -435,7 +440,7 @@ class AuthorDetails extends Component {
className={styles.tab}
selectedClassName={styles.selectedTab}
>
{translate('BooksTotal', [statistics.totalBookCount])}
{translate('BooksTotal', [totalBookCount])}
</Tab>
<Tab
@@ -463,7 +468,7 @@ class AuthorDetails extends Component {
className={styles.tab}
selectedClassName={styles.selectedTab}
>
{translate('FilesTotal', [statistics.bookFileCount])}
{translate('FilesTotal', [bookFileCount])}
</Tab>
{

View File

@@ -136,8 +136,9 @@
}
.title {
font-weight: 300;
font-size: 30px;
line-height: 50px;
line-height: 30px;
}
}

View File

@@ -25,12 +25,7 @@ const defaultFontSize = parseInt(fonts.defaultFontSize);
const lineHeight = parseFloat(fonts.lineHeight);
function getFanartUrl(images) {
const fanartImage = images.find((x) => x.coverType === 'fanart');
if (fanartImage) {
// Remove protocol
return fanartImage.url.replace(/^https?:/, '');
}
return images.find((x) => x.coverType === 'fanart')?.url;
}
class AuthorDetailsHeader extends Component {

View File

@@ -1,6 +1,7 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import AuthorHistoryContentConnector from './AuthorHistoryContentConnector';
import AuthorHistoryModalContent from './AuthorHistoryModalContent';
@@ -14,6 +15,7 @@ function AuthorHistoryModal(props) {
return (
<Modal
isOpen={isOpen}
size={sizes.EXTRA_LARGE}
onModalClose={onModalClose}
>
<AuthorHistoryContentConnector

View File

@@ -5,6 +5,7 @@ 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 translate from 'Utilities/String/translate';
import AuthorHistoryTableContent from './AuthorHistoryTableContent';
class AuthorHistoryModalContent extends Component {
@@ -20,7 +21,7 @@ class AuthorHistoryModalContent extends Component {
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
History
{translate('History')}
</ModalHeader>
<ModalBody>
@@ -31,7 +32,7 @@ class AuthorHistoryModalContent extends Component {
<ModalFooter>
<Button onPress={onModalClose}>
Close
{translate('Close')}
</Button>
</ModalFooter>
</ModalContent>

View File

@@ -4,7 +4,6 @@
word-break: break-word;
}
.details,
.actions {
composes: cell from '~Components/Table/Cells/TableRowCell.css';

View File

@@ -2,7 +2,6 @@
// Please do not change this file!
interface CssExports {
'actions': string;
'details': string;
'sourceTitle': string;
}
export const cssExports: CssExports;

View File

@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import HistoryDetailsConnector from 'Activity/History/Details/HistoryDetailsConnector';
import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
import BookFormats from 'Book/BookFormats';
import BookQuality from 'Book/BookQuality';
import Icon from 'Components/Icon';
import IconButton from 'Components/Link/IconButton';
@@ -11,6 +12,7 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import Popover from 'Components/Tooltip/Popover';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import translate from 'Utilities/String/translate';
import styles from './AuthorHistoryRow.css';
@@ -75,6 +77,8 @@ class AuthorHistoryRow extends Component {
sourceTitle,
quality,
qualityCutoffNotMet,
customFormats,
customFormatScore,
date,
data,
book
@@ -106,11 +110,19 @@ class AuthorHistoryRow extends Component {
/>
</TableRowCell>
<TableRowCell>
<BookFormats formats={customFormats} />
</TableRowCell>
<TableRowCell>
{formatCustomFormatScore(customFormatScore, customFormats.length)}
</TableRowCell>
<RelativeDateCellConnector
date={date}
/>
<TableRowCell className={styles.details}>
<TableRowCell className={styles.actions}>
<Popover
anchor={
<Icon
@@ -127,14 +139,13 @@ class AuthorHistoryRow extends Component {
}
position={tooltipPositions.LEFT}
/>
</TableRowCell>
<TableRowCell className={styles.actions}>
{
eventType === 'grabbed' &&
<IconButton
title={translate('MarkAsFailed')}
name={icons.REMOVE}
size={14}
onPress={this.onMarkAsFailedPress}
/>
}
@@ -160,6 +171,8 @@ AuthorHistoryRow.propTypes = {
sourceTitle: PropTypes.string.isRequired,
quality: PropTypes.object.isRequired,
qualityCutoffNotMet: PropTypes.bool.isRequired,
customFormats: PropTypes.arrayOf(PropTypes.object),
customFormatScore: PropTypes.number.isRequired,
date: PropTypes.string.isRequired,
data: PropTypes.object.isRequired,
fullAuthor: PropTypes.bool.isRequired,

View File

@@ -0,0 +1,9 @@
.container {
border: 1px solid var(--borderColor);
border-radius: 4px;
background-color: var(--inputBackgroundColor);
&:last-of-type {
margin-bottom: 0;
}
}

View File

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

View File

@@ -1,6 +1,7 @@
import React from 'react';
import AuthorHistoryContentConnector from 'Author/History/AuthorHistoryContentConnector';
import AuthorHistoryTableContent from 'Author/History/AuthorHistoryTableContent';
import styles from './AuthorHistoryTable.css';
function AuthorHistoryTable(props) {
const {
@@ -8,10 +9,12 @@ function AuthorHistoryTable(props) {
} = props;
return (
<AuthorHistoryContentConnector
component={AuthorHistoryTableContent}
{...otherProps}
/>
<div className={styles.container}>
<AuthorHistoryContentConnector
component={AuthorHistoryTableContent}
{...otherProps}
/>
</div>
);
}

View File

@@ -0,0 +1,5 @@
.blankpad {
padding-top: 10px;
padding-bottom: 10px;
padding-left: 2em;
}

View File

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

View File

@@ -1,12 +1,14 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import Icon from 'Components/Icon';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { kinds } from 'Helpers/Props';
import { icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import AuthorHistoryRowConnector from './AuthorHistoryRowConnector';
import styles from './AuthorHistoryTableContent.css';
const columns = [
{
@@ -15,32 +17,41 @@ const columns = [
},
{
name: 'book',
label: 'Book',
label: () => translate('Book'),
isVisible: true
},
{
name: 'sourceTitle',
label: 'Source Title',
label: () => translate( 'SourceTitle'),
isVisible: true
},
{
name: 'quality',
label: 'Quality',
label: () => translate('Quality'),
isVisible: true
},
{
name: 'customFormats',
label: () => translate('CustomFormats'),
isSortable: false,
isVisible: true
},
{
name: 'customFormatScore',
label: React.createElement(Icon, {
name: icons.SCORE,
title: () => translate('CustomFormatScore')
}),
isSortable: true,
isVisible: true
},
{
name: 'date',
label: 'Date',
isVisible: true
},
{
name: 'details',
label: 'Details',
label: () => translate('Date'),
isVisible: true
},
{
name: 'actions',
label: 'Actions',
isVisible: true
}
];
@@ -64,7 +75,7 @@ class AuthorHistoryTableContent extends Component {
const hasItems = !!items.length;
return (
<>
<div>
{
isFetching &&
<LoadingIndicator />
@@ -79,7 +90,7 @@ class AuthorHistoryTableContent extends Component {
{
isPopulated && !hasItems && !error &&
<div>
<div className={styles.blankpad}>
{translate('NoHistory')}
</div>
}
@@ -103,7 +114,7 @@ class AuthorHistoryTableContent extends Component {
</TableBody>
</Table>
}
</>
</div>
);
}
}

View File

@@ -16,7 +16,7 @@ import AuthorIndex from './AuthorIndex';
function createMapStateToProps() {
return createSelector(
createAuthorClientSideCollectionItemsSelector('authorIndex'),
createCommandExecutingSelector(commandNames.REFRESH_AUTHOR),
createCommandExecutingSelector(commandNames.BULK_REFRESH_AUTHOR),
createCommandExecutingSelector(commandNames.RSS_SYNC),
createCommandExecutingSelector(commandNames.RENAME_AUTHOR),
createCommandExecutingSelector(commandNames.RETAG_AUTHOR),
@@ -24,17 +24,17 @@ function createMapStateToProps() {
(
author,
isRefreshingAuthor,
isRssSyncExecuting,
isOrganizingAuthor,
isRetaggingAuthor,
isRssSyncExecuting,
dimensionsState
) => {
return {
...author,
isRefreshingAuthor,
isRssSyncExecuting,
isOrganizingAuthor,
isRetaggingAuthor,
isRssSyncExecuting,
isSmallScreen: dimensionsState.isSmallScreen
};
}

View File

@@ -3,6 +3,7 @@ import React from 'react';
import Label from 'Components/Label';
import { kinds } from 'Helpers/Props';
import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
function getTooltip(title, quality, size, isMonitored, isCutoffNotMet) {
const revision = quality.revision;
@@ -28,6 +29,36 @@ function getTooltip(title, quality, size, isMonitored, isCutoffNotMet) {
return title;
}
function revisionLabel(className, quality, showRevision) {
if (!showRevision) {
return;
}
if (quality.revision.isRepack) {
return (
<Label
className={className}
kind={kinds.PRIMARY}
title={translate('Repack')}
>
R
</Label>
);
}
if (quality.revision.version && quality.revision.version > 1) {
return (
<Label
className={className}
kind={kinds.PRIMARY}
title={translate('Proper')}
>
P
</Label>
);
}
}
function BookQuality(props) {
const {
className,
@@ -35,7 +66,8 @@ function BookQuality(props) {
quality,
size,
isMonitored,
isCutoffNotMet
isCutoffNotMet,
showRevision
} = props;
let kind = kinds.DEFAULT;
@@ -50,13 +82,15 @@ function BookQuality(props) {
}
return (
<Label
className={className}
kind={kind}
title={getTooltip(title, quality, size, isMonitored, isCutoffNotMet)}
>
{quality.quality.name}
</Label>
<span>
<Label
className={className}
kind={kind}
title={getTooltip(title, quality, size, isMonitored, isCutoffNotMet)}
>
{quality.quality.name}
</Label>{revisionLabel(className, quality, showRevision)}
</span>
);
}
@@ -66,12 +100,14 @@ BookQuality.propTypes = {
quality: PropTypes.object.isRequired,
size: PropTypes.number,
isMonitored: PropTypes.bool,
isCutoffNotMet: PropTypes.bool
isCutoffNotMet: PropTypes.bool,
showRevision: PropTypes.bool
};
BookQuality.defaultProps = {
title: '',
isMonitored: true
isMonitored: true,
showRevision: false
};
export default BookQuality;

View File

@@ -99,9 +99,14 @@ class BookDetails extends Component {
nextBook,
isSearching,
onRefreshPress,
onSearchPress
onSearchPress,
statistics = {}
} = this.props;
const {
bookFileCount = 0
} = statistics;
const {
isOrganizeModalOpen,
isRetagModalOpen,
@@ -238,21 +243,21 @@ class BookDetails extends Component {
className={styles.tab}
selectedClassName={styles.selectedTab}
>
History
{translate('History')}
</Tab>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
Search
{translate('Search')}
</Tab>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
Files
{translate('FilesTotal', [bookFileCount])}
</Tab>
{
@@ -335,6 +340,7 @@ BookDetails.propTypes = {
ratings: PropTypes.object.isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired,
links: PropTypes.arrayOf(PropTypes.object).isRequired,
statistics: PropTypes.object.isRequired,
monitored: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
isSaving: PropTypes.bool.isRequired,

View File

@@ -117,8 +117,9 @@
}
.title {
font-weight: 300;
font-size: 30px;
line-height: 50px;
line-height: 30px;
}
}

View File

@@ -21,12 +21,7 @@ const defaultFontSize = parseInt(fonts.defaultFontSize);
const lineHeight = parseFloat(fonts.lineHeight);
function getFanartUrl(images) {
const fanartImage = images.find((x) => x.coverType === 'fanart');
if (fanartImage) {
// Remove protocol
return fanartImage.url.replace(/^https?:/, '');
}
return images.find((x) => x.coverType === 'fanart')?.url;
}
class BookDetailsHeader extends Component {

View File

@@ -16,8 +16,8 @@ import BookIndex from './BookIndex';
function createMapStateToProps() {
return createSelector(
createBookClientSideCollectionItemsSelector('bookIndex'),
createCommandExecutingSelector(commandNames.REFRESH_AUTHOR),
createCommandExecutingSelector(commandNames.REFRESH_BOOK),
createCommandExecutingSelector(commandNames.BULK_REFRESH_AUTHOR),
createCommandExecutingSelector(commandNames.BULK_REFRESH_BOOK),
createCommandExecutingSelector(commandNames.RSS_SYNC),
createCommandExecutingSelector(commandNames.CUTOFF_UNMET_BOOK_SEARCH),
createCommandExecutingSelector(commandNames.MISSING_BOOK_SEARCH),

View File

@@ -229,7 +229,6 @@ class BookIndexRow extends Component {
className={styles[name]}
>
{bookFileCount}
</VirtualTableRowCell>
);
}

View File

@@ -0,0 +1,9 @@
.container {
border: 1px solid var(--borderColor);
border-radius: 4px;
background-color: var(--inputBackgroundColor);
&:last-of-type {
margin-bottom: 0;
}
}

View File

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

View File

@@ -1,5 +1,6 @@
import React from 'react';
import BookFileEditorTableContentConnector from './BookFileEditorTableContentConnector';
import styles from './BookFileEditorTable.css';
function BookFileEditorTable(props) {
const {
@@ -7,9 +8,11 @@ function BookFileEditorTable(props) {
} = props;
return (
<BookFileEditorTableContentConnector
{...otherProps}
/>
<div className={styles.container}>
<BookFileEditorTableContentConnector
{...otherProps}
/>
</div>
);
}

View File

@@ -1,6 +1,6 @@
.filesTable {
margin-bottom: 20px;
padding-top: 15px;
margin: 10px;
padding-top: 5px;
border: 1px solid var(--borderColor);
border-top: 1px solid var(--borderColor);
border-radius: 4px;
@@ -13,9 +13,15 @@
.actions {
display: flex;
margin-right: auto;
margin: 10px;
}
.selectInput {
margin-left: 10px;
}
.blankpad {
padding-top: 10px;
padding-bottom: 10px;
padding-left: 2em;
}

View File

@@ -2,6 +2,7 @@
// Please do not change this file!
interface CssExports {
'actions': string;
'blankpad': string;
'filesTable': string;
'selectInput': string;
}

View File

@@ -1,6 +1,7 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import SelectInput from 'Components/Form/SelectInput';
import SpinnerButton from 'Components/Link/SpinnerButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
@@ -120,7 +121,7 @@ class BookFileEditorTableContent extends Component {
const hasSelectedFiles = this.getSelectedIds().length > 0;
return (
<>
<div>
{
isFetching && !isPopulated ?
<LoadingIndicator /> :
@@ -129,13 +130,13 @@ class BookFileEditorTableContent extends Component {
{
!isFetching && error ?
<div>{error}</div> :
<Alert kind={kinds.DANGER}>{error}</Alert> :
null
}
{
isPopulated && !items.length ?
<div>
<div className={styles.blankpad}>
No book files to manage.
</div> :
null
@@ -173,26 +174,30 @@ class BookFileEditorTableContent extends Component {
null
}
<div className={styles.actions}>
<SpinnerButton
kind={kinds.DANGER}
isSpinning={isDeleting}
isDisabled={!hasSelectedFiles}
onPress={this.onDeletePress}
>
Delete
</SpinnerButton>
{
isPopulated && items.length ? (
<div className={styles.actions}>
<SpinnerButton
kind={kinds.DANGER}
isSpinning={isDeleting}
isDisabled={!hasSelectedFiles}
onPress={this.onDeletePress}
>
{translate('Delete')}
</SpinnerButton>
<div className={styles.selectInput}>
<SelectInput
name="quality"
value="selectQuality"
values={qualityOptions}
isDisabled={!hasSelectedFiles}
onChange={this.onQualityChange}
/>
</div>
</div>
<div className={styles.selectInput}>
<SelectInput
name="quality"
value="selectQuality"
values={qualityOptions}
isDisabled={!hasSelectedFiles}
onChange={this.onQualityChange}
/>
</div>
</div>
) : null
}
<ConfirmModal
isOpen={isConfirmDeleteModalOpen}
@@ -203,7 +208,7 @@ class BookFileEditorTableContent extends Component {
onConfirm={this.onConfirmDelete}
onCancel={this.onConfirmDeleteModalClose}
/>
</>
</div>
);
}
}

View File

@@ -47,7 +47,7 @@ class CalendarConnector extends Component {
gotoCalendarToday
} = this.props;
registerPagePopulator(this.repopulate);
registerPagePopulator(this.repopulate, ['bookFileUpdated', 'bookFileDeleted']);
if (useCurrentPage) {
fetchCalendar();

View File

@@ -1,9 +1,7 @@
.description {
line-height: $lineHeight;
}
.description {
margin-left: 0;
line-height: $lineHeight;
overflow-wrap: break-word;
}
@media (min-width: 768px) {

View File

@@ -25,6 +25,10 @@
white-space: pre-wrap;
}
.version {
margin-top: 20px;
}
@media only screen and (max-width: $breakpointMedium) {
.image {
height: 250px;

View File

@@ -6,6 +6,7 @@ interface CssExports {
'image': string;
'imageContainer': string;
'message': string;
'version': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -1,60 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import styles from './ErrorBoundaryError.css';
function ErrorBoundaryError(props) {
const {
className,
messageClassName,
detailsClassName,
message,
error,
info
} = props;
return (
<div className={className}>
<div className={messageClassName}>
{message}
</div>
<div className={styles.imageContainer}>
<img
className={styles.image}
src={`${window.Readarr.urlBase}/Content/Images/error.png`}
/>
</div>
<details className={detailsClassName}>
{
error &&
<div>
{error.toString()}
</div>
}
<div className={styles.info}>
{info.componentStack}
</div>
</details>
</div>
);
}
ErrorBoundaryError.propTypes = {
className: PropTypes.string.isRequired,
messageClassName: PropTypes.string.isRequired,
detailsClassName: PropTypes.string.isRequired,
message: PropTypes.string.isRequired,
error: PropTypes.object.isRequired,
info: PropTypes.object.isRequired
};
ErrorBoundaryError.defaultProps = {
className: styles.container,
messageClassName: styles.message,
detailsClassName: styles.details,
message: 'There was an error loading this content'
};
export default ErrorBoundaryError;

View File

@@ -0,0 +1,77 @@
import React, { useEffect, useState } from 'react';
import StackTrace from 'stacktrace-js';
import translate from 'Utilities/String/translate';
import styles from './ErrorBoundaryError.css';
interface ErrorBoundaryErrorProps {
className: string;
messageClassName: string;
detailsClassName: string;
message: string;
error: Error;
info: {
componentStack: string;
};
}
function ErrorBoundaryError(props: ErrorBoundaryErrorProps) {
const {
className = styles.container,
messageClassName = styles.message,
detailsClassName = styles.details,
message = translate('ErrorLoadingContent'),
error,
info,
} = props;
const [detailedError, setDetailedError] = useState<
StackTrace.StackFrame[] | null
>(null);
useEffect(() => {
if (error) {
StackTrace.fromError(error).then((de) => {
setDetailedError(de);
});
} else {
setDetailedError(null);
}
}, [error, setDetailedError]);
return (
<div className={className}>
<div className={messageClassName}>{message}</div>
<div className={styles.imageContainer}>
<img
className={styles.image}
src={`${window.Readarr.urlBase}/Content/Images/error.png`}
/>
</div>
<details className={detailsClassName}>
{error ? <div>{error.message}</div> : null}
{detailedError ? (
detailedError.map((d, index) => {
return (
<div key={index}>
{` at ${d.functionName} (${d.fileName}:${d.lineNumber}:${d.columnNumber})`}
</div>
);
})
) : (
<div>{info.componentStack}</div>
)}
{
<div className={styles.version}>
Version: {window.Readarr.version}
</div>
}
</details>
</div>
);
}
export default ErrorBoundaryError;

View File

@@ -9,6 +9,10 @@
&:hover {
background-color: var(--inputHoverBackgroundColor);
}
&.isDisabled {
cursor: not-allowed;
}
}
.optionCheck {

View File

@@ -202,6 +202,8 @@ class SignalRConnector extends Component {
this.props.dispatchUpdateItem({ section, ...body.resource });
} else if (body.action === 'deleted') {
this.props.dispatchRemoveItem({ section, id: body.resource.id });
repopulatePage('bookFileDeleted');
}
// Repopulate the page to handle recently imported file

View File

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

View File

@@ -0,0 +1,120 @@
import createAjaxRequest from 'Utilities/createAjaxRequest';
// This file contains some helpers for power users in a browser console
let hasWarned = false;
function checkActivationWarning() {
if (!hasWarned) {
console.log('Activated ReadarrApi console helpers.');
console.warn('Be warned: There will be no further confirmation checks.');
hasWarned = true;
}
}
function attachAsyncActions(promise) {
promise.filter = function() {
const args = arguments;
const res = this.then((d) => d.filter(...args));
attachAsyncActions(res);
return res;
};
promise.map = function() {
const args = arguments;
const res = this.then((d) => d.map(...args));
attachAsyncActions(res);
return res;
};
promise.all = function() {
const res = this.then((d) => Promise.all(d));
attachAsyncActions(res);
return res;
};
promise.forEach = function(action) {
const res = this.then((d) => Promise.all(d.map(action)));
attachAsyncActions(res);
return res;
};
}
class ResourceApi {
constructor(api, url) {
this.api = api;
this.url = url;
}
single(id) {
return this.api.fetch(`${this.url}/${id}`);
}
all() {
return this.api.fetch(this.url);
}
filter(pred) {
return this.all().filter(pred);
}
update(resource) {
return this.api.fetch(`${this.url}/${resource.id}`, { method: 'PUT', data: resource });
}
delete(resource) {
if (typeof resource === 'object' && resource !== null && resource.id) {
resource = resource.id;
}
if (!resource || !Number.isInteger(resource)) {
throw Error('Invalid resource', resource);
}
return this.api.fetch(`${this.url}/${resource}`, { method: 'DELETE' });
}
fetch(url, options) {
return this.api.fetch(`${this.url}${url}`, options);
}
}
class ConsoleApi {
constructor() {
this.author = new ResourceApi(this, '/author');
}
resource(url) {
return new ResourceApi(this, url);
}
fetch(url, options) {
checkActivationWarning();
options = options || {};
const req = {
url,
method: options.method || 'GET'
};
if (options.data) {
req.dataType = 'json';
req.data = JSON.stringify(options.data);
}
const promise = createAjaxRequest(req).request;
promise.fail((xhr) => {
console.error(`Failed to fetch ${url}`, xhr);
});
attachAsyncActions(promise);
return promise;
}
}
window.ReadarrApi = new ConsoleApi();
export default ConsoleApi;

View File

@@ -28,6 +28,10 @@
text-align: center;
}
.quality {
white-space: nowrap;
}
.customFormatScore {
composes: cell from '~Components/Table/Cells/TableRowCell.css';

View File

@@ -178,7 +178,7 @@ class InteractiveSearchRow extends Component {
</TableRowCell>
<TableRowCell className={styles.quality}>
<BookQuality quality={quality} />
<BookQuality quality={quality} showRevision={true} />
</TableRowCell>
<TableRowCell className={styles.customFormatScore}>

View File

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

View File

@@ -8,11 +8,13 @@
}
.name {
flex: 1 0 300px;
@add-mixin truncate;
flex: 0 1 600px;
}
.foreignId {
flex: 0 0 200px;
flex: 0 0 100px;
}
.actions {

View File

@@ -4,12 +4,12 @@
font-weight: bold;
}
.foreignId {
flex: 0 0 200px;
.name {
flex: 0 1 600px;
}
.name {
flex: 1 0 300px;
.foreignId {
flex: 0 0 100px;
}
.addImportListExclusion {

View File

@@ -14,9 +14,11 @@ import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import useSelectState from 'Helpers/Hooks/useSelectState';
import { kinds } from 'Helpers/Props';
import SortDirection from 'Helpers/Props/SortDirection';
import {
bulkDeleteIndexers,
bulkEditIndexers,
setManageIndexersSort,
} from 'Store/Actions/settingsActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import { SelectStateInputProps } from 'typings/props';
@@ -80,6 +82,8 @@ const COLUMNS = [
interface ManageIndexersModalContentProps {
onModalClose(): void;
sortKey?: string;
sortDirection?: SortDirection;
}
function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
@@ -92,6 +96,8 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
isSaving,
error,
items,
sortKey,
sortDirection,
}: IndexerAppState = useSelector(
createClientSideCollectionSelector('settings.indexers')
);
@@ -112,6 +118,13 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
const selectedCount = selectedIds.length;
const onSortPress = useCallback(
(value: string) => {
dispatch(setManageIndexersSort({ sortKey: value }));
},
[dispatch]
);
const onDeletePress = useCallback(() => {
setIsDeleteModalOpen(true);
}, [setIsDeleteModalOpen]);
@@ -214,6 +227,9 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
allSelected={allSelected}
allUnselected={allUnselected}
onSelectAllChange={onSelectAllChange}
sortKey={sortKey}
sortDirection={sortDirection}
onSortPress={onSortPress}
>
<TableBody>
{items.map((item) => {

View File

@@ -212,26 +212,24 @@ class MediaManagement extends Component {
</FormGroup>
{
settings.importExtraFiles.value &&
settings.importExtraFiles.value ?
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>
{translate('ImportExtraFiles')}
</FormLabel>
<FormLabel>{translate('ImportExtraFiles')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="extraFileExtensions"
helpTexts={[
translate('ExtraFileExtensionsHelpTexts1'),
translate('ExtraFileExtensionsHelpTexts2')
translate('ExtraFileExtensionsHelpText'),
translate('ExtraFileExtensionsHelpTextsExamples')
]}
onChange={onInputChange}
{...settings.extraFileExtensions}
/>
</FormGroup>
</FormGroup> : null
}
</FieldSet>
}

View File

@@ -60,6 +60,7 @@ class Notification extends Component {
onReleaseImport,
onUpgrade,
onRename,
onAuthorAdded,
onAuthorDelete,
onBookDelete,
onBookFileDelete,
@@ -73,6 +74,7 @@ class Notification extends Component {
supportsOnReleaseImport,
supportsOnUpgrade,
supportsOnRename,
supportsOnAuthorAdded,
supportsOnAuthorDelete,
supportsOnBookDelete,
supportsOnBookFileDelete,
@@ -136,6 +138,14 @@ class Notification extends Component {
null
}
{
supportsOnAuthorAdded && onAuthorAdded ?
<Label kind={kinds.SUCCESS}>
{translate('OnAuthorAdded')}
</Label> :
null
}
{
supportsOnAuthorDelete && onAuthorDelete ?
<Label kind={kinds.SUCCESS}>
@@ -244,6 +254,7 @@ Notification.propTypes = {
onReleaseImport: PropTypes.bool.isRequired,
onUpgrade: PropTypes.bool.isRequired,
onRename: PropTypes.bool.isRequired,
onAuthorAdded: PropTypes.bool.isRequired,
onAuthorDelete: PropTypes.bool.isRequired,
onBookDelete: PropTypes.bool.isRequired,
onBookFileDelete: PropTypes.bool.isRequired,
@@ -257,6 +268,7 @@ Notification.propTypes = {
supportsOnReleaseImport: PropTypes.bool.isRequired,
supportsOnUpgrade: PropTypes.bool.isRequired,
supportsOnRename: PropTypes.bool.isRequired,
supportsOnAuthorAdded: PropTypes.bool.isRequired,
supportsOnAuthorDelete: PropTypes.bool.isRequired,
supportsOnBookDelete: PropTypes.bool.isRequired,
supportsOnBookFileDelete: PropTypes.bool.isRequired,

View File

@@ -19,6 +19,7 @@ function NotificationEventItems(props) {
onReleaseImport,
onUpgrade,
onRename,
onAuthorAdded,
onAuthorDelete,
onBookDelete,
onBookFileDelete,
@@ -32,6 +33,7 @@ function NotificationEventItems(props) {
supportsOnReleaseImport,
supportsOnUpgrade,
supportsOnRename,
supportsOnAuthorAdded,
supportsOnAuthorDelete,
supportsOnBookDelete,
supportsOnBookFileDelete,
@@ -123,6 +125,17 @@ function NotificationEventItems(props) {
/>
</div>
<div>
<FormInputGroup
type={inputTypes.CHECK}
name="onAuthorAdded"
helpText={translate('OnAuthorAddedHelpText')}
isDisabled={!supportsOnAuthorAdded.value}
{...onAuthorAdded}
onChange={onInputChange}
/>
</div>
<div>
<FormInputGroup
type={inputTypes.CHECK}

View File

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

View File

@@ -1,4 +1,5 @@
import { createAction } from 'redux-actions';
import { sortDirections } from 'Helpers/Props';
import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler';
import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler';
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
@@ -7,6 +8,7 @@ import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHand
import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler';
import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler';
import createSetClientSideCollectionSortReducer from 'Store/Actions/Creators/Reducers/createSetClientSideCollectionSortReducer';
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import { createThunk } from 'Store/thunks';
@@ -33,6 +35,7 @@ export const CANCEL_TEST_DOWNLOAD_CLIENT = 'settings/downloadClients/cancelTestD
export const TEST_ALL_DOWNLOAD_CLIENTS = 'settings/downloadClients/testAllDownloadClients';
export const BULK_EDIT_DOWNLOAD_CLIENTS = 'settings/downloadClients/bulkEditDownloadClients';
export const BULK_DELETE_DOWNLOAD_CLIENTS = 'settings/downloadClients/bulkDeleteDownloadClients';
export const SET_MANAGE_DOWNLOAD_CLIENTS_SORT = 'settings/downloadClients/setManageDownloadClientsSort';
//
// Action Creators
@@ -49,6 +52,7 @@ export const cancelTestDownloadClient = createThunk(CANCEL_TEST_DOWNLOAD_CLIENT)
export const testAllDownloadClients = createThunk(TEST_ALL_DOWNLOAD_CLIENTS);
export const bulkEditDownloadClients = createThunk(BULK_EDIT_DOWNLOAD_CLIENTS);
export const bulkDeleteDownloadClients = createThunk(BULK_DELETE_DOWNLOAD_CLIENTS);
export const setManageDownloadClientsSort = createAction(SET_MANAGE_DOWNLOAD_CLIENTS_SORT);
export const setDownloadClientValue = createAction(SET_DOWNLOAD_CLIENT_VALUE, (payload) => {
return {
@@ -88,7 +92,9 @@ export default {
isTesting: false,
isTestingAll: false,
items: [],
pendingChanges: {}
pendingChanges: {},
sortKey: 'name',
sortDirection: sortDirections.DESCENDING
},
//
@@ -121,7 +127,10 @@ export default {
return selectedSchema;
});
}
},
[SET_MANAGE_DOWNLOAD_CLIENTS_SORT]: createSetClientSideCollectionSortReducer(section)
}
};

View File

@@ -1,4 +1,5 @@
import { createAction } from 'redux-actions';
import { sortDirections } from 'Helpers/Props';
import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler';
import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler';
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
@@ -7,6 +8,7 @@ import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHand
import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler';
import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler';
import createSetClientSideCollectionSortReducer from 'Store/Actions/Creators/Reducers/createSetClientSideCollectionSortReducer';
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import { createThunk } from 'Store/thunks';
@@ -36,6 +38,7 @@ export const CANCEL_TEST_INDEXER = 'settings/indexers/cancelTestIndexer';
export const TEST_ALL_INDEXERS = 'settings/indexers/testAllIndexers';
export const BULK_EDIT_INDEXERS = 'settings/indexers/bulkEditIndexers';
export const BULK_DELETE_INDEXERS = 'settings/indexers/bulkDeleteIndexers';
export const SET_MANAGE_INDEXERS_SORT = 'settings/indexers/setManageIndexersSort';
//
// Action Creators
@@ -53,6 +56,7 @@ export const cancelTestIndexer = createThunk(CANCEL_TEST_INDEXER);
export const testAllIndexers = createThunk(TEST_ALL_INDEXERS);
export const bulkEditIndexers = createThunk(BULK_EDIT_INDEXERS);
export const bulkDeleteIndexers = createThunk(BULK_DELETE_INDEXERS);
export const setManageIndexersSort = createAction(SET_MANAGE_INDEXERS_SORT);
export const setIndexerValue = createAction(SET_INDEXER_VALUE, (payload) => {
return {
@@ -92,7 +96,9 @@ export default {
isTesting: false,
isTestingAll: false,
items: [],
pendingChanges: {}
pendingChanges: {},
sortKey: 'name',
sortDirection: sortDirections.DESCENDING
},
//
@@ -151,7 +157,10 @@ export default {
};
return updateSectionState(state, section, newState);
}
},
[SET_MANAGE_INDEXERS_SORT]: createSetClientSideCollectionSortReducer(section)
}
};

View File

@@ -106,6 +106,7 @@ export default {
selectedSchema.onReleaseImport = selectedSchema.supportsOnReleaseImport;
selectedSchema.onUpgrade = selectedSchema.supportsOnUpgrade;
selectedSchema.onRename = selectedSchema.supportsOnRename;
selectedSchema.onAuthorAdded = selectedSchema.supportsOnAuthorAdded;
selectedSchema.onAuthorDelete = selectedSchema.supportsOnAuthorDelete;
selectedSchema.onBookDelete = selectedSchema.supportsOnBookDelete;
selectedSchema.onBookFileDelete = selectedSchema.supportsOnBookFileDelete;

View File

@@ -41,6 +41,14 @@ export const defaultState = {
},
columns: [
{
name: 'select',
columnLabel: 'Select',
isSortable: false,
isVisible: true,
isModifiable: false,
isHidden: true
},
{
name: 'path',
label: 'Path',

View File

@@ -158,7 +158,7 @@ export const defaultState = {
bookFileCount: function(item) {
const { statistics = {} } = item;
return statistics.bookCount || 0;
return statistics.bookFileCount || 0;
},
ratings: function(item) {

View File

@@ -84,11 +84,6 @@ export const defaultState = {
label: 'Source Title',
isVisible: false
},
{
name: 'sourceTitle',
label: 'Source Title',
isVisible: false
},
{
name: 'customFormatScore',
columnLabel: 'Custom Format Score',

View File

@@ -5,6 +5,7 @@ import { sortDirections } from 'Helpers/Props';
import { createThunk, handleThunks } from 'Store/thunks';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import updateSectionState from 'Utilities/State/updateSectionState';
import naturalExpansion from 'Utilities/String/naturalExpansion';
import { set, update, updateItem } from './baseActions';
import createFetchHandler from './Creators/createFetchHandler';
import createHandleActions from './Creators/createHandleActions';
@@ -17,6 +18,7 @@ export const section = 'interactiveImport';
const booksSection = `${section}.books`;
const bookFilesSection = `${section}.bookFiles`;
let abortCurrentFetchRequest = null;
let abortCurrentRequest = null;
let currentIds = [];
@@ -32,15 +34,17 @@ export const defaultState = {
error: null,
items: [],
pendingChanges: {},
sortKey: 'quality',
sortDirection: sortDirections.DESCENDING,
sortKey: 'path',
sortDirection: sortDirections.ASCENDING,
secondarySortKey: 'path',
secondarySortDirection: sortDirections.ASCENDING,
recentFolders: [],
importMode: 'chooseImportMode',
sortPredicates: {
path: function(item, direction) {
const path = item.path;
return path.toLowerCase();
return naturalExpansion(path.toLowerCase());
},
author: function(item, direction) {
@@ -74,6 +78,8 @@ export const defaultState = {
};
export const persistState = [
'interactiveImport.sortKey',
'interactiveImport.sortDirection',
'interactiveImport.recentFolders',
'interactiveImport.importMode'
];
@@ -122,6 +128,11 @@ export const clearInteractiveImportBookFiles = createAction(CLEAR_INTERACTIVE_IM
// Action Handlers
export const actionHandlers = handleThunks({
[FETCH_INTERACTIVE_IMPORT_ITEMS]: function(getState, payload, dispatch) {
if (abortCurrentFetchRequest) {
abortCurrentFetchRequest();
abortCurrentFetchRequest = null;
}
if (!payload.downloadId && !payload.folder) {
dispatch(set({ section, error: { message: '`downloadId` or `folder` is required.' } }));
return;
@@ -129,12 +140,14 @@ export const actionHandlers = handleThunks({
dispatch(set({ section, isFetching: true }));
const promise = createAjaxRequest({
const { request, abortRequest } = createAjaxRequest({
url: '/manualimport',
data: payload
}).request;
});
promise.done((data) => {
abortCurrentFetchRequest = abortRequest;
request.done((data) => {
dispatch(batchActions([
update({ section, data }),
@@ -147,7 +160,11 @@ export const actionHandlers = handleThunks({
]));
});
promise.fail((xhr) => {
request.fail((xhr) => {
if (xhr.aborted) {
return;
}
dispatch(set({
section,
isFetching: false,

View File

@@ -71,6 +71,7 @@ function getInternalLink(source) {
function getTestLink(source, props) {
switch (source) {
case 'IndexerStatusCheck':
case 'IndexerLongTermStatusCheck':
return (
<SpinnerIconButton
name={icons.TEST}

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
@@ -9,8 +10,12 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import VirtualTable from 'Components/Table/VirtualTable';
import VirtualTableRow from 'Components/Table/VirtualTableRow';
import { align, icons, sortDirections } from 'Helpers/Props';
import { align, icons, kinds, sortDirections } from 'Helpers/Props';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import UnmappedFilesTableHeader from './UnmappedFilesTableHeader';
import UnmappedFilesTableRow from './UnmappedFilesTableRow';
@@ -23,10 +28,43 @@ class UnmappedFilesTable extends Component {
super(props, context);
this.state = {
scroller: null
scroller: null,
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {}
};
}
componentDidMount() {
this.setSelectedState();
}
componentDidUpdate(prevProps) {
const {
items,
sortKey,
sortDirection,
isDeleting,
deleteError
} = this.props;
if (sortKey !== prevProps.sortKey ||
sortDirection !== prevProps.sortDirection ||
hasDifferentItemsOrOrder(prevProps.items, items)
) {
this.setSelectedState();
}
const hasFinishedDeleting = prevProps.isDeleting &&
!isDeleting &&
!deleteError;
if (hasFinishedDeleting) {
this.onSelectAllChange({ value: false });
}
}
//
// Control
@@ -34,6 +72,68 @@ class UnmappedFilesTable extends Component {
this.setState({ scroller: ref });
};
getSelectedIds = () => {
if (this.state.allUnselected) {
return [];
}
return getSelectedIds(this.state.selectedState);
};
setSelectedState() {
const {
items
} = this.props;
const {
selectedState
} = this.state;
const newSelectedState = {};
items.forEach((file) => {
const isItemSelected = selectedState[file.id];
if (isItemSelected) {
newSelectedState[file.id] = isItemSelected;
} else {
newSelectedState[file.id] = false;
}
});
const selectedCount = getSelectedIds(newSelectedState).length;
const newStateCount = Object.keys(newSelectedState).length;
let isAllSelected = false;
let isAllUnselected = false;
if (selectedCount === 0) {
isAllUnselected = true;
} else if (selectedCount === newStateCount) {
isAllSelected = true;
}
this.setState({ selectedState: newSelectedState, allSelected: isAllSelected, allUnselected: isAllUnselected });
}
onSelectAllChange = ({ value }) => {
this.setState(selectAll(this.state.selectedState, value));
};
onSelectAllPress = () => {
this.onSelectAllChange({ value: !this.state.allSelected });
};
onSelectedChange = ({ id, value, shiftKey = false }) => {
this.setState((state) => {
return toggleSelected(state, this.props.items, id, value, shiftKey);
});
};
onDeleteUnmappedFilesPress = () => {
const selectedIds = this.getSelectedIds();
this.props.deleteUnmappedFiles(selectedIds);
};
rowRenderer = ({ key, rowIndex, style }) => {
const {
items,
@@ -41,6 +141,10 @@ class UnmappedFilesTable extends Component {
deleteUnmappedFile
} = this.props;
const {
selectedState
} = this.state;
const item = items[rowIndex];
return (
@@ -51,6 +155,8 @@ class UnmappedFilesTable extends Component {
<UnmappedFilesTableRow
key={item.id}
columns={columns}
isSelected={selectedState[item.id]}
onSelectedChange={this.onSelectedChange}
deleteUnmappedFile={deleteUnmappedFile}
{...item}
/>
@@ -63,6 +169,7 @@ class UnmappedFilesTable extends Component {
const {
isFetching,
isPopulated,
isDeleting,
error,
items,
columns,
@@ -72,13 +179,19 @@ class UnmappedFilesTable extends Component {
onSortPress,
isScanningFolders,
onAddMissingAuthorsPress,
deleteUnmappedFiles,
...otherProps
} = this.props;
const {
scroller
scroller,
allSelected,
allUnselected,
selectedState
} = this.state;
const selectedTrackFileIds = this.getSelectedIds();
return (
<PageContent title={translate('UnmappedFiles')}>
<PageToolbar>
@@ -90,6 +203,13 @@ class UnmappedFilesTable extends Component {
isSpinning={isScanningFolders}
onPress={onAddMissingAuthorsPress}
/>
<PageToolbarButton
label={translate('DeleteSelected')}
iconName={icons.DELETE}
isDisabled={selectedTrackFileIds.length === 0}
isSpinning={isDeleting}
onPress={this.onDeleteUnmappedFilesPress}
/>
</PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}>
@@ -117,9 +237,9 @@ class UnmappedFilesTable extends Component {
{
isPopulated && !error && !items.length &&
<div>
<Alert kind={kinds.INFO}>
Success! My work is done, all files on disk are matched to known books.
</div>
</Alert>
}
{
@@ -138,8 +258,12 @@ class UnmappedFilesTable extends Component {
sortDirection={sortDirection}
onTableOptionChange={onTableOptionChange}
onSortPress={onSortPress}
allSelected={allSelected}
allUnselected={allUnselected}
onSelectAllChange={this.onSelectAllChange}
/>
}
selectedState={selectedState}
sortKey={sortKey}
sortDirection={sortDirection}
/>
@@ -153,6 +277,8 @@ class UnmappedFilesTable extends Component {
UnmappedFilesTable.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
isDeleting: PropTypes.bool.isRequired,
deleteError: PropTypes.object,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
@@ -161,6 +287,7 @@ UnmappedFilesTable.propTypes = {
onTableOptionChange: PropTypes.func.isRequired,
onSortPress: PropTypes.func.isRequired,
deleteUnmappedFile: PropTypes.func.isRequired,
deleteUnmappedFiles: PropTypes.func.isRequired,
isScanningFolders: PropTypes.bool.isRequired,
onAddMissingAuthorsPress: PropTypes.func.isRequired
};

View File

@@ -5,7 +5,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import withCurrentPage from 'Components/withCurrentPage';
import { deleteBookFile, fetchBookFiles, setBookFilesSort, setBookFilesTableOption } from 'Store/Actions/bookFileActions';
import { deleteBookFile, deleteBookFiles, fetchBookFiles, setBookFilesSort, setBookFilesTableOption } from 'Store/Actions/bookFileActions';
import { executeCommand } from 'Store/Actions/commandActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
@@ -28,7 +28,9 @@ function createMapStateToProps() {
items,
...otherProps
} = bookFiles;
const unmappedFiles = _.filter(items, { bookId: 0 });
return {
items: unmappedFiles,
...otherProps,
@@ -57,6 +59,10 @@ function createMapDispatchToProps(dispatch, props) {
dispatch(deleteBookFile({ id }));
},
deleteUnmappedFiles(bookFileIds) {
dispatch(deleteBookFiles({ bookFileIds }));
},
onAddMissingAuthorsPress() {
dispatch(executeCommand({
name: commandNames.RESCAN_FOLDERS,
@@ -106,7 +112,8 @@ UnmappedFilesTableConnector.propTypes = {
onSortPress: PropTypes.func.isRequired,
onTableOptionChange: PropTypes.func.isRequired,
fetchUnmappedFiles: PropTypes.func.isRequired,
deleteUnmappedFile: PropTypes.func.isRequired
deleteUnmappedFile: PropTypes.func.isRequired,
deleteUnmappedFiles: PropTypes.func.isRequired
};
export default withCurrentPage(

View File

@@ -4,6 +4,7 @@ import IconButton from 'Components/Link/IconButton';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
import { icons } from 'Helpers/Props';
// import hasGrowableColumns from './hasGrowableColumns';
import styles from './UnmappedFilesTableHeader.css';
@@ -12,6 +13,9 @@ function UnmappedFilesTableHeader(props) {
const {
columns,
onTableOptionChange,
allSelected,
allUnselected,
onSelectAllChange,
...otherProps
} = props;
@@ -30,6 +34,17 @@ function UnmappedFilesTableHeader(props) {
return null;
}
if (name === 'select') {
return (
<VirtualTableSelectAllHeaderCell
key={name}
allSelected={allSelected}
allUnselected={allUnselected}
onSelectAllChange={onSelectAllChange}
/>
);
}
if (name === 'actions') {
return (
<VirtualTableHeaderCell
@@ -71,6 +86,9 @@ function UnmappedFilesTableHeader(props) {
UnmappedFilesTableHeader.propTypes = {
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
allSelected: PropTypes.bool.isRequired,
allUnselected: PropTypes.bool.isRequired,
onSelectAllChange: PropTypes.func.isRequired,
onTableOptionChange: PropTypes.func.isRequired
};

View File

@@ -20,3 +20,9 @@
flex: 0 0 100px;
}
.checkInput {
composes: input from '~Components/Form/CheckInput.css';
margin-top: 0;
}

View File

@@ -2,6 +2,7 @@
// Please do not change this file!
interface CssExports {
'actions': string;
'checkInput': string;
'dateAdded': string;
'path': string;
'quality': string;

View File

@@ -6,6 +6,7 @@ import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
import { icons, kinds } from 'Helpers/Props';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import formatBytes from 'Utilities/Number/formatBytes';
@@ -69,7 +70,9 @@ class UnmappedFilesTableRow extends Component {
size,
dateAdded,
quality,
columns
columns,
isSelected,
onSelectedChange
} = this.props;
const folder = path.substring(0, Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')));
@@ -93,6 +96,19 @@ class UnmappedFilesTableRow extends Component {
return null;
}
if (name === 'select') {
return (
<VirtualTableSelectCell
inputClassName={styles.checkInput}
id={id}
key={name}
isSelected={isSelected}
isDisabled={false}
onSelectedChange={onSelectedChange}
/>
);
}
if (name === 'path') {
return (
<VirtualTableRowCell
@@ -208,6 +224,8 @@ UnmappedFilesTableRow.propTypes = {
quality: PropTypes.object.isRequired,
dateAdded: PropTypes.string.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
isSelected: PropTypes.bool,
onSelectedChange: PropTypes.func.isRequired,
deleteUnmappedFile: PropTypes.func.isRequired
};

View File

@@ -0,0 +1,11 @@
const regex = /\d+/g;
function naturalExpansion(input) {
if (!input) {
return '';
}
return input.replace(regex, (n) => n.padStart(8, '0'));
}
export default naturalExpansion;

View File

@@ -1,9 +1,11 @@
const regex = /\b\w+/g;
function titleCase(input) {
if (!input) {
return '';
}
return input.replace(/\b\w+/g, (match) => {
return input.replace(regex, (match) => {
return match.charAt(0).toUpperCase() + match.substr(1).toLowerCase();
});
}

View File

@@ -25,7 +25,7 @@ export async function fetchTranslations(): Promise<boolean> {
export default function translate(
key: string,
tokens?: Record<string, string | number | boolean>
tokens: Record<string, string | number | boolean> = { appName: 'Readarr' }
) {
const translation = translations[key] || key;

View File

@@ -53,7 +53,7 @@ class CutoffUnmetConnector extends Component {
gotoCutoffUnmetFirstPage
} = this.props;
registerPagePopulator(this.repopulate, ['bookFileUpdated']);
registerPagePopulator(this.repopulate, ['bookFileUpdated', 'bookFileDeleted']);
if (useCurrentPage) {
fetchCutoffUnmet();

View File

@@ -50,7 +50,7 @@ class MissingConnector extends Component {
gotoMissingFirstPage
} = this.props;
registerPagePopulator(this.repopulate, ['bookFileUpdated']);
registerPagePopulator(this.repopulate, ['bookFileUpdated', 'bookFileDeleted']);
if (useCurrentPage) {
fetchMissing();

View File

@@ -4,6 +4,8 @@ import { render } from 'react-dom';
import createAppStore from 'Store/createAppStore';
import App from './App/App';
import 'Diag/ConsoleApi';
export async function bootstrap() {
const history = createBrowserHistory();
const store = createAppStore(history);

View File

@@ -30,7 +30,7 @@
"@fortawesome/free-regular-svg-icons": "6.4.0",
"@fortawesome/free-solid-svg-icons": "6.4.0",
"@fortawesome/react-fontawesome": "0.2.0",
"@microsoft/signalr": "6.0.16",
"@microsoft/signalr": "6.0.25",
"@sentry/browser": "7.51.2",
"@sentry/integrations": "7.51.2",
"@types/node": "18.16.16",
@@ -83,30 +83,28 @@
"redux-localstorage": "0.4.1",
"redux-thunk": "2.3.0",
"reselect": "4.1.8",
"stacktrace-js": "2.0.2",
"typescript": "4.9.5"
},
"devDependencies": {
"@babel/core": "7.22.9",
"@babel/eslint-parser": "7.22.9",
"@babel/plugin-proposal-class-properties": "7.18.6",
"@babel/core": "7.22.11",
"@babel/eslint-parser": "7.22.11",
"@babel/plugin-proposal-export-default-from": "7.22.5",
"@babel/plugin-proposal-export-namespace-from": "7.18.9",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6",
"@babel/plugin-proposal-optional-chaining": "7.21.0",
"@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/preset-env": "7.22.9",
"@babel/preset-env": "7.22.15",
"@babel/preset-react": "7.22.5",
"@babel/preset-typescript": "7.22.5",
"@babel/preset-typescript": "7.22.11",
"@types/lodash": "4.14.197",
"@types/react-lazyload": "3.2.1",
"@types/redux-actions": "2.6.2",
"@typescript-eslint/eslint-plugin": "6.0.0",
"@typescript-eslint/parser": "6.0.0",
"@typescript-eslint/eslint-plugin": "6.5.0",
"@typescript-eslint/parser": "6.5.0",
"autoprefixer": "10.4.14",
"babel-loader": "9.1.3",
"babel-plugin-inline-classnames": "2.0.1",
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
"core-js": "3.31.1",
"css-loader": "6.7.3",
"core-js": "3.32.1",
"css-loader": "6.8.1",
"css-modules-typescript-loader": "4.0.1",
"eslint": "8.44.0",
"eslint-config-prettier": "8.8.0",
@@ -120,10 +118,10 @@
"file-loader": "6.2.0",
"filemanager-webpack-plugin": "8.0.0",
"fork-ts-checker-webpack-plugin": "8.0.0",
"html-webpack-plugin": "5.5.1",
"html-webpack-plugin": "5.5.3",
"loader-utils": "^3.2.1",
"mini-css-extract-plugin": "2.7.5",
"postcss": "8.4.23",
"mini-css-extract-plugin": "2.7.6",
"postcss": "8.4.31",
"postcss-color-function": "4.1.0",
"postcss-loader": "7.3.0",
"postcss-mixins": "9.0.4",
@@ -135,14 +133,14 @@
"rimraf": "4.4.1",
"run-sequence": "2.2.1",
"streamqueue": "1.1.2",
"style-loader": "3.3.2",
"stylelint": "15.10.1",
"style-loader": "3.3.3",
"stylelint": "15.10.3",
"stylelint-order": "6.0.3",
"terser-webpack-plugin": "5.3.9",
"ts-loader": "9.4.3",
"ts-loader": "9.4.4",
"typescript-plugin-css-modules": "5.0.1",
"url-loader": "4.1.1",
"webpack": "5.88.1",
"webpack": "5.88.2",
"webpack-cli": "5.1.4",
"webpack-livereload-plugin": "3.0.2",
"worker-loader": "3.0.8"

View File

@@ -4,10 +4,11 @@
<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.123" />
<PackageVersion Include="DryIoc.dll" Version="5.4.0" />
<PackageVersion Include="DryIoc.dll" Version="5.4.3" />
<PackageVersion Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0" />
<PackageVersion Include="Equ" Version="2.3.0" />
<PackageVersion Include="FluentAssertions" Version="5.10.3" />
<PackageVersion Include="Polly" Version="8.2.0" />
<PackageVersion Include="Servarr.FluentMigrator.Runner" Version="3.3.2.9" />
<PackageVersion Include="Servarr.FluentMigrator.Runner.SQLite" Version="3.3.2.9" />
<PackageVersion Include="Servarr.FluentMigrator.Runner.Postgres" Version="3.3.2.9" />
@@ -16,11 +17,11 @@
<PackageVersion Include="ImpromptuInterface" Version="7.0.1" />
<PackageVersion Include="LazyCache" Version="2.4.0" />
<PackageVersion Include="Mailkit" Version="3.6.0" />
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.16" />
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.25" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.1" />
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="6.0.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageVersion Include="Microsoft.Win32.Registry" Version="5.0.0" />
@@ -32,7 +33,7 @@
<PackageVersion Include="NLog.Extensions.Logging" Version="5.2.3" />
<PackageVersion Include="NLog" Version="5.1.4" />
<PackageVersion Include="NLog.Targets.Syslog" Version="7.0.0" />
<PackageVersion Include="Npgsql" Version="7.0.4" />
<PackageVersion Include="Npgsql" Version="7.0.6" />
<PackageVersion Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageVersion Include="NUnit" Version="3.13.3" />
<PackageVersion Include="NunitXml.TestLogger" Version="3.0.117" />
@@ -43,7 +44,7 @@
<PackageVersion Include="Selenium.WebDriver.ChromeDriver" Version="91.0.4472.10100" />
<PackageVersion Include="Sentry" Version="3.31.0" />
<PackageVersion Include="SharpZipLib" Version="1.4.2" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.0.1" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.0.2" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.1.118" />
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.5.0" />
<PackageVersion Include="System.Buffers" Version="4.5.1" />
@@ -57,10 +58,10 @@
<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.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.7" />
<PackageVersion Include="System.Text.Json" Version="6.0.9" />
<PackageVersion Include="System.ValueTuple" Version="4.5.0" />
<PackageVersion Include="TagLibSharp-Lidarr" Version="2.2.0.19" />
</ItemGroup>
</Project>
</Project>

View File

@@ -1,6 +1,9 @@
using System.Collections.Generic;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Localization;
using NzbDrone.Test.Common;
using Readarr.Http.ClientSchema;
@@ -9,6 +12,16 @@ namespace NzbDrone.Api.Test.ClientSchemaTests
[TestFixture]
public class SchemaBuilderFixture : TestBase
{
[SetUp]
public void Setup()
{
Mocker.GetMock<ILocalizationService>()
.Setup(s => s.GetLocalizedString(It.IsAny<string>(), It.IsAny<Dictionary<string, object>>()))
.Returns<string, Dictionary<string, object>>((s, d) => s);
SchemaBuilder.Initialize(Mocker.Container);
}
[Test]
public void should_return_field_for_every_property()
{

View File

@@ -6,6 +6,7 @@ using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Moq;
using NLog;
@@ -114,21 +115,21 @@ namespace NzbDrone.Common.Test.Http
}
[Test]
public void should_execute_simple_get()
public async Task should_execute_simple_get()
{
var request = new HttpRequest($"https://{_httpBinHost}/get");
var response = Subject.Execute(request);
var response = await Subject.ExecuteAsync(request);
response.Content.Should().NotBeNullOrWhiteSpace();
}
[Test]
public void should_execute_https_get()
public async Task should_execute_https_get()
{
var request = new HttpRequest($"https://{_httpBinHost}/get");
var response = Subject.Execute(request);
var response = await Subject.ExecuteAsync(request);
response.Content.Should().NotBeNullOrWhiteSpace();
}
@@ -140,47 +141,47 @@ namespace NzbDrone.Common.Test.Http
Mocker.GetMock<IConfigService>().SetupGet(x => x.CertificateValidation).Returns(validationType);
var request = new HttpRequest($"https://expired.badssl.com");
Assert.Throws<HttpRequestException>(() => Subject.Execute(request));
Assert.ThrowsAsync<HttpRequestException>(async () => await Subject.ExecuteAsync(request));
ExceptionVerification.ExpectedErrors(1);
}
[Test]
public void bad_ssl_should_pass_if_remote_validation_disabled()
public async Task bad_ssl_should_pass_if_remote_validation_disabled()
{
Mocker.GetMock<IConfigService>().SetupGet(x => x.CertificateValidation).Returns(CertificateValidationType.Disabled);
var request = new HttpRequest($"https://expired.badssl.com");
Subject.Execute(request);
await Subject.ExecuteAsync(request);
ExceptionVerification.ExpectedErrors(0);
}
[Test]
public void should_execute_typed_get()
public async Task should_execute_typed_get()
{
var request = new HttpRequest($"https://{_httpBinHost}/get?test=1");
var response = Subject.Get<HttpBinResource>(request);
var response = await Subject.GetAsync<HttpBinResource>(request);
response.Resource.Url.EndsWith("/get?test=1");
response.Resource.Args.Should().Contain("test", "1");
}
[Test]
public void should_execute_simple_post()
public async Task should_execute_simple_post()
{
var message = "{ my: 1 }";
var request = new HttpRequest($"https://{_httpBinHost}/post");
request.SetContent(message);
var response = Subject.Post<HttpBinResource>(request);
var response = await Subject.PostAsync<HttpBinResource>(request);
response.Resource.Data.Should().Be(message);
}
[Test]
public void should_execute_post_with_content_type()
public async Task should_execute_post_with_content_type()
{
var message = "{ my: 1 }";
@@ -188,17 +189,16 @@ namespace NzbDrone.Common.Test.Http
request.SetContent(message);
request.Headers.ContentType = "application/json";
var response = Subject.Post<HttpBinResource>(request);
var response = await Subject.PostAsync<HttpBinResource>(request);
response.Resource.Data.Should().Be(message);
}
[Test]
public void should_execute_get_using_gzip()
public async Task should_execute_get_using_gzip()
{
var request = new HttpRequest($"https://{_httpBinHost}/gzip");
var response = Subject.Get<HttpBinResource>(request);
var response = await Subject.GetAsync<HttpBinResource>(request);
response.Resource.Headers["Accept-Encoding"].ToString().Should().Contain("gzip");
@@ -208,11 +208,10 @@ namespace NzbDrone.Common.Test.Http
[Test]
[Platform(Exclude = "MacOsX", Reason = "Azure agent update prevents brotli on OSX")]
public void should_execute_get_using_brotli()
public async Task should_execute_get_using_brotli()
{
var request = new HttpRequest($"https://{_httpBinHost}/brotli");
var response = Subject.Get<HttpBinResource>(request);
var response = await Subject.GetAsync<HttpBinResource>(request);
response.Resource.Headers["Accept-Encoding"].ToString().Should().Contain("br");
@@ -230,7 +229,7 @@ namespace NzbDrone.Common.Test.Http
{
var request = new HttpRequest($"https://{_httpBinHost}/status/{statusCode}");
var exception = Assert.Throws<HttpException>(() => Subject.Get<HttpBinResource>(request));
var exception = Assert.ThrowsAsync<HttpException>(async () => await Subject.GetAsync<HttpBinResource>(request));
((int)exception.Response.StatusCode).Should().Be(statusCode);
@@ -243,7 +242,7 @@ namespace NzbDrone.Common.Test.Http
var request = new HttpRequest($"https://{_httpBinHost}/status/{HttpStatusCode.NotFound}");
request.SuppressHttpErrorStatusCodes = new[] { HttpStatusCode.NotFound };
var exception = Assert.Throws<HttpException>(() => Subject.Get<HttpBinResource>(request));
Assert.ThrowsAsync<HttpException>(async () => await Subject.GetAsync<HttpBinResource>(request));
ExceptionVerification.IgnoreWarns();
}
@@ -253,7 +252,7 @@ namespace NzbDrone.Common.Test.Http
{
var request = new HttpRequest($"https://{_httpBinHost}/status/{HttpStatusCode.NotFound}");
var exception = Assert.Throws<HttpException>(() => Subject.Get<HttpBinResource>(request));
var exception = Assert.ThrowsAsync<HttpException>(async () => await Subject.GetAsync<HttpBinResource>(request));
ExceptionVerification.ExpectedWarns(1);
}
@@ -264,28 +263,28 @@ namespace NzbDrone.Common.Test.Http
var request = new HttpRequest($"https://{_httpBinHost}/status/{HttpStatusCode.NotFound}");
request.LogHttpError = false;
var exception = Assert.Throws<HttpException>(() => Subject.Get<HttpBinResource>(request));
Assert.ThrowsAsync<HttpException>(async () => await Subject.GetAsync<HttpBinResource>(request));
ExceptionVerification.ExpectedWarns(0);
}
[Test]
public void should_not_follow_redirects_when_not_in_production()
public async Task should_not_follow_redirects_when_not_in_production()
{
var request = new HttpRequest($"https://{_httpBinHost}/redirect/1");
Subject.Get(request);
await Subject.GetAsync(request);
ExceptionVerification.ExpectedErrors(1);
}
[Test]
public void should_follow_redirects()
public async Task should_follow_redirects()
{
var request = new HttpRequest($"https://{_httpBinHost}/redirect/1");
request.AllowAutoRedirect = true;
var response = Subject.Get(request);
var response = await Subject.GetAsync(request);
response.StatusCode.Should().Be(HttpStatusCode.OK);
@@ -293,12 +292,12 @@ namespace NzbDrone.Common.Test.Http
}
[Test]
public void should_not_follow_redirects()
public async Task should_not_follow_redirects()
{
var request = new HttpRequest($"https://{_httpBinHost}/redirect/1");
request.AllowAutoRedirect = false;
var response = Subject.Get(request);
var response = await Subject.GetAsync(request);
response.StatusCode.Should().Be(HttpStatusCode.Found);
@@ -306,14 +305,14 @@ namespace NzbDrone.Common.Test.Http
}
[Test]
public void should_follow_redirects_to_https()
public async Task should_follow_redirects_to_https()
{
var request = new HttpRequestBuilder($"https://{_httpBinHost}/redirect-to")
.AddQueryParam("url", $"https://readarr.com/")
.Build();
request.AllowAutoRedirect = true;
var response = Subject.Get(request);
var response = await Subject.GetAsync(request);
response.StatusCode.Should().Be(HttpStatusCode.OK);
response.Content.Should().Contain("Readarr");
@@ -327,17 +326,17 @@ namespace NzbDrone.Common.Test.Http
var request = new HttpRequest($"https://{_httpBinHost}/redirect/6");
request.AllowAutoRedirect = true;
Assert.Throws<WebException>(() => Subject.Get(request));
Assert.ThrowsAsync<WebException>(async () => await Subject.GetAsync(request));
ExceptionVerification.ExpectedErrors(0);
}
[Test]
public void should_send_user_agent()
public async Task should_send_user_agent()
{
var request = new HttpRequest($"https://{_httpBinHost}/get");
var response = Subject.Get<HttpBinResource>(request);
var response = await Subject.GetAsync<HttpBinResource>(request);
response.Resource.Headers.Should().ContainKey("User-Agent");
@@ -347,24 +346,24 @@ namespace NzbDrone.Common.Test.Http
}
[TestCase("Accept", "text/xml, text/rss+xml, application/rss+xml")]
public void should_send_headers(string header, string value)
public async Task should_send_headers(string header, string value)
{
var request = new HttpRequest($"https://{_httpBinHost}/get");
request.Headers.Add(header, value);
var response = Subject.Get<HttpBinResource>(request);
var response = await Subject.GetAsync<HttpBinResource>(request);
response.Resource.Headers[header].ToString().Should().Be(value);
}
[Test]
public void should_download_file()
public async Task should_download_file()
{
var file = GetTempFilePath();
var url = "https://readarr.com/img/slider/artistdetails.png";
Subject.DownloadFile(url, file);
await Subject.DownloadFileAsync(url, file);
var fileInfo = new FileInfo(file);
fileInfo.Exists.Should().BeTrue();
@@ -372,7 +371,7 @@ namespace NzbDrone.Common.Test.Http
}
[Test]
public void should_download_file_with_redirect()
public async Task should_download_file_with_redirect()
{
var file = GetTempFilePath();
@@ -380,7 +379,7 @@ namespace NzbDrone.Common.Test.Http
.AddQueryParam("url", $"https://readarr.com/img/slider/artistdetails.png")
.Build();
Subject.DownloadFile(request.Url.FullUri, file);
await Subject.DownloadFileAsync(request.Url.FullUri, file);
ExceptionVerification.ExpectedErrors(0);
@@ -394,7 +393,7 @@ namespace NzbDrone.Common.Test.Http
{
var file = GetTempFilePath();
Assert.Throws<HttpException>(() => Subject.DownloadFile("https://download.sonarr.tv/wrongpath", file));
Assert.ThrowsAsync<HttpException>(async () => await Subject.DownloadFileAsync("https://download.sonarr.tv/wrongpath", file));
File.Exists(file).Should().BeFalse();
@@ -402,7 +401,7 @@ namespace NzbDrone.Common.Test.Http
}
[Test]
public void should_not_write_redirect_content_to_stream()
public async Task should_not_write_redirect_content_to_stream()
{
var file = GetTempFilePath();
@@ -412,7 +411,7 @@ namespace NzbDrone.Common.Test.Http
request.AllowAutoRedirect = false;
request.ResponseStream = fileStream;
var response = Subject.Get(request);
var response = await Subject.GetAsync(request);
response.StatusCode.Should().Be(HttpStatusCode.Moved);
}
@@ -427,12 +426,12 @@ namespace NzbDrone.Common.Test.Http
}
[Test]
public void should_send_cookie()
public async Task should_send_cookie()
{
var request = new HttpRequest($"https://{_httpBinHost}/get");
request.Cookies["my"] = "cookie";
var response = Subject.Get<HttpBinResource>(request);
var response = await Subject.GetAsync<HttpBinResource>(request);
response.Resource.Headers.Should().ContainKey("Cookie");
@@ -461,13 +460,13 @@ namespace NzbDrone.Common.Test.Http
}
[Test]
public void should_preserve_cookie_during_session()
public async Task should_preserve_cookie_during_session()
{
GivenOldCookie();
var request = new HttpRequest($"https://{_httpBinHost2}/get");
var response = Subject.Get<HttpBinResource>(request);
var response = await Subject.GetAsync<HttpBinResource>(request);
response.Resource.Headers.Should().ContainKey("Cookie");
@@ -477,30 +476,30 @@ namespace NzbDrone.Common.Test.Http
}
[Test]
public void should_not_send_cookie_to_other_host()
public async Task should_not_send_cookie_to_other_host()
{
GivenOldCookie();
var request = new HttpRequest($"https://{_httpBinHost}/get");
var response = Subject.Get<HttpBinResource>(request);
var response = await Subject.GetAsync<HttpBinResource>(request);
response.Resource.Headers.Should().NotContainKey("Cookie");
}
[Test]
public void should_not_store_request_cookie()
public async Task should_not_store_request_cookie()
{
var requestGet = new HttpRequest($"https://{_httpBinHost}/get");
requestGet.Cookies.Add("my", "cookie");
requestGet.AllowAutoRedirect = false;
requestGet.StoreRequestCookie = false;
requestGet.StoreResponseCookie = false;
var responseGet = Subject.Get<HttpBinResource>(requestGet);
var responseGet = await Subject.GetAsync<HttpBinResource>(requestGet);
var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies");
requestCookies.AllowAutoRedirect = false;
var responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
var responseCookies = await Subject.GetAsync<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().BeEmpty();
@@ -508,18 +507,18 @@ namespace NzbDrone.Common.Test.Http
}
[Test]
public void should_store_request_cookie()
public async Task should_store_request_cookie()
{
var requestGet = new HttpRequest($"https://{_httpBinHost}/get");
requestGet.Cookies.Add("my", "cookie");
requestGet.AllowAutoRedirect = false;
requestGet.StoreRequestCookie.Should().BeTrue();
requestGet.StoreResponseCookie = false;
var responseGet = Subject.Get<HttpBinResource>(requestGet);
var responseGet = await Subject.GetAsync<HttpBinResource>(requestGet);
var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies");
requestCookies.AllowAutoRedirect = false;
var responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
var responseCookies = await Subject.GetAsync<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
@@ -527,7 +526,7 @@ namespace NzbDrone.Common.Test.Http
}
[Test]
public void should_delete_request_cookie()
public async Task should_delete_request_cookie()
{
var requestDelete = new HttpRequest($"https://{_httpBinHost}/cookies/delete?my");
requestDelete.Cookies.Add("my", "cookie");
@@ -536,13 +535,13 @@ namespace NzbDrone.Common.Test.Http
requestDelete.StoreResponseCookie = false;
// Delete and redirect since that's the only way to check the internal temporary cookie container
var responseCookies = Subject.Get<HttpCookieResource>(requestDelete);
var responseCookies = await Subject.GetAsync<HttpCookieResource>(requestDelete);
responseCookies.Resource.Cookies.Should().BeEmpty();
}
[Test]
public void should_clear_request_cookie()
public async Task should_clear_request_cookie()
{
var requestSet = new HttpRequest($"https://{_httpBinHost}/cookies");
requestSet.Cookies.Add("my", "cookie");
@@ -550,7 +549,7 @@ namespace NzbDrone.Common.Test.Http
requestSet.StoreRequestCookie = true;
requestSet.StoreResponseCookie = false;
var responseSet = Subject.Get<HttpCookieResource>(requestSet);
var responseSet = await Subject.GetAsync<HttpCookieResource>(requestSet);
var requestClear = new HttpRequest($"https://{_httpBinHost}/cookies");
requestClear.Cookies.Add("my", null);
@@ -558,24 +557,24 @@ namespace NzbDrone.Common.Test.Http
requestClear.StoreRequestCookie = true;
requestClear.StoreResponseCookie = false;
var responseClear = Subject.Get<HttpCookieResource>(requestClear);
var responseClear = await Subject.GetAsync<HttpCookieResource>(requestClear);
responseClear.Resource.Cookies.Should().BeEmpty();
}
[Test]
public void should_not_store_response_cookie()
public async Task should_not_store_response_cookie()
{
var requestSet = new HttpRequest($"https://{_httpBinHost}/cookies/set?my=cookie");
requestSet.AllowAutoRedirect = false;
requestSet.StoreRequestCookie = false;
requestSet.StoreResponseCookie.Should().BeFalse();
var responseSet = Subject.Get(requestSet);
var responseSet = await Subject.GetAsync(requestSet);
var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies");
var responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
var responseCookies = await Subject.GetAsync<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().BeEmpty();
@@ -583,18 +582,18 @@ namespace NzbDrone.Common.Test.Http
}
[Test]
public void should_store_response_cookie()
public async Task should_store_response_cookie()
{
var requestSet = new HttpRequest($"https://{_httpBinHost}/cookies/set?my=cookie");
requestSet.AllowAutoRedirect = false;
requestSet.StoreRequestCookie = false;
requestSet.StoreResponseCookie = true;
var responseSet = Subject.Get(requestSet);
var responseSet = await Subject.GetAsync(requestSet);
var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies");
var responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
var responseCookies = await Subject.GetAsync<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
@@ -602,13 +601,13 @@ namespace NzbDrone.Common.Test.Http
}
[Test]
public void should_temp_store_response_cookie()
public async Task should_temp_store_response_cookie()
{
var requestSet = new HttpRequest($"https://{_httpBinHost}/cookies/set?my=cookie");
requestSet.AllowAutoRedirect = true;
requestSet.StoreRequestCookie = false;
requestSet.StoreResponseCookie.Should().BeFalse();
var responseSet = Subject.Get<HttpCookieResource>(requestSet);
var responseSet = await Subject.GetAsync<HttpCookieResource>(requestSet);
// Set and redirect since that's the only way to check the internal temporary cookie container
responseSet.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
@@ -617,7 +616,7 @@ namespace NzbDrone.Common.Test.Http
}
[Test]
public void should_overwrite_response_cookie()
public async Task should_overwrite_response_cookie()
{
var requestSet = new HttpRequest($"https://{_httpBinHost}/cookies/set?my=cookie");
requestSet.Cookies.Add("my", "oldcookie");
@@ -625,11 +624,11 @@ namespace NzbDrone.Common.Test.Http
requestSet.StoreRequestCookie = false;
requestSet.StoreResponseCookie = true;
var responseSet = Subject.Get(requestSet);
var responseSet = await Subject.GetAsync(requestSet);
var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies");
var responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
var responseCookies = await Subject.GetAsync<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
@@ -637,7 +636,7 @@ namespace NzbDrone.Common.Test.Http
}
[Test]
public void should_overwrite_temp_response_cookie()
public async Task should_overwrite_temp_response_cookie()
{
var requestSet = new HttpRequest($"https://{_httpBinHost}/cookies/set?my=cookie");
requestSet.Cookies.Add("my", "oldcookie");
@@ -645,13 +644,13 @@ namespace NzbDrone.Common.Test.Http
requestSet.StoreRequestCookie = true;
requestSet.StoreResponseCookie = false;
var responseSet = Subject.Get<HttpCookieResource>(requestSet);
var responseSet = await Subject.GetAsync<HttpCookieResource>(requestSet);
responseSet.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies");
var responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
var responseCookies = await Subject.GetAsync<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "oldcookie");
@@ -659,14 +658,14 @@ namespace NzbDrone.Common.Test.Http
}
[Test]
public void should_not_delete_response_cookie()
public async Task should_not_delete_response_cookie()
{
var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies");
requestCookies.Cookies.Add("my", "cookie");
requestCookies.AllowAutoRedirect = false;
requestCookies.StoreRequestCookie = true;
requestCookies.StoreResponseCookie = false;
var responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
var responseCookies = await Subject.GetAsync<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
@@ -675,13 +674,13 @@ namespace NzbDrone.Common.Test.Http
requestDelete.StoreRequestCookie = false;
requestDelete.StoreResponseCookie = false;
var responseDelete = Subject.Get(requestDelete);
var responseDelete = await Subject.GetAsync(requestDelete);
requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies");
requestCookies.StoreRequestCookie = false;
requestCookies.StoreResponseCookie = false;
responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
responseCookies = await Subject.GetAsync<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
@@ -689,14 +688,14 @@ namespace NzbDrone.Common.Test.Http
}
[Test]
public void should_delete_response_cookie()
public async Task should_delete_response_cookie()
{
var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies");
requestCookies.Cookies.Add("my", "cookie");
requestCookies.AllowAutoRedirect = false;
requestCookies.StoreRequestCookie = true;
requestCookies.StoreResponseCookie = false;
var responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
var responseCookies = await Subject.GetAsync<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
@@ -705,13 +704,13 @@ namespace NzbDrone.Common.Test.Http
requestDelete.StoreRequestCookie = false;
requestDelete.StoreResponseCookie = true;
var responseDelete = Subject.Get(requestDelete);
var responseDelete = await Subject.GetAsync(requestDelete);
requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies");
requestCookies.StoreRequestCookie = false;
requestCookies.StoreResponseCookie = false;
responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
responseCookies = await Subject.GetAsync<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().BeEmpty();
@@ -719,14 +718,14 @@ namespace NzbDrone.Common.Test.Http
}
[Test]
public void should_delete_temp_response_cookie()
public async Task should_delete_temp_response_cookie()
{
var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies");
requestCookies.Cookies.Add("my", "cookie");
requestCookies.AllowAutoRedirect = false;
requestCookies.StoreRequestCookie = true;
requestCookies.StoreResponseCookie = false;
var responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
var responseCookies = await Subject.GetAsync<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
@@ -734,7 +733,7 @@ namespace NzbDrone.Common.Test.Http
requestDelete.AllowAutoRedirect = true;
requestDelete.StoreRequestCookie = false;
requestDelete.StoreResponseCookie = false;
var responseDelete = Subject.Get<HttpCookieResource>(requestDelete);
var responseDelete = await Subject.GetAsync<HttpCookieResource>(requestDelete);
responseDelete.Resource.Cookies.Should().BeEmpty();
@@ -752,13 +751,13 @@ namespace NzbDrone.Common.Test.Http
{
var request = new HttpRequest($"https://{_httpBinHost}/status/429");
Assert.Throws<TooManyRequestsException>(() => Subject.Get(request));
Assert.ThrowsAsync<TooManyRequestsException>(async () => await Subject.GetAsync(request));
ExceptionVerification.IgnoreWarns();
}
[Test]
public void should_call_interceptor()
public async Task should_call_interceptor()
{
Mocker.SetConstant<IEnumerable<IHttpRequestInterceptor>>(new[] { Mocker.GetMock<IHttpRequestInterceptor>().Object });
@@ -772,7 +771,7 @@ namespace NzbDrone.Common.Test.Http
var request = new HttpRequest($"https://{_httpBinHost}/get");
Subject.Get(request);
await Subject.GetAsync(request);
Mocker.GetMock<IHttpRequestInterceptor>()
.Verify(v => v.PreRequest(It.IsAny<HttpRequest>()), Times.Once());
@@ -783,7 +782,7 @@ namespace NzbDrone.Common.Test.Http
[TestCase("en-US")]
[TestCase("es-ES")]
public void should_parse_malformed_cloudflare_cookie(string culture)
public async Task should_parse_malformed_cloudflare_cookie(string culture)
{
var origCulture = Thread.CurrentThread.CurrentCulture;
Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo(culture);
@@ -799,11 +798,11 @@ namespace NzbDrone.Common.Test.Http
requestSet.AllowAutoRedirect = false;
requestSet.StoreResponseCookie = true;
var responseSet = Subject.Get(requestSet);
var responseSet = await Subject.GetAsync(requestSet);
var request = new HttpRequest($"https://{_httpBinHost}/get");
var response = Subject.Get<HttpBinResource>(request);
var response = await Subject.GetAsync<HttpBinResource>(request);
response.Resource.Headers.Should().ContainKey("Cookie");
@@ -821,7 +820,7 @@ namespace NzbDrone.Common.Test.Http
}
[TestCase("lang_code=en; expires=Wed, 23-Dec-2026 18:09:14 GMT; Max-Age=31536000; path=/; domain=.abc.com")]
public void should_reject_malformed_domain_cookie(string malformedCookie)
public async Task should_reject_malformed_domain_cookie(string malformedCookie)
{
try
{
@@ -831,11 +830,11 @@ namespace NzbDrone.Common.Test.Http
requestSet.AllowAutoRedirect = false;
requestSet.StoreResponseCookie = true;
var responseSet = Subject.Get(requestSet);
var responseSet = await Subject.GetAsync(requestSet);
var request = new HttpRequest($"https://{_httpBinHost}/get");
var response = Subject.Get<HttpBinResource>(request);
var response = await Subject.GetAsync<HttpBinResource>(request);
response.Resource.Headers.Should().NotContainKey("Cookie");
@@ -847,12 +846,12 @@ namespace NzbDrone.Common.Test.Http
}
[Test]
public void should_correctly_use_basic_auth_with_basic_network_credential()
public async Task should_correctly_use_basic_auth()
{
var request = new HttpRequest($"https://{_httpBinHost}/basic-auth/username/password");
request.Credentials = new BasicNetworkCredential("username", "password");
var response = Subject.Execute(request);
var response = await Subject.ExecuteAsync(request);
response.StatusCode.Should().Be(HttpStatusCode.OK);
}

View File

@@ -70,15 +70,15 @@ namespace NzbDrone.Common.Test.InstrumentationTests
[TestCase(@"[Info] MigrationController: *** Migrating Database=readarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;token=mySecret;Enlist=False&username=mySecret;mypassword=mySecret;mypass=shouldkeep1;test_token=mySecret;password=123%@%_@!#^#@;use_password=mySecret;get_token=shouldkeep2;usetoken=shouldkeep3;passwrd=mySecret;")]
// Announce URLs (passkeys) Magnet & Tracker
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2f9pr04sg601233210imaveql2tyu8xyui%2fannounce""}")]
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2ftracker.php%2f9pr04sg601233210imaveql2tyu8xyui%2fannounce""}")]
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2fannounce%2f9pr04sg601233210imaveql2tyu8xyui""}")]
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2fannounce.php%3fpasskey%3d9pr04sg601233210imaveql2tyu8xyui""}")]
[TestCase(@"tracker"":""https://xxx.yyy/9pr04sg601233210imaveql2tyu8xyui/announce""}")]
[TestCase(@"tracker"":""https://xxx.yyy/tracker.php/9pr04sg601233210imaveql2tyu8xyui/announce""}")]
[TestCase(@"tracker"":""https://xxx.yyy/announce/9pr04sg601233210imaveql2tyu8xyui""}")]
[TestCase(@"tracker"":""https://xxx.yyy/announce.php?passkey=9pr04sg601233210imaveql2tyu8xyui""}")]
[TestCase(@"tracker"":""http://xxx.yyy/announce.php?passkey=9pr04sg601233210imaveql2tyu8xyui"",""info"":""http://xxx.yyy/info?a=b""")]
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2f9pr04sg601233210IMAveQL2tyu8xyui%2fannounce""}")]
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2ftracker.php%2f9pr04sg601233210IMAveQL2tyu8xyui%2fannounce""}")]
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2fannounce%2f9pr04sg601233210IMAveQL2tyu8xyui""}")]
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2fannounce.php%3fpasskey%3d9pr04sg601233210IMAveQL2tyu8xyui""}")]
[TestCase(@"tracker"":""https://xxx.yyy/9pr04sg601233210IMAveQL2tyu8xyui/announce""}")]
[TestCase(@"tracker"":""https://xxx.yyy/tracker.php/9pr04sg601233210IMAveQL2tyu8xyui/announce""}")]
[TestCase(@"tracker"":""https://xxx.yyy/announce/9pr04sg601233210IMAveQL2tyu8xyui""}")]
[TestCase(@"tracker"":""https://xxx.yyy/announce.php?passkey=9pr04sg601233210IMAveQL2tyu8xyui""}")]
[TestCase(@"tracker"":""http://xxx.yyy/announce.php?passkey=9pr04sg601233210IMAveQL2tyu8xyui"",""info"":""http://xxx.yyy/info?a=b""")]
// Notifiarr
[TestCase(@"https://xxx.yyy/api/v1/notification/readarr/9pr04sg6-0123-3210-imav-eql2tyu8xyui")]

View File

@@ -162,7 +162,7 @@ namespace NzbDrone.Common.Extensions
{
if (text.IsNullOrWhiteSpace())
{
throw new ArgumentNullException("text");
throw new ArgumentNullException(nameof(text));
}
return text.IndexOfAny(Path.GetInvalidPathChars()) >= 0;

View File

@@ -1,9 +1,10 @@
using System.Net;
using System.Threading.Tasks;
namespace NzbDrone.Common.Http.Dispatchers
{
public interface IHttpDispatcher
{
HttpResponse GetResponse(HttpRequest request, CookieContainer cookies);
Task<HttpResponse> GetResponseAsync(HttpRequest request, CookieContainer cookies);
}
}

View File

@@ -44,9 +44,13 @@ namespace NzbDrone.Common.Http.Dispatchers
_credentialCache = cacheManager.GetCache<CredentialCache>(typeof(ManagedHttpDispatcher), "credentialcache");
}
public HttpResponse GetResponse(HttpRequest request, CookieContainer cookies)
public async Task<HttpResponse> GetResponseAsync(HttpRequest request, CookieContainer cookies)
{
var requestMessage = new HttpRequestMessage(request.Method, (Uri)request.Url);
var requestMessage = new HttpRequestMessage(request.Method, (Uri)request.Url)
{
Version = HttpVersion.Version20,
VersionPolicy = HttpVersionPolicy.RequestVersionOrLower
};
requestMessage.Headers.UserAgent.ParseAdd(_userAgentBuilder.GetUserAgent(request.UseSimplifiedUserAgent));
requestMessage.Headers.ConnectionClose = !request.ConnectionKeepAlive;
@@ -99,7 +103,7 @@ namespace NzbDrone.Common.Http.Dispatchers
var httpClient = GetClient(request.Url);
using var responseMessage = httpClient.Send(requestMessage, HttpCompletionOption.ResponseHeadersRead, cts.Token);
using var responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cts.Token);
{
byte[] data = null;
@@ -107,11 +111,11 @@ namespace NzbDrone.Common.Http.Dispatchers
{
if (request.ResponseStream != null && responseMessage.StatusCode == HttpStatusCode.OK)
{
responseMessage.Content.CopyTo(request.ResponseStream, null, cts.Token);
await responseMessage.Content.CopyToAsync(request.ResponseStream, null, cts.Token);
}
else
{
data = responseMessage.Content.ReadAsByteArrayAsync(cts.Token).GetAwaiter().GetResult();
data = await responseMessage.Content.ReadAsByteArrayAsync(cts.Token);
}
}
catch (Exception ex)
@@ -123,7 +127,7 @@ namespace NzbDrone.Common.Http.Dispatchers
headers.Add(responseMessage.Content.Headers.ToNameValueCollection());
return new HttpResponse(request, new HttpHeader(headers), data, responseMessage.StatusCode);
return new HttpResponse(request, new HttpHeader(headers), data, responseMessage.StatusCode, responseMessage.Version);
}
}
@@ -160,6 +164,8 @@ namespace NzbDrone.Common.Http.Dispatchers
var client = new System.Net.Http.HttpClient(handler)
{
DefaultRequestVersion = HttpVersion.Version20,
DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrLower,
Timeout = Timeout.InfiniteTimeSpan
};

View File

@@ -5,6 +5,7 @@ using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.EnvironmentInfo;
@@ -25,6 +26,16 @@ namespace NzbDrone.Common.Http
HttpResponse Post(HttpRequest request);
HttpResponse<T> Post<T>(HttpRequest request)
where T : new();
Task<HttpResponse> ExecuteAsync(HttpRequest request);
Task DownloadFileAsync(string url, string fileName, string userAgent = null);
Task<HttpResponse> GetAsync(HttpRequest request);
Task<HttpResponse<T>> GetAsync<T>(HttpRequest request)
where T : new();
Task<HttpResponse> HeadAsync(HttpRequest request);
Task<HttpResponse> PostAsync(HttpRequest request);
Task<HttpResponse<T>> PostAsync<T>(HttpRequest request)
where T : new();
}
public class HttpClient : IHttpClient
@@ -52,11 +63,11 @@ namespace NzbDrone.Common.Http
_cookieContainerCache = cacheManager.GetCache<CookieContainer>(typeof(HttpClient));
}
public HttpResponse Execute(HttpRequest request)
public virtual async Task<HttpResponse> ExecuteAsync(HttpRequest request)
{
var cookieContainer = InitializeRequestCookies(request);
var response = ExecuteRequest(request, cookieContainer);
var response = await ExecuteRequestAsync(request, cookieContainer);
if (request.AllowAutoRedirect && response.HasHttpRedirect)
{
@@ -82,7 +93,7 @@ namespace NzbDrone.Common.Http
request.ContentSummary = null;
}
response = ExecuteRequest(request, cookieContainer);
response = await ExecuteRequestAsync(request, cookieContainer);
}
while (response.HasHttpRedirect);
}
@@ -112,6 +123,11 @@ namespace NzbDrone.Common.Http
return response;
}
public HttpResponse Execute(HttpRequest request)
{
return ExecuteAsync(request).GetAwaiter().GetResult();
}
private static bool RequestRequiresForceGet(HttpStatusCode statusCode, HttpMethod requestMethod)
{
return statusCode switch
@@ -122,7 +138,7 @@ namespace NzbDrone.Common.Http
};
}
private HttpResponse ExecuteRequest(HttpRequest request, CookieContainer cookieContainer)
private async Task<HttpResponse> ExecuteRequestAsync(HttpRequest request, CookieContainer cookieContainer)
{
foreach (var interceptor in _requestInterceptors)
{
@@ -131,14 +147,14 @@ namespace NzbDrone.Common.Http
if (request.RateLimit != TimeSpan.Zero)
{
_rateLimitService.WaitAndPulse(request.Url.Host, request.RateLimitKey, request.RateLimit);
await _rateLimitService.WaitAndPulseAsync(request.Url.Host, request.RateLimitKey, request.RateLimit);
}
_logger.Trace(request);
var stopWatch = Stopwatch.StartNew();
var response = _httpDispatcher.GetResponse(request, cookieContainer);
var response = await _httpDispatcher.GetResponseAsync(request, cookieContainer);
HandleResponseCookies(response, cookieContainer);
@@ -246,7 +262,7 @@ namespace NzbDrone.Common.Http
}
}
public void DownloadFile(string url, string fileName, string userAgent = null)
public async Task DownloadFileAsync(string url, string fileName, string userAgent = null)
{
var fileNamePart = fileName + ".part";
@@ -261,12 +277,13 @@ namespace NzbDrone.Common.Http
_logger.Debug("Downloading [{0}] to [{1}]", url, fileName);
var stopWatch = Stopwatch.StartNew();
using (var fileStream = new FileStream(fileNamePart, FileMode.Create, FileAccess.ReadWrite))
await using (var fileStream = new FileStream(fileNamePart, FileMode.Create, FileAccess.ReadWrite))
{
var request = new HttpRequest(url);
request.AllowAutoRedirect = true;
request.ResponseStream = fileStream;
var response = Get(request);
request.RequestTimeout = TimeSpan.FromSeconds(300);
var response = await GetAsync(request);
if (response.Headers.ContentType != null && response.Headers.ContentType.Contains("text/html"))
{
@@ -293,38 +310,71 @@ namespace NzbDrone.Common.Http
}
}
public HttpResponse Get(HttpRequest request)
public void DownloadFile(string url, string fileName, string userAgent = null)
{
// https://docs.microsoft.com/en-us/archive/msdn-magazine/2015/july/async-programming-brownfield-async-development#the-thread-pool-hack
Task.Run(() => DownloadFileAsync(url, fileName, userAgent)).GetAwaiter().GetResult();
}
public Task<HttpResponse> GetAsync(HttpRequest request)
{
request.Method = HttpMethod.Get;
return Execute(request);
return ExecuteAsync(request);
}
public HttpResponse Get(HttpRequest request)
{
return Task.Run(() => GetAsync(request)).GetAwaiter().GetResult();
}
public async Task<HttpResponse<T>> GetAsync<T>(HttpRequest request)
where T : new()
{
var response = await GetAsync(request);
CheckResponseContentType(response);
return new HttpResponse<T>(response);
}
public HttpResponse<T> Get<T>(HttpRequest request)
where T : new()
{
var response = Get(request);
CheckResponseContentType(response);
return new HttpResponse<T>(response);
return Task.Run(() => GetAsync<T>(request)).GetAwaiter().GetResult();
}
public Task<HttpResponse> HeadAsync(HttpRequest request)
{
request.Method = HttpMethod.Head;
return ExecuteAsync(request);
}
public HttpResponse Head(HttpRequest request)
{
request.Method = HttpMethod.Head;
return Execute(request);
return Task.Run(() => HeadAsync(request)).GetAwaiter().GetResult();
}
public Task<HttpResponse> PostAsync(HttpRequest request)
{
request.Method = HttpMethod.Post;
return ExecuteAsync(request);
}
public HttpResponse Post(HttpRequest request)
{
request.Method = HttpMethod.Post;
return Execute(request);
return Task.Run(() => PostAsync(request)).GetAwaiter().GetResult();
}
public async Task<HttpResponse<T>> PostAsync<T>(HttpRequest request)
where T : new()
{
var response = await PostAsync(request);
CheckResponseContentType(response);
return new HttpResponse<T>(response);
}
public HttpResponse<T> Post<T>(HttpRequest request)
where T : new()
{
var response = Post(request);
CheckResponseContentType(response);
return new HttpResponse<T>(response);
return Task.Run(() => PostAsync<T>(request)).GetAwaiter().GetResult();
}
private void CheckResponseContentType(HttpResponse response)

View File

@@ -16,6 +16,7 @@ namespace NzbDrone.Common.Http
Method = HttpMethod.Get;
Url = new HttpUri(url);
Headers = new HttpHeader();
ConnectionKeepAlive = true;
AllowAutoRedirect = true;
StoreRequestCookie = true;
LogHttpError = true;

View File

@@ -9,28 +9,31 @@ namespace NzbDrone.Common.Http
{
public class HttpResponse
{
private static readonly Regex RegexSetCookie = new Regex("^(.*?)=(.*?)(?:;|$)", RegexOptions.Compiled);
private static readonly Regex RegexSetCookie = new ("^(.*?)=(.*?)(?:;|$)", RegexOptions.Compiled);
public HttpResponse(HttpRequest request, HttpHeader headers, byte[] binaryData, HttpStatusCode statusCode = HttpStatusCode.OK)
public HttpResponse(HttpRequest request, HttpHeader headers, byte[] binaryData, HttpStatusCode statusCode = HttpStatusCode.OK, Version version = null)
{
Request = request;
Headers = headers;
ResponseData = binaryData;
StatusCode = statusCode;
Version = version;
}
public HttpResponse(HttpRequest request, HttpHeader headers, string content, HttpStatusCode statusCode = HttpStatusCode.OK)
public HttpResponse(HttpRequest request, HttpHeader headers, string content, HttpStatusCode statusCode = HttpStatusCode.OK, Version version = null)
{
Request = request;
Headers = headers;
ResponseData = Headers.GetEncodingFromContentType().GetBytes(content);
_content = content;
StatusCode = statusCode;
Version = version;
}
public HttpRequest Request { get; private set; }
public HttpHeader Headers { get; private set; }
public HttpStatusCode StatusCode { get; private set; }
public Version Version { get; private set; }
public byte[] ResponseData { get; private set; }
private string _content;
@@ -84,7 +87,7 @@ namespace NzbDrone.Common.Http
public override string ToString()
{
var result = string.Format("Res: [{0}] {1}: {2}.{3} ({4} bytes)", Request.Method, Request.Url, (int)StatusCode, StatusCode, ResponseData?.Length ?? 0);
var result = $"Res: HTTP/{Version} [{Request.Method}] {Request.Url}: {(int)StatusCode}.{StatusCode} ({ResponseData?.Length ?? 0} bytes)";
if (HasHttpError && Headers.ContentType.IsNotNullOrWhiteSpace() && !Headers.ContentType.Equals("text/html", StringComparison.InvariantCultureIgnoreCase))
{
@@ -99,7 +102,7 @@ namespace NzbDrone.Common.Http
where T : new()
{
public HttpResponse(HttpResponse response)
: base(response.Request, response.Headers, response.ResponseData, response.StatusCode)
: base(response.Request, response.Headers, response.ResponseData, response.StatusCode, response.Version)
{
Resource = Json.Deserialize<T>(response.Content);
}

View File

@@ -20,7 +20,7 @@ namespace NzbDrone.Common.Instrumentation
new (@"\b(\w*)?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Trackers Announce Keys; Designed for Qbit Json; should work for all in theory
new (@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?<secret>[a-z0-9]{16,})|(?<secret>[a-z0-9]{16,})(/|%2f)announce"),
new (@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?<secret>[a-z0-9]{16,})|(?<secret>[a-z0-9]{16,})(/|%2f)announce", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Path
new (@"C:\\Users\\(?<secret>[^\""]+?)(\\|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Extensions;
@@ -10,6 +11,8 @@ namespace NzbDrone.Common.TPL
{
void WaitAndPulse(string key, TimeSpan interval);
void WaitAndPulse(string key, string subKey, TimeSpan interval);
Task WaitAndPulseAsync(string key, TimeSpan interval);
Task WaitAndPulseAsync(string key, string subKey, TimeSpan interval);
}
public class RateLimitService : IRateLimitService
@@ -28,7 +31,34 @@ namespace NzbDrone.Common.TPL
WaitAndPulse(key, null, interval);
}
public async Task WaitAndPulseAsync(string key, TimeSpan interval)
{
await WaitAndPulseAsync(key, null, interval);
}
public void WaitAndPulse(string key, string subKey, TimeSpan interval)
{
var delay = GetDelay(key, subKey, interval);
if (delay.TotalSeconds > 0.0)
{
_logger.Trace("Rate Limit triggered, delaying '{0}' for {1:0.000} sec", key, delay.TotalSeconds);
System.Threading.Thread.Sleep(delay);
}
}
public async Task WaitAndPulseAsync(string key, string subKey, TimeSpan interval)
{
var delay = GetDelay(key, subKey, interval);
if (delay.TotalSeconds > 0.0)
{
_logger.Trace("Rate Limit triggered, delaying '{0}' for {1:0.000} sec", key, delay.TotalSeconds);
await Task.Delay(delay);
}
}
private TimeSpan GetDelay(string key, string subKey, TimeSpan interval)
{
var waitUntil = DateTime.UtcNow.Add(interval);
@@ -59,13 +89,7 @@ namespace NzbDrone.Common.TPL
waitUntil -= interval;
var delay = waitUntil - DateTime.UtcNow;
if (delay.TotalSeconds > 0.0)
{
_logger.Trace("Rate Limit triggered, delaying '{0}' for {1:0.000} sec", key, delay.TotalSeconds);
System.Threading.Thread.Sleep(delay);
}
return waitUntil - DateTime.UtcNow;
}
}
}

View File

@@ -5,6 +5,7 @@ using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.Books;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.CustomFormats;
using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.Download.TrackedDownloads;
@@ -369,5 +370,31 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
Subject.IsSatisfiedBy(_remoteBook, null).Accepted.Should().BeTrue();
}
[Test]
public void should_return_false_if_same_quality_non_proper_in_queue_and_download_propers_is_do_not_upgrade()
{
_remoteBook.ParsedBookInfo.Quality = new QualityModel(Quality.FLAC, new Revision(2));
_author.QualityProfile.Value.Cutoff = _remoteBook.ParsedBookInfo.Quality.Quality.Id;
Mocker.GetMock<IConfigService>()
.Setup(s => s.DownloadPropersAndRepacks)
.Returns(ProperDownloadTypes.DoNotUpgrade);
var remoteBook = Builder<RemoteBook>.CreateNew()
.With(r => r.Author = _author)
.With(r => r.Books = new List<Book> { _book })
.With(r => r.ParsedBookInfo = new ParsedBookInfo
{
Quality = new QualityModel(Quality.FLAC)
})
.With(r => r.Release = _releaseInfo)
.With(r => r.CustomFormats = new List<CustomFormat>())
.Build();
GivenQueue(new List<RemoteBook> { remoteBook });
Subject.IsSatisfiedBy(_remoteBook, null).Accepted.Should().BeFalse();
}
}
}

View File

@@ -1,9 +1,10 @@
using System.Collections.Generic;
using System.Collections.Generic;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.Books;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.DecisionEngine.Specifications.RssSync;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.IndexerSearch.Definitions;
@@ -33,8 +34,13 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
};
Mocker
.GetMock<IIndexerRepository>()
.GetMock<IIndexerFactory>()
.Setup(m => m.Get(It.IsAny<int>()))
.Throws(new ModelNotFoundException(typeof(IndexerDefinition), -1));
Mocker
.GetMock<IIndexerFactory>()
.Setup(m => m.Get(1))
.Returns(_fakeIndexerDefinition);
_specification = Mocker.Resolve<IndexerTagSpecification>();
@@ -106,5 +112,25 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
_specification.IsSatisfiedBy(_parseResultMulti, new BookSearchCriteria { MonitoredBooksOnly = true }).Accepted.Should().BeFalse();
}
[Test]
public void release_without_indexerid_should_return_true()
{
_fakeIndexerDefinition.Tags = new HashSet<int> { 456 };
_fakeAuthor.Tags = new HashSet<int> { 123, 789 };
_fakeRelease.IndexerId = 0;
_specification.IsSatisfiedBy(_parseResultMulti, new BookSearchCriteria { MonitoredBooksOnly = true }).Accepted.Should().BeTrue();
}
[Test]
public void release_with_invalid_indexerid_should_return_true()
{
_fakeIndexerDefinition.Tags = new HashSet<int> { 456 };
_fakeAuthor.Tags = new HashSet<int> { 123, 789 };
_fakeRelease.IndexerId = 2;
_specification.IsSatisfiedBy(_parseResultMulti, new BookSearchCriteria { MonitoredBooksOnly = true }).Accepted.Should().BeTrue();
}
}
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
@@ -58,7 +59,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
}
[Test]
public void should_download_report_if_book_was_not_already_downloaded()
public async Task should_download_report_if_book_was_not_already_downloaded()
{
var books = new List<Book> { GetBook(1) };
var remoteBook = GetRemoteBook(books, new QualityModel(Quality.MP3));
@@ -66,12 +67,12 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
var decisions = new List<DownloadDecision>();
decisions.Add(new DownloadDecision(remoteBook));
Subject.ProcessDecisions(decisions);
await Subject.ProcessDecisions(decisions);
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteBook>()), Times.Once());
}
[Test]
public void should_only_download_book_once()
public async Task should_only_download_book_once()
{
var books = new List<Book> { GetBook(1) };
var remoteBook = GetRemoteBook(books, new QualityModel(Quality.MP3));
@@ -80,12 +81,12 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
decisions.Add(new DownloadDecision(remoteBook));
decisions.Add(new DownloadDecision(remoteBook));
Subject.ProcessDecisions(decisions);
await Subject.ProcessDecisions(decisions);
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteBook>()), Times.Once());
}
[Test]
public void should_not_download_if_any_book_was_already_downloaded()
public async Task should_not_download_if_any_book_was_already_downloaded()
{
var remoteBook1 = GetRemoteBook(
new List<Book> { GetBook(1) },
@@ -99,12 +100,12 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
decisions.Add(new DownloadDecision(remoteBook1));
decisions.Add(new DownloadDecision(remoteBook2));
Subject.ProcessDecisions(decisions);
await Subject.ProcessDecisions(decisions);
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteBook>()), Times.Once());
}
[Test]
public void should_return_downloaded_reports()
public async Task should_return_downloaded_reports()
{
var books = new List<Book> { GetBook(1) };
var remoteBook = GetRemoteBook(books, new QualityModel(Quality.MP3));
@@ -112,11 +113,13 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
var decisions = new List<DownloadDecision>();
decisions.Add(new DownloadDecision(remoteBook));
Subject.ProcessDecisions(decisions).Grabbed.Should().HaveCount(1);
var result = await Subject.ProcessDecisions(decisions);
result.Grabbed.Should().HaveCount(1);
}
[Test]
public void should_return_all_downloaded_reports()
public async Task should_return_all_downloaded_reports()
{
var remoteBook1 = GetRemoteBook(
new List<Book> { GetBook(1) },
@@ -130,11 +133,13 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
decisions.Add(new DownloadDecision(remoteBook1));
decisions.Add(new DownloadDecision(remoteBook2));
Subject.ProcessDecisions(decisions).Grabbed.Should().HaveCount(2);
var result = await Subject.ProcessDecisions(decisions);
result.Grabbed.Should().HaveCount(2);
}
[Test]
public void should_only_return_downloaded_reports()
public async Task should_only_return_downloaded_reports()
{
var remoteBook1 = GetRemoteBook(
new List<Book> { GetBook(1) },
@@ -153,11 +158,13 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
decisions.Add(new DownloadDecision(remoteBook2));
decisions.Add(new DownloadDecision(remoteBook3));
Subject.ProcessDecisions(decisions).Grabbed.Should().HaveCount(2);
var result = await Subject.ProcessDecisions(decisions);
result.Grabbed.Should().HaveCount(2);
}
[Test]
public void should_not_add_to_downloaded_list_when_download_fails()
public async Task should_not_add_to_downloaded_list_when_download_fails()
{
var books = new List<Book> { GetBook(1) };
var remoteBook = GetRemoteBook(books, new QualityModel(Quality.MP3));
@@ -166,7 +173,11 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
decisions.Add(new DownloadDecision(remoteBook));
Mocker.GetMock<IDownloadService>().Setup(s => s.DownloadReport(It.IsAny<RemoteBook>())).Throws(new Exception());
Subject.ProcessDecisions(decisions).Grabbed.Should().BeEmpty();
var result = await Subject.ProcessDecisions(decisions);
result.Grabbed.Should().BeEmpty();
ExceptionVerification.ExpectedWarns(1);
}
@@ -181,7 +192,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
}
[Test]
public void should_not_grab_if_pending()
public async Task should_not_grab_if_pending()
{
var books = new List<Book> { GetBook(1) };
var remoteBook = GetRemoteBook(books, new QualityModel(Quality.MP3));
@@ -189,12 +200,12 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
var decisions = new List<DownloadDecision>();
decisions.Add(new DownloadDecision(remoteBook, new Rejection("Failure!", RejectionType.Temporary)));
Subject.ProcessDecisions(decisions);
await Subject.ProcessDecisions(decisions);
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteBook>()), Times.Never());
}
[Test]
public void should_not_add_to_pending_if_book_was_grabbed()
public async Task should_not_add_to_pending_if_book_was_grabbed()
{
var books = new List<Book> { GetBook(1) };
var remoteBook = GetRemoteBook(books, new QualityModel(Quality.MP3));
@@ -203,12 +214,12 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
decisions.Add(new DownloadDecision(remoteBook));
decisions.Add(new DownloadDecision(remoteBook, new Rejection("Failure!", RejectionType.Temporary)));
Subject.ProcessDecisions(decisions);
await Subject.ProcessDecisions(decisions);
Mocker.GetMock<IPendingReleaseService>().Verify(v => v.AddMany(It.IsAny<List<Tuple<DownloadDecision, PendingReleaseReason>>>()), Times.Never());
}
[Test]
public void should_add_to_pending_even_if_already_added_to_pending()
public async Task should_add_to_pending_even_if_already_added_to_pending()
{
var books = new List<Book> { GetBook(1) };
var remoteBook = GetRemoteBook(books, new QualityModel(Quality.MP3));
@@ -217,12 +228,12 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
decisions.Add(new DownloadDecision(remoteBook, new Rejection("Failure!", RejectionType.Temporary)));
decisions.Add(new DownloadDecision(remoteBook, new Rejection("Failure!", RejectionType.Temporary)));
Subject.ProcessDecisions(decisions);
await Subject.ProcessDecisions(decisions);
Mocker.GetMock<IPendingReleaseService>().Verify(v => v.AddMany(It.IsAny<List<Tuple<DownloadDecision, PendingReleaseReason>>>()), Times.Once());
}
[Test]
public void should_add_to_failed_if_already_failed_for_that_protocol()
public async Task should_add_to_failed_if_already_failed_for_that_protocol()
{
var books = new List<Book> { GetBook(1) };
var remoteBook = GetRemoteBook(books, new QualityModel(Quality.MP3));
@@ -234,12 +245,12 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
Mocker.GetMock<IDownloadService>().Setup(s => s.DownloadReport(It.IsAny<RemoteBook>()))
.Throws(new DownloadClientUnavailableException("Download client failed"));
Subject.ProcessDecisions(decisions);
await Subject.ProcessDecisions(decisions);
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteBook>()), Times.Once());
}
[Test]
public void should_not_add_to_failed_if_failed_for_a_different_protocol()
public async Task should_not_add_to_failed_if_failed_for_a_different_protocol()
{
var books = new List<Book> { GetBook(1) };
var remoteBook = GetRemoteBook(books, new QualityModel(Quality.MP3), DownloadProtocol.Usenet);
@@ -252,13 +263,13 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
Mocker.GetMock<IDownloadService>().Setup(s => s.DownloadReport(It.Is<RemoteBook>(r => r.Release.DownloadProtocol == DownloadProtocol.Usenet)))
.Throws(new DownloadClientUnavailableException("Download client failed"));
Subject.ProcessDecisions(decisions);
await Subject.ProcessDecisions(decisions);
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.Is<RemoteBook>(r => r.Release.DownloadProtocol == DownloadProtocol.Usenet)), Times.Once());
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.Is<RemoteBook>(r => r.Release.DownloadProtocol == DownloadProtocol.Torrent)), Times.Once());
}
[Test]
public void should_add_to_rejected_if_release_unavailable_on_indexer()
public async Task should_add_to_rejected_if_release_unavailable_on_indexer()
{
var books = new List<Book> { GetBook(1) };
var remoteBook = GetRemoteBook(books, new QualityModel(Quality.MP3));
@@ -270,7 +281,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
.Setup(s => s.DownloadReport(It.IsAny<RemoteBook>()))
.Throws(new ReleaseUnavailableException(remoteBook.Release, "That 404 Error is not just a Quirk"));
var result = Subject.ProcessDecisions(decisions);
var result = await Subject.ProcessDecisions(decisions);
result.Grabbed.Should().BeEmpty();
result.Rejected.Should().NotBeEmpty();

View File

@@ -4,6 +4,7 @@ using System.IO;
using System.IO.Abstractions;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
@@ -69,7 +70,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole
protected void GivenFailedDownload()
{
Mocker.GetMock<IHttpClient>()
.Setup(s => s.Get(It.IsAny<HttpRequest>()))
.Setup(s => s.GetAsync(It.IsAny<HttpRequest>()))
.Throws(new WebException());
}
@@ -147,19 +148,19 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole
}
[Test]
public void Download_should_download_file_if_it_doesnt_exist()
public async Task Download_should_download_file_if_it_doesnt_exist()
{
var remoteBook = CreateRemoteBook();
Subject.Download(remoteBook, CreateIndexer());
await Subject.Download(remoteBook, CreateIndexer());
Mocker.GetMock<IHttpClient>().Verify(c => c.Get(It.Is<HttpRequest>(v => v.Url.FullUri == _downloadUrl)), Times.Once());
Mocker.GetMock<IHttpClient>().Verify(c => c.GetAsync(It.Is<HttpRequest>(v => v.Url.FullUri == _downloadUrl)), Times.Once());
Mocker.GetMock<IDiskProvider>().Verify(c => c.OpenWriteStream(_filePath), Times.Once());
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFile(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never());
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFileAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never());
}
[Test]
public void Download_should_save_magnet_if_enabled()
public async Task Download_should_save_magnet_if_enabled()
{
GivenMagnetFilePath();
Subject.Definition.Settings.As<TorrentBlackholeSettings>().SaveMagnetFiles = true;
@@ -167,16 +168,16 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole
var remoteBook = CreateRemoteBook();
remoteBook.Release.DownloadUrl = null;
Subject.Download(remoteBook, CreateIndexer());
await Subject.Download(remoteBook, CreateIndexer());
Mocker.GetMock<IHttpClient>().Verify(c => c.Get(It.Is<HttpRequest>(v => v.Url.FullUri == _downloadUrl)), Times.Never());
Mocker.GetMock<IHttpClient>().Verify(c => c.GetAsync(It.Is<HttpRequest>(v => v.Url.FullUri == _downloadUrl)), Times.Never());
Mocker.GetMock<IDiskProvider>().Verify(c => c.OpenWriteStream(_filePath), Times.Never());
Mocker.GetMock<IDiskProvider>().Verify(c => c.OpenWriteStream(_magnetFilePath), Times.Once());
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFile(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never());
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFileAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never());
}
[Test]
public void Download_should_save_magnet_using_specified_extension()
public async Task Download_should_save_magnet_using_specified_extension()
{
var magnetFileExtension = ".url";
GivenMagnetFilePath(magnetFileExtension);
@@ -187,12 +188,12 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole
var remoteBook = CreateRemoteBook();
remoteBook.Release.DownloadUrl = null;
Subject.Download(remoteBook, CreateIndexer());
await Subject.Download(remoteBook, CreateIndexer());
Mocker.GetMock<IHttpClient>().Verify(c => c.Get(It.Is<HttpRequest>(v => v.Url.FullUri == _downloadUrl)), Times.Never());
Mocker.GetMock<IHttpClient>().Verify(c => c.GetAsync(It.Is<HttpRequest>(v => v.Url.FullUri == _downloadUrl)), Times.Never());
Mocker.GetMock<IDiskProvider>().Verify(c => c.OpenWriteStream(_filePath), Times.Never());
Mocker.GetMock<IDiskProvider>().Verify(c => c.OpenWriteStream(_magnetFilePath), Times.Once());
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFile(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never());
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFileAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never());
}
[Test]
@@ -202,31 +203,31 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole
var remoteBook = CreateRemoteBook();
remoteBook.Release.DownloadUrl = null;
Assert.Throws<ReleaseDownloadException>(() => Subject.Download(remoteBook, CreateIndexer()));
Assert.ThrowsAsync<ReleaseDownloadException>(async () => await Subject.Download(remoteBook, CreateIndexer()));
Mocker.GetMock<IHttpClient>().Verify(c => c.Get(It.Is<HttpRequest>(v => v.Url.FullUri == _downloadUrl)), Times.Never());
Mocker.GetMock<IHttpClient>().Verify(c => c.GetAsync(It.Is<HttpRequest>(v => v.Url.FullUri == _downloadUrl)), Times.Never());
Mocker.GetMock<IDiskProvider>().Verify(c => c.OpenWriteStream(_filePath), Times.Never());
Mocker.GetMock<IDiskProvider>().Verify(c => c.OpenWriteStream(_magnetFilePath), Times.Never());
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFile(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never());
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFileAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never());
}
[Test]
public void Download_should_prefer_torrent_over_magnet()
public async Task Download_should_prefer_torrent_over_magnet()
{
Subject.Definition.Settings.As<TorrentBlackholeSettings>().SaveMagnetFiles = true;
var remoteBook = CreateRemoteBook();
Subject.Download(remoteBook, CreateIndexer());
await Subject.Download(remoteBook, CreateIndexer());
Mocker.GetMock<IHttpClient>().Verify(c => c.Get(It.Is<HttpRequest>(v => v.Url.FullUri == _downloadUrl)), Times.Once());
Mocker.GetMock<IHttpClient>().Verify(c => c.GetAsync(It.Is<HttpRequest>(v => v.Url.FullUri == _downloadUrl)), Times.Once());
Mocker.GetMock<IDiskProvider>().Verify(c => c.OpenWriteStream(_filePath), Times.Once());
Mocker.GetMock<IDiskProvider>().Verify(c => c.OpenWriteStream(_magnetFilePath), Times.Never());
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFile(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never());
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFileAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never());
}
[Test]
public void Download_should_replace_illegal_characters_in_title()
public async Task Download_should_replace_illegal_characters_in_title()
{
var illegalTitle = "Radiohead - Scotch Mist [2008/FLAC/Lossless]";
var expectedFilename = Path.Combine(_blackholeFolder, "Radiohead - Scotch Mist [2008+FLAC+Lossless]" + Path.GetExtension(_filePath));
@@ -234,11 +235,11 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole
var remoteBook = CreateRemoteBook();
remoteBook.Release.Title = illegalTitle;
Subject.Download(remoteBook, CreateIndexer());
await Subject.Download(remoteBook, CreateIndexer());
Mocker.GetMock<IHttpClient>().Verify(c => c.Get(It.Is<HttpRequest>(v => v.Url.FullUri == _downloadUrl)), Times.Once());
Mocker.GetMock<IHttpClient>().Verify(c => c.GetAsync(It.Is<HttpRequest>(v => v.Url.FullUri == _downloadUrl)), Times.Once());
Mocker.GetMock<IDiskProvider>().Verify(c => c.OpenWriteStream(expectedFilename), Times.Once());
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFile(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never());
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFileAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never());
}
[Test]
@@ -247,7 +248,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole
var remoteBook = CreateRemoteBook();
remoteBook.Release.DownloadUrl = null;
Assert.Throws<ReleaseDownloadException>(() => Subject.Download(remoteBook, CreateIndexer()));
Assert.ThrowsAsync<ReleaseDownloadException>(async () => await Subject.Download(remoteBook, CreateIndexer()));
}
[Test]
@@ -317,11 +318,13 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole
}
[Test]
public void should_return_null_hash()
public async Task should_return_null_hash()
{
var remoteBook = CreateRemoteBook();
Subject.Download(remoteBook, CreateIndexer()).Should().BeNull();
var result = await Subject.Download(remoteBook, CreateIndexer());
result.Should().BeNull();
}
}
}

View File

@@ -4,6 +4,7 @@ using System.IO;
using System.IO.Abstractions;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
@@ -119,19 +120,19 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole
}
[Test]
public void Download_should_download_file_if_it_doesnt_exist()
public async Task Download_should_download_file_if_it_doesnt_exist()
{
var remoteBook = CreateRemoteBook();
Subject.Download(remoteBook, CreateIndexer());
await Subject.Download(remoteBook, CreateIndexer());
Mocker.GetMock<IHttpClient>().Verify(c => c.Get(It.Is<HttpRequest>(v => v.Url.FullUri == _downloadUrl)), Times.Once());
Mocker.GetMock<IHttpClient>().Verify(c => c.GetAsync(It.Is<HttpRequest>(v => v.Url.FullUri == _downloadUrl)), Times.Once());
Mocker.GetMock<IDiskProvider>().Verify(c => c.OpenWriteStream(_filePath), Times.Once());
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFile(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never());
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFileAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never());
}
[Test]
public void Download_should_replace_illegal_characters_in_title()
public async Task Download_should_replace_illegal_characters_in_title()
{
var illegalTitle = "Radiohead - Scotch Mist [2008/FLAC/Lossless]";
var expectedFilename = Path.Combine(_blackholeFolder, "Radiohead - Scotch Mist [2008+FLAC+Lossless]" + Path.GetExtension(_filePath));
@@ -139,11 +140,11 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole
var remoteBook = CreateRemoteBook();
remoteBook.Release.Title = illegalTitle;
Subject.Download(remoteBook, CreateIndexer());
await Subject.Download(remoteBook, CreateIndexer());
Mocker.GetMock<IHttpClient>().Verify(c => c.Get(It.Is<HttpRequest>(v => v.Url.FullUri == _downloadUrl)), Times.Once());
Mocker.GetMock<IHttpClient>().Verify(c => c.GetAsync(It.Is<HttpRequest>(v => v.Url.FullUri == _downloadUrl)), Times.Once());
Mocker.GetMock<IDiskProvider>().Verify(c => c.OpenWriteStream(expectedFilename), Times.Once());
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFile(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never());
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFileAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never());
}
[Test]

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@@ -200,26 +201,26 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DelugeTests
}
[Test]
public void Download_should_return_unique_id()
public async Task Download_should_return_unique_id()
{
GivenSuccessfulDownload();
var remoteBook = CreateRemoteBook();
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
id.Should().NotBeNullOrEmpty();
}
[TestCase("magnet:?xt=urn:btih:ZPBPA2P6ROZPKRHK44D5OW6NHXU5Z6KR&tr=udp", "CBC2F069FE8BB2F544EAE707D75BCD3DE9DCF951")]
public void Download_should_get_hash_from_magnet_url(string magnetUrl, string expectedHash)
public async Task Download_should_get_hash_from_magnet_url(string magnetUrl, string expectedHash)
{
GivenSuccessfulDownload();
var remoteBook = CreateRemoteBook();
remoteBook.Release.DownloadUrl = magnetUrl;
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
id.Should().Be(expectedHash);
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using FluentAssertions;
using Moq;
using NLog;
@@ -36,8 +37,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests
.Returns(() => CreateRemoteBook());
Mocker.GetMock<IHttpClient>()
.Setup(s => s.Get(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new byte[0]));
.Setup(s => s.GetAsync(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), Array.Empty<byte>())));
Mocker.GetMock<IRemotePathMappingService>()
.Setup(v => v.RemapRemoteToLocal(It.IsAny<string>(), It.IsAny<OsPath>()))

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@@ -385,7 +386,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
}
[Test]
public void Download_with_TvDirectory_should_force_directory()
public async Task Download_with_TvDirectory_should_force_directory()
{
GivenSerialNumber();
GivenTvDirectory();
@@ -393,7 +394,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
var remoteBook = CreateRemoteBook();
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
id.Should().NotBeNullOrEmpty();
@@ -402,7 +403,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
}
[Test]
public void Download_with_category_should_force_directory()
public async Task Download_with_category_should_force_directory()
{
GivenSerialNumber();
GivenMusicCategory();
@@ -410,7 +411,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
var remoteBook = CreateRemoteBook();
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
id.Should().NotBeNullOrEmpty();
@@ -419,14 +420,14 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
}
[Test]
public void Download_without_TvDirectory_and_Category_should_use_default()
public async Task Download_without_TvDirectory_and_Category_should_use_default()
{
GivenSerialNumber();
GivenSuccessfulDownload();
var remoteBook = CreateRemoteBook();
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
id.Should().NotBeNullOrEmpty();
@@ -505,7 +506,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
.Setup(s => s.GetSerialNumber(_settings))
.Throws(new ApplicationException("Some unknown exception, HttpException or DownloadClientException"));
Assert.Throws(Is.InstanceOf<Exception>(), () => Subject.Download(remoteBook, CreateIndexer()));
Assert.ThrowsAsync(Is.InstanceOf<Exception>(), async () => await Subject.Download(remoteBook, CreateIndexer()));
Mocker.GetMock<IDownloadStationTaskProxy>()
.Verify(v => v.AddTaskFromUrl(It.IsAny<string>(), null, _settings), Times.Never());

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@@ -262,7 +263,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
}
[Test]
public void Download_with_TvDirectory_should_force_directory()
public async Task Download_with_TvDirectory_should_force_directory()
{
GivenSerialNumber();
GivenTvDirectory();
@@ -270,7 +271,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
var remoteBook = CreateRemoteBook();
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
id.Should().NotBeNullOrEmpty();
@@ -279,7 +280,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
}
[Test]
public void Download_with_category_should_force_directory()
public async Task Download_with_category_should_force_directory()
{
GivenSerialNumber();
GivenMusicCategory();
@@ -287,7 +288,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
var remoteBook = CreateRemoteBook();
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
id.Should().NotBeNullOrEmpty();
@@ -296,14 +297,14 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
}
[Test]
public void Download_without_TvDirectory_and_Category_should_use_default()
public async Task Download_without_TvDirectory_and_Category_should_use_default()
{
GivenSerialNumber();
GivenSuccessfulDownload();
var remoteBook = CreateRemoteBook();
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
id.Should().NotBeNullOrEmpty();
@@ -382,7 +383,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
.Setup(s => s.GetSerialNumber(_settings))
.Throws(new ApplicationException("Some unknown exception, HttpException or DownloadClientException"));
Assert.Throws(Is.InstanceOf<Exception>(), () => Subject.Download(remoteBook, CreateIndexer()));
Assert.ThrowsAsync(Is.InstanceOf<Exception>(), async () => await Subject.Download(remoteBook, CreateIndexer()));
Mocker.GetMock<IDownloadStationTaskProxy>()
.Verify(v => v.AddTaskFromUrl(It.IsAny<string>(), null, _settings), Times.Never());

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@@ -103,8 +104,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.HadoukenTests
protected void GivenSuccessfulDownload()
{
Mocker.GetMock<IHttpClient>()
.Setup(s => s.Get(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new byte[1000]));
.Setup(s => s.GetAsync(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new byte[1000])));
Mocker.GetMock<IHadoukenProxy>()
.Setup(s => s.AddTorrentUri(It.IsAny<HadoukenSettings>(), It.IsAny<string>()))
@@ -196,13 +197,13 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.HadoukenTests
}
[Test]
public void Download_should_return_unique_id()
public async Task Download_should_return_unique_id()
{
GivenSuccessfulDownload();
var remoteBook = CreateRemoteBook();
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
id.Should().NotBeNullOrEmpty();
}
@@ -277,7 +278,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.HadoukenTests
}
[Test]
public void Download_from_magnet_link_should_return_hash_uppercase()
public async Task Download_from_magnet_link_should_return_hash_uppercase()
{
var remoteBook = CreateRemoteBook();
@@ -286,13 +287,13 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.HadoukenTests
Mocker.GetMock<IHadoukenProxy>()
.Setup(v => v.AddTorrentUri(It.IsAny<HadoukenSettings>(), It.IsAny<string>()));
var result = Subject.Download(remoteBook, CreateIndexer());
var result = await Subject.Download(remoteBook, CreateIndexer());
Assert.IsFalse(result.Any(c => char.IsLower(c)));
}
[Test]
public void Download_from_torrent_file_should_return_hash_uppercase()
public async Task Download_from_torrent_file_should_return_hash_uppercase()
{
var remoteBook = CreateRemoteBook();
@@ -300,7 +301,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.HadoukenTests
.Setup(v => v.AddTorrentFile(It.IsAny<HadoukenSettings>(), It.IsAny<byte[]>()))
.Returns("hash");
var result = Subject.Download(remoteBook, CreateIndexer());
var result = await Subject.Download(remoteBook, CreateIndexer());
Assert.IsFalse(result.Any(c => char.IsLower(c)));
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@@ -200,13 +201,13 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbVortexTests
}
[Test]
public void Download_should_return_unique_id()
public async Task Download_should_return_unique_id()
{
GivenSuccessfulDownload();
var remoteBook = CreateRemoteBook();
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
id.Should().NotBeNullOrEmpty();
}
@@ -218,7 +219,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbVortexTests
var remoteBook = CreateRemoteBook();
Assert.Throws<DownloadClientException>(() => Subject.Download(remoteBook, CreateIndexer()));
Assert.ThrowsAsync<DownloadClientException>(async () => await Subject.Download(remoteBook, CreateIndexer()));
}
[Test]

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