Compare commits

...

106 Commits

Author SHA1 Message Date
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
195 changed files with 2437 additions and 1135 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.5'
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.413'
dotnetVersion: '6.0.417'
nodeVersion: '16.X'
innoVersion: '6.2.0'
windowsImage: 'windows-2022'

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

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

@@ -7,6 +7,7 @@ import TableBody from 'Components/Table/TableBody';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import AuthorHistoryRowConnector from './AuthorHistoryRowConnector';
import styles from './AuthorHistoryTableContent.css';
const columns = [
{
@@ -64,7 +65,7 @@ class AuthorHistoryTableContent extends Component {
const hasItems = !!items.length;
return (
<>
<div>
{
isFetching &&
<LoadingIndicator />
@@ -79,7 +80,7 @@ class AuthorHistoryTableContent extends Component {
{
isPopulated && !hasItems && !error &&
<div>
<div className={styles.blankpad}>
{translate('NoHistory')}
</div>
}
@@ -103,7 +104,7 @@ class AuthorHistoryTableContent extends Component {
</TableBody>
</Table>
}
</>
</div>
);
}
}

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

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

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

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

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

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

@@ -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.21",
"@microsoft/signalr": "6.0.25",
"@sentry/browser": "7.51.2",
"@sentry/integrations": "7.51.2",
"@types/node": "18.16.16",
@@ -95,6 +95,7 @@
"@babel/preset-react": "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.5.0",
"@typescript-eslint/parser": "6.5.0",
@@ -120,7 +121,7 @@
"html-webpack-plugin": "5.5.3",
"loader-utils": "^3.2.1",
"mini-css-extract-plugin": "2.7.6",
"postcss": "8.4.23",
"postcss": "8.4.31",
"postcss-color-function": "4.1.0",
"postcss-loader": "7.3.0",
"postcss-mixins": "9.0.4",

View File

@@ -4,7 +4,7 @@
<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.1" />
<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" />
@@ -16,11 +16,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.21" />
<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 +32,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" />
@@ -59,7 +59,7 @@
<PackageVersion Include="System.Security.Principal.Windows" Version="5.0.0" />
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="6.0.1" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="6.0.0" />
<PackageVersion Include="System.Text.Json" Version="6.0.8" />
<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>

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

@@ -115,7 +115,7 @@ namespace NzbDrone.Common.Http.Dispatchers
}
else
{
data = responseMessage.Content.ReadAsByteArrayAsync(cts.Token).GetAwaiter().GetResult();
data = await responseMessage.Content.ReadAsByteArrayAsync(cts.Token);
}
}
catch (Exception ex)

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

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

@@ -405,7 +405,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
Size = 1000,
Progress = 0.7,
Eta = 8640000,
State = "stalledDL",
State = "pausedUP",
Label = "",
SavePath = @"C:\Torrents".AsOsAgnostic(),
ContentPath = @"C:\Torrents\Droned.S01.12".AsOsAgnostic()

View File

@@ -452,6 +452,30 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests
result.OutputRootFolders.First().Should().Be(fullCategoryDir);
}
[TestCase("0")]
[TestCase("15d")]
public void should_set_history_removes_completed_downloads_false(string historyRetention)
{
_config.Misc.history_retention = historyRetention;
var downloadClientInfo = Subject.GetStatus();
downloadClientInfo.RemovesCompletedDownloads.Should().BeFalse();
}
[TestCase("-1")]
[TestCase("15")]
[TestCase("3")]
[TestCase("3d")]
public void should_set_history_removes_completed_downloads_true(string historyRetention)
{
_config.Misc.history_retention = historyRetention;
var downloadClientInfo = Subject.GetStatus();
downloadClientInfo.RemovesCompletedDownloads.Should().BeTrue();
}
[TestCase(@"Y:\nzbget\root", @"completed\downloads", @"vv", @"Y:\nzbget\root\completed\downloads", @"Y:\nzbget\root\completed\downloads\vv")]
[TestCase(@"Y:\nzbget\root", @"completed", @"vv", @"Y:\nzbget\root\completed", @"Y:\nzbget\root\completed\vv")]
[TestCase(@"/nzbget/root", @"completed/downloads", @"vv", @"/nzbget/root/completed/downloads", @"/nzbget/root/completed/downloads/vv")]

View File

@@ -0,0 +1,78 @@
using System;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.Clients;
using NzbDrone.Core.HealthCheck.Checks;
using NzbDrone.Core.Localization;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.HealthCheck.Checks
{
[TestFixture]
public class DownloadClientRemovesCompletedDownloadsCheckFixture : CoreTest<DownloadClientRemovesCompletedDownloadsCheck>
{
private DownloadClientInfo _clientStatus;
private Mock<IDownloadClient> _downloadClient;
private static Exception[] DownloadClientExceptions =
{
new DownloadClientUnavailableException("error"),
new DownloadClientAuthenticationException("error"),
new DownloadClientException("error")
};
[SetUp]
public void Setup()
{
_clientStatus = new DownloadClientInfo
{
IsLocalhost = true,
// SortingMode = null,
RemovesCompletedDownloads = true
};
_downloadClient = Mocker.GetMock<IDownloadClient>();
_downloadClient.Setup(s => s.Definition)
.Returns(new DownloadClientDefinition { Name = "Test" });
_downloadClient.Setup(s => s.GetStatus())
.Returns(_clientStatus);
Mocker.GetMock<IProvideDownloadClient>()
.Setup(s => s.GetDownloadClients(It.IsAny<bool>()))
.Returns(new IDownloadClient[] { _downloadClient.Object });
Mocker.GetMock<ILocalizationService>()
.Setup(s => s.GetLocalizedString(It.IsAny<string>()))
.Returns("Some Warning Message");
}
[Test]
public void should_return_warning_if_removing_completed_downloads_is_enabled()
{
Subject.Check().ShouldBeWarning();
}
[Test]
public void should_return_ok_if_remove_completed_downloads_is_not_enabled()
{
_clientStatus.RemovesCompletedDownloads = false;
Subject.Check().ShouldBeOk();
}
[Test]
[TestCaseSource(nameof(DownloadClientExceptions))]
public void should_return_ok_if_client_throws_downloadclientexception(Exception ex)
{
_downloadClient.Setup(s => s.GetStatus())
.Throws(ex);
Subject.Check().ShouldBeOk();
ExceptionVerification.ExpectedErrors(0);
}
}
}

View File

@@ -27,6 +27,8 @@ namespace NzbDrone.Core.Test.ImportListTests
_importListReports = new List<ImportListItemInfo> { importListItem1 };
var mockImportList = new Mock<IImportList>();
Mocker.GetMock<IFetchAndParseImportList>()
.Setup(v => v.Fetch())
.Returns(_importListReports);
@@ -53,6 +55,10 @@ namespace NzbDrone.Core.Test.ImportListTests
.Setup(v => v.Get(It.IsAny<int>()))
.Returns(new ImportListDefinition { ShouldMonitor = ImportListMonitorType.SpecificBook });
Mocker.GetMock<IImportListFactory>()
.Setup(v => v.AutomaticAddEnabled(It.IsAny<bool>()))
.Returns(new List<IImportList> { mockImportList.Object });
Mocker.GetMock<IFetchAndParseImportList>()
.Setup(v => v.Fetch())
.Returns(_importListReports);
@@ -322,5 +328,31 @@ namespace NzbDrone.Core.Test.ImportListTests
t.First().AddOptions.BooksToMonitor.Count == expectedBooksMonitored &&
t.First().Monitored == expectedAuthorMonitored), false));
}
[Test]
public void should_not_fetch_if_no_lists_are_enabled()
{
Mocker.GetMock<IImportListFactory>()
.Setup(v => v.AutomaticAddEnabled(It.IsAny<bool>()))
.Returns(new List<IImportList>());
Subject.Execute(new ImportListSyncCommand());
Mocker.GetMock<IFetchAndParseImportList>()
.Verify(v => v.Fetch(), Times.Never);
}
[Test]
public void should_not_process_if_no_items_are_returned()
{
Mocker.GetMock<IFetchAndParseImportList>()
.Setup(v => v.Fetch())
.Returns(new List<ImportListItemInfo>());
Subject.Execute(new ImportListSyncCommand());
Mocker.GetMock<IImportListExclusionService>()
.Verify(v => v.All(), Times.Never);
}
}
}

View File

@@ -68,5 +68,16 @@ namespace NzbDrone.Core.Test.IndexerTests
VerifyNoUpdate();
}
[Test]
public void should_not_record_failure_for_unknown_provider()
{
Subject.RecordFailure(0);
Mocker.GetMock<IIndexerStatusRepository>()
.Verify(v => v.FindByProviderId(1), Times.Never);
VerifyNoUpdate();
}
}
}

View File

@@ -5,12 +5,15 @@ using System.IO;
using System.Linq;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Books;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common;
using NzbDrone.Test.Common.AutoMoq;
@@ -166,7 +169,7 @@ namespace NzbDrone.Core.Test.MediaFiles.AudioTagServiceFixture
}
[Test]
[TestCaseSource(typeof(TestCaseFactory), "TestCases")]
[TestCaseSource(typeof(TestCaseFactory), nameof(TestCaseFactory.TestCases))]
public void should_read_duration(string filename, string[] ignored)
{
var path = Path.Combine(_testdir, filename);
@@ -177,7 +180,7 @@ namespace NzbDrone.Core.Test.MediaFiles.AudioTagServiceFixture
}
[Test]
[TestCaseSource(typeof(TestCaseFactory), "TestCases")]
[TestCaseSource(typeof(TestCaseFactory), nameof(TestCaseFactory.TestCases))]
public void should_read_write_tags(string filename, string[] skipProperties)
{
GivenFileCopy(filename);
@@ -198,7 +201,7 @@ namespace NzbDrone.Core.Test.MediaFiles.AudioTagServiceFixture
}
[Test]
[TestCaseSource(typeof(TestCaseFactory), "TestCases")]
[TestCaseSource(typeof(TestCaseFactory), nameof(TestCaseFactory.TestCases))]
public void should_read_audiotag_from_file_with_no_tags(string filename, string[] skipProperties)
{
GivenFileCopy(filename);
@@ -220,7 +223,7 @@ namespace NzbDrone.Core.Test.MediaFiles.AudioTagServiceFixture
}
[Test]
[TestCaseSource(typeof(TestCaseFactory), "TestCases")]
[TestCaseSource(typeof(TestCaseFactory), nameof(TestCaseFactory.TestCases))]
public void should_read_parsedtrackinfo_from_file_with_no_tags(string filename, string[] skipProperties)
{
GivenFileCopy(filename);
@@ -235,7 +238,7 @@ namespace NzbDrone.Core.Test.MediaFiles.AudioTagServiceFixture
}
[Test]
[TestCaseSource(typeof(TestCaseFactory), "TestCases")]
[TestCaseSource(typeof(TestCaseFactory), nameof(TestCaseFactory.TestCases))]
public void should_set_quality_and_mediainfo_for_corrupt_file(string filename, string[] skipProperties)
{
// use missing to simulate corrupt
@@ -250,7 +253,7 @@ namespace NzbDrone.Core.Test.MediaFiles.AudioTagServiceFixture
}
[Test]
[TestCaseSource(typeof(TestCaseFactory), "TestCases")]
[TestCaseSource(typeof(TestCaseFactory), nameof(TestCaseFactory.TestCases))]
public void should_read_file_with_only_title_tag(string filename, string[] ignored)
{
GivenFileCopy(filename);
@@ -270,7 +273,7 @@ namespace NzbDrone.Core.Test.MediaFiles.AudioTagServiceFixture
}
[Test]
[TestCaseSource(typeof(TestCaseFactory), "TestCases")]
[TestCaseSource(typeof(TestCaseFactory), nameof(TestCaseFactory.TestCases))]
public void should_remove_date_from_tags_when_not_in_metadata(string filename, string[] ignored)
{
GivenFileCopy(filename);
@@ -365,6 +368,29 @@ namespace NzbDrone.Core.Test.MediaFiles.AudioTagServiceFixture
var fileInfo = _diskProvider.GetFileInfo(file.Path);
file.Modified.Should().Be(fileInfo.LastWriteTimeUtc);
file.Size.Should().Be(fileInfo.Length);
Mocker.GetMock<IEventAggregator>()
.Verify(v => v.PublishEvent(It.IsAny<BookFileRetaggedEvent>()), Times.Once());
}
[TestCase("nin.mp3")]
public void write_tags_should_not_update_tags_if_already_updated(string filename)
{
Mocker.GetMock<IConfigService>()
.Setup(x => x.ScrubAudioTags)
.Returns(true);
GivenFileCopy(filename);
var file = GivenPopulatedTrackfile(0);
file.Path = _copiedFile;
Subject.WriteTags(file, false, true);
Subject.WriteTags(file, false, true);
Subject.WriteTags(file, false, true);
Mocker.GetMock<IEventAggregator>()
.Verify(v => v.PublishEvent(It.IsAny<BookFileRetaggedEvent>()), Times.Once());
}
[Test]

View File

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

View File

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

View File

@@ -64,6 +64,11 @@ namespace NzbDrone.Core.Test.NotificationTests
TestLogger.Info("OnRename was called");
}
public override void OnAuthorAdded(Author author)
{
TestLogger.Info("OnAuthorAdded was called");
}
public override void OnAuthorDelete(AuthorDeleteMessage message)
{
TestLogger.Info("OnAuthorDelete was called");
@@ -138,6 +143,7 @@ namespace NzbDrone.Core.Test.NotificationTests
notification.SupportsOnUpgrade.Should().BeTrue();
notification.SupportsOnRename.Should().BeTrue();
notification.SupportsOnHealthIssue.Should().BeTrue();
notification.SupportsOnAuthorAdded.Should().BeTrue();
notification.SupportsOnAuthorDelete.Should().BeTrue();
notification.SupportsOnBookDelete.Should().BeTrue();
notification.SupportsOnBookFileDelete.Should().BeTrue();
@@ -157,6 +163,7 @@ namespace NzbDrone.Core.Test.NotificationTests
notification.SupportsOnReleaseImport.Should().BeFalse();
notification.SupportsOnUpgrade.Should().BeFalse();
notification.SupportsOnRename.Should().BeFalse();
notification.SupportsOnAuthorAdded.Should().BeFalse();
notification.SupportsOnAuthorDelete.Should().BeFalse();
notification.SupportsOnBookDelete.Should().BeFalse();
notification.SupportsOnBookFileDelete.Should().BeFalse();

View File

@@ -35,16 +35,13 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("or")]
[TestCase("an")]
[TestCase("of")]
public void should_remove_common_words(string word)
public void should_remove_common_words_from_middle_of_title(string word)
{
var dirtyFormat = new[]
{
"word.{0}.word",
"word {0} word",
"word-{0}-word",
"word.word.{0}",
"word-word-{0}",
"word-word {0}",
"word-{0}-word"
};
foreach (var s in dirtyFormat)
@@ -54,6 +51,27 @@ namespace NzbDrone.Core.Test.ParserTests
}
}
[TestCase("the")]
[TestCase("and")]
[TestCase("or")]
[TestCase("an")]
[TestCase("of")]
public void should_not_remove_common_words_from_end_of_title(string word)
{
var dirtyFormat = new[]
{
"word.word.{0}",
"word-word-{0}",
"word-word {0}"
};
foreach (var s in dirtyFormat)
{
var dirty = string.Format(s, word);
dirty.CleanAuthorName().Should().Be("wordword" + word.ToLower());
}
}
[Test]
public void should_remove_a_from_middle_of_title()
{

View File

@@ -8,6 +8,7 @@ using System.Threading;
using System.Threading.Tasks;
using FluentValidation;
using FluentValidation.Results;
using Newtonsoft.Json;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Disk;
@@ -218,11 +219,11 @@ namespace NzbDrone.Core.Books.Calibre
double? seriesIndex = null;
if (double.TryParse(serieslink?.Position, out var index))
{
_logger.Trace($"Parsed {serieslink?.Position} as {index}");
_logger.Trace("Parsed '{0}' as '{1}'", serieslink.Position, index);
seriesIndex = index;
}
_logger.Trace($"Book: {book} Series: {series?.Title}, Position: {seriesIndex}");
_logger.Trace("Book: {0} Series: {1}, Position: {2}", book, series?.Title, seriesIndex);
var cover = edition.Images.FirstOrDefault(x => x.CoverType == MediaCoverTypes.Cover);
string image = null;
@@ -275,7 +276,9 @@ namespace NzbDrone.Core.Books.Calibre
var updatedPath = GetOriginalFormat(updated.Formats);
if (updatedPath != file.Path)
_logger.Trace("File path from Calibre: '{0}'", updatedPath);
if (updatedPath.IsNotNullOrWhiteSpace() && updatedPath != file.Path)
{
_rootFolderWatchingService.ReportFileSystemChangeBeginning(updatedPath);
file.Path = updatedPath;
@@ -304,6 +307,7 @@ namespace NzbDrone.Core.Books.Calibre
var request = builder.Build();
request.SetContent(payload.ToJson());
request.ContentSummary = payload.ToJson(Formatting.None);
_httpClient.Execute(request);
}

View File

@@ -322,6 +322,20 @@ namespace NzbDrone.Core.Configuration
}
}
public void MigrateConfigFile()
{
if (!File.Exists(_configFile))
{
return;
}
// If SSL is enabled and a cert hash is still in the config file disable SSL
if (EnableSsl && GetValue("SslCertHash", null).IsNotNullOrWhiteSpace())
{
SetValue("EnableSsl", false);
}
}
private void DeleteOldValues()
{
var xDoc = LoadConfigFile();
@@ -404,6 +418,7 @@ namespace NzbDrone.Core.Configuration
public void HandleAsync(ApplicationStartedEvent message)
{
MigrateConfigFile();
EnsureDefaultConfigFile();
DeleteOldValues();
}

View File

@@ -142,7 +142,7 @@ namespace NzbDrone.Core.CustomFormats
}
}
return matches;
return matches.OrderBy(x => x.Name).ToList();
}
private static List<CustomFormat> ParseCustomFormat(BookFile bookFile, Author author, List<CustomFormat> allCustomFormats)

View File

@@ -9,9 +9,9 @@ namespace NzbDrone.Core.Datastore
{
public interface IConnectionStringFactory
{
string MainDbConnectionString { get; }
string LogDbConnectionString { get; }
string CacheDbConnectionString { get; }
DatabaseConnectionInfo MainDbConnection { get; }
DatabaseConnectionInfo LogDbConnection { get; }
DatabaseConnectionInfo CacheDbConnection { get; }
string GetDatabasePath(string connectionString);
}
@@ -23,19 +23,19 @@ namespace NzbDrone.Core.Datastore
{
_configFileProvider = configFileProvider;
MainDbConnectionString = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresMainDb) :
MainDbConnection = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresMainDb) :
GetConnectionString(appFolderInfo.GetDatabase());
LogDbConnectionString = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresLogDb) :
LogDbConnection = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresLogDb) :
GetConnectionString(appFolderInfo.GetLogDatabase());
CacheDbConnectionString = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresCacheDb) :
CacheDbConnection = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresCacheDb) :
GetConnectionString(appFolderInfo.GetCacheDatabase());
}
public string MainDbConnectionString { get; private set; }
public string LogDbConnectionString { get; private set; }
public string CacheDbConnectionString { get; private set; }
public DatabaseConnectionInfo MainDbConnection { get; private set; }
public DatabaseConnectionInfo LogDbConnection { get; private set; }
public DatabaseConnectionInfo CacheDbConnection { get; private set; }
public string GetDatabasePath(string connectionString)
{
@@ -44,37 +44,40 @@ namespace NzbDrone.Core.Datastore
return connectionBuilder.DataSource;
}
private static string GetConnectionString(string dbPath)
private static DatabaseConnectionInfo GetConnectionString(string dbPath)
{
var connectionBuilder = new SQLiteConnectionStringBuilder();
connectionBuilder.DataSource = dbPath;
connectionBuilder.CacheSize = -10000;
connectionBuilder.DateTimeKind = DateTimeKind.Utc;
connectionBuilder.JournalMode = OsInfo.IsOsx ? SQLiteJournalModeEnum.Truncate : SQLiteJournalModeEnum.Wal;
connectionBuilder.Pooling = true;
connectionBuilder.Version = 3;
var connectionBuilder = new SQLiteConnectionStringBuilder
{
DataSource = dbPath,
CacheSize = -20000,
DateTimeKind = DateTimeKind.Utc,
JournalMode = OsInfo.IsOsx ? SQLiteJournalModeEnum.Truncate : SQLiteJournalModeEnum.Wal,
Pooling = true,
Version = 3,
BusyTimeout = 100
};
if (OsInfo.IsOsx)
{
connectionBuilder.Add("Full FSync", true);
}
return connectionBuilder.ConnectionString;
return new DatabaseConnectionInfo(DatabaseType.SQLite, connectionBuilder.ConnectionString);
}
private string GetPostgresConnectionString(string dbName)
private DatabaseConnectionInfo GetPostgresConnectionString(string dbName)
{
var connectionBuilder = new NpgsqlConnectionStringBuilder();
var connectionBuilder = new NpgsqlConnectionStringBuilder
{
Database = dbName,
Host = _configFileProvider.PostgresHost,
Username = _configFileProvider.PostgresUser,
Password = _configFileProvider.PostgresPassword,
Port = _configFileProvider.PostgresPort,
Enlist = false
};
connectionBuilder.Database = dbName;
connectionBuilder.Host = _configFileProvider.PostgresHost;
connectionBuilder.Username = _configFileProvider.PostgresUser;
connectionBuilder.Password = _configFileProvider.PostgresPassword;
connectionBuilder.Port = _configFileProvider.PostgresPort;
connectionBuilder.Enlist = false;
return connectionBuilder.ConnectionString;
return new DatabaseConnectionInfo(DatabaseType.PostgreSQL, connectionBuilder.ConnectionString);
}
}
}

View File

@@ -0,0 +1,14 @@
namespace NzbDrone.Core.Datastore
{
public class DatabaseConnectionInfo
{
public DatabaseConnectionInfo(DatabaseType databaseType, string connectionString)
{
DatabaseType = databaseType;
ConnectionString = connectionString;
}
public DatabaseType DatabaseType { get; internal set; }
public string ConnectionString { get; internal set; }
}
}

View File

@@ -2,6 +2,7 @@ using System;
using System.Data.Common;
using System.Data.SQLite;
using System.Net.Sockets;
using System.Threading;
using NLog;
using Npgsql;
using NzbDrone.Common.Disk;
@@ -59,30 +60,30 @@ namespace NzbDrone.Core.Datastore
public IDatabase Create(MigrationContext migrationContext)
{
string connectionString;
DatabaseConnectionInfo connectionInfo;
switch (migrationContext.MigrationType)
{
case MigrationType.Main:
{
connectionString = _connectionStringFactory.MainDbConnectionString;
CreateMain(connectionString, migrationContext);
connectionInfo = _connectionStringFactory.MainDbConnection;
CreateMain(connectionInfo.ConnectionString, migrationContext, connectionInfo.DatabaseType);
break;
}
case MigrationType.Log:
{
connectionString = _connectionStringFactory.LogDbConnectionString;
CreateLog(connectionString, migrationContext);
connectionInfo = _connectionStringFactory.LogDbConnection;
CreateLog(connectionInfo.ConnectionString, migrationContext, connectionInfo.DatabaseType);
break;
}
case MigrationType.Cache:
{
connectionString = _connectionStringFactory.CacheDbConnectionString;
CreateLog(connectionString, migrationContext);
connectionInfo = _connectionStringFactory.CacheDbConnection;
CreateLog(connectionInfo.ConnectionString, migrationContext, connectionInfo.DatabaseType);
break;
}
@@ -97,14 +98,14 @@ namespace NzbDrone.Core.Datastore
{
DbConnection conn;
if (connectionString.Contains(".db"))
if (connectionInfo.DatabaseType == DatabaseType.SQLite)
{
conn = SQLiteFactory.Instance.CreateConnection();
conn.ConnectionString = connectionString;
conn.ConnectionString = connectionInfo.ConnectionString;
}
else
{
conn = new NpgsqlConnection(connectionString);
conn = new NpgsqlConnection(connectionInfo.ConnectionString);
}
conn.Open();
@@ -114,12 +115,12 @@ namespace NzbDrone.Core.Datastore
return db;
}
private void CreateMain(string connectionString, MigrationContext migrationContext)
private void CreateMain(string connectionString, MigrationContext migrationContext, DatabaseType databaseType)
{
try
{
_restoreDatabaseService.Restore();
_migrationController.Migrate(connectionString, migrationContext);
_migrationController.Migrate(connectionString, migrationContext, databaseType);
}
catch (SQLiteException e)
{
@@ -142,15 +143,17 @@ namespace NzbDrone.Core.Datastore
{
Logger.Error(e, "Failure to connect to Postgres DB, {0} retries remaining", retryCount);
Thread.Sleep(5000);
try
{
_migrationController.Migrate(connectionString, migrationContext);
_migrationController.Migrate(connectionString, migrationContext, databaseType);
return;
}
catch (Exception ex)
{
if (--retryCount > 0)
{
System.Threading.Thread.Sleep(5000);
continue;
}
@@ -169,11 +172,11 @@ namespace NzbDrone.Core.Datastore
}
}
private void CreateLog(string connectionString, MigrationContext migrationContext)
private void CreateLog(string connectionString, MigrationContext migrationContext, DatabaseType databaseType)
{
try
{
_migrationController.Migrate(connectionString, migrationContext);
_migrationController.Migrate(connectionString, migrationContext, databaseType);
}
catch (SQLiteException e)
{
@@ -193,7 +196,7 @@ namespace NzbDrone.Core.Datastore
Logger.Error("Unable to recreate logging database automatically. It will need to be removed manually.");
}
_migrationController.Migrate(connectionString, migrationContext);
_migrationController.Migrate(connectionString, migrationContext, databaseType);
}
catch (Exception e)
{

View File

@@ -0,0 +1,14 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(038)]
public class add_on_author_added_to_notifications : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Alter.Table("Notifications").AddColumn("OnAuthorAdded").AsBoolean().WithDefaultValue(false);
}
}
}

View File

@@ -14,7 +14,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
{
public interface IMigrationController
{
void Migrate(string connectionString, MigrationContext migrationContext);
void Migrate(string connectionString, MigrationContext migrationContext, DatabaseType databaseType);
}
public class MigrationController : IMigrationController
@@ -29,7 +29,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
_migrationLoggerProvider = migrationLoggerProvider;
}
public void Migrate(string connectionString, MigrationContext migrationContext)
public void Migrate(string connectionString, MigrationContext migrationContext, DatabaseType databaseType)
{
var sw = Stopwatch.StartNew();
@@ -37,7 +37,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
ServiceProvider serviceProvider;
var db = connectionString.Contains(".db") ? "sqlite" : "postgres";
var db = databaseType == DatabaseType.SQLite ? "sqlite" : "postgres";
serviceProvider = new ServiceCollection()
.AddLogging(b => b.AddNLog())

View File

@@ -85,6 +85,7 @@ namespace NzbDrone.Core.Datastore
.Ignore(i => i.SupportsOnReleaseImport)
.Ignore(i => i.SupportsOnUpgrade)
.Ignore(i => i.SupportsOnRename)
.Ignore(i => i.SupportsOnAuthorAdded)
.Ignore(i => i.SupportsOnAuthorDelete)
.Ignore(i => i.SupportsOnBookDelete)
.Ignore(i => i.SupportsOnBookFileDelete)

View File

@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using NLog;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.CustomFormats;
using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.IndexerSearch.Definitions;
@@ -15,16 +16,19 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
private readonly IQueueService _queueService;
private readonly UpgradableSpecification _upgradableSpecification;
private readonly ICustomFormatCalculationService _formatService;
private readonly IConfigService _configService;
private readonly Logger _logger;
public QueueSpecification(IQueueService queueService,
UpgradableSpecification upgradableSpecification,
ICustomFormatCalculationService formatService,
IConfigService configService,
Logger logger)
{
_queueService = queueService;
_upgradableSpecification = upgradableSpecification;
_formatService = formatService;
_configService = configService;
_logger = logger;
}
@@ -85,6 +89,15 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
{
return Decision.Reject("Another release is queued and the Quality profile does not allow upgrades");
}
if (_upgradableSpecification.IsRevisionUpgrade(remoteBook.ParsedBookInfo.Quality, subject.ParsedBookInfo.Quality))
{
if (_configService.DownloadPropersAndRepacks == ProperDownloadTypes.DoNotUpgrade)
{
_logger.Debug("Auto downloading of propers is disabled");
return Decision.Reject("Proper downloading is disabled");
}
}
}
return Decision.Accept();

View File

@@ -1,6 +1,7 @@
using System.Linq;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser.Model;
@@ -10,12 +11,12 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
public class IndexerTagSpecification : IDecisionEngineSpecification
{
private readonly Logger _logger;
private readonly IIndexerRepository _indexerRepository;
private readonly IIndexerFactory _indexerFactory;
public IndexerTagSpecification(Logger logger, IIndexerRepository indexerRepository)
public IndexerTagSpecification(Logger logger, IIndexerFactory indexerFactory)
{
_logger = logger;
_indexerRepository = indexerRepository;
_indexerFactory = indexerFactory;
}
public SpecificationPriority Priority => SpecificationPriority.Default;
@@ -23,8 +24,24 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
public virtual Decision IsSatisfiedBy(RemoteBook subject, SearchCriteriaBase searchCriteria)
{
// If indexer has tags, check that at least one of them is present on the author
var indexerTags = _indexerRepository.Get(subject.Release.IndexerId).Tags;
if (subject.Release == null || subject.Author?.Tags == null || subject.Release.IndexerId == 0)
{
return Decision.Accept();
}
IndexerDefinition indexer;
try
{
indexer = _indexerFactory.Get(subject.Release.IndexerId);
}
catch (ModelNotFoundException)
{
_logger.Debug("Indexer with id {0} does not exist, skipping indexer tags check", subject.Release.IndexerId);
return Decision.Accept();
}
// If indexer has tags, check that at least one of them is present on the series
var indexerTags = indexer.Tags;
if (indexerTags.Any() && indexerTags.Intersect(subject.Author.Tags).Empty())
{

View File

@@ -304,13 +304,13 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
break;
}
if (version >= new Version("2.6.1"))
if (version >= new Version("2.6.1") && item.Status == DownloadItemStatus.Completed)
{
if (torrent.ContentPath != torrent.SavePath)
{
item.OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.ContentPath));
}
else if (item.Status == DownloadItemStatus.Completed)
else
{
item.Status = DownloadItemStatus.Warning;
item.Message = "Unable to Import. Path matches client base download directory, it's possible 'Keep top-level folder' is disabled for this torrent or 'Torrent Content Layout' is NOT set to 'Original' or 'Create Subfolder'?";
@@ -386,10 +386,13 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
}
}
var minimumRetention = 60 * 24 * 14;
return new DownloadClientInfo
{
IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost",
OutputRootFolders = new List<OsPath> { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, destDir) }
OutputRootFolders = new List<OsPath> { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, destDir) },
RemovesCompletedDownloads = (config.MaxRatioEnabled || (config.MaxSeedingTimeEnabled && config.MaxSeedingTime < minimumRetention)) && (config.MaxRatioAction == QBittorrentMaxRatioAction.Remove || config.MaxRatioAction == QBittorrentMaxRatioAction.DeleteFiles)
};
}

View File

@@ -263,6 +263,17 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
status.OutputRootFolders = new List<OsPath> { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, category.FullPath) };
}
if (config.Misc.history_retention.IsNotNullOrWhiteSpace() && config.Misc.history_retention.EndsWith("d"))
{
int.TryParse(config.Misc.history_retention.AsSpan(0, config.Misc.history_retention.Length - 1),
out var daysRetention);
status.RemovesCompletedDownloads = daysRetention < 14;
}
else
{
status.RemovesCompletedDownloads = config.Misc.history_retention != "0";
}
return status;
}

View File

@@ -29,6 +29,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
public string[] date_categories { get; set; }
public bool enable_date_sorting { get; set; }
public bool pre_check { get; set; }
public string history_retention { get; set; }
}
public class SabnzbdCategory

View File

@@ -11,6 +11,7 @@ namespace NzbDrone.Core.Download
}
public bool IsLocalhost { get; set; }
public bool RemovesCompletedDownloads { get; set; }
public List<OsPath> OutputRootFolders { get; set; }
}
}

View File

@@ -346,6 +346,7 @@ namespace NzbDrone.Core.Extras.Metadata
private void DownloadImage(Author author, ImageFileResult image)
{
var fullPath = Path.Combine(author.Path, image.RelativePath);
var downloaded = true;
try
{
@@ -353,12 +354,23 @@ namespace NzbDrone.Core.Extras.Metadata
{
_httpClient.DownloadFile(image.Url, fullPath);
}
else
else if (_diskProvider.FileExists(image.Url))
{
_diskProvider.CopyFile(image.Url, fullPath);
}
else
{
downloaded = false;
}
_mediaFileAttributeService.SetFilePermissions(fullPath);
if (downloaded)
{
_mediaFileAttributeService.SetFilePermissions(fullPath);
}
}
catch (HttpException ex)
{
_logger.Warn(ex, "Couldn't download image {0} for {1}. {2}", image.Url, author, ex.Message);
}
catch (WebException ex)
{

View File

@@ -0,0 +1,64 @@
using System;
using NLog;
using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.Clients;
using NzbDrone.Core.Localization;
using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.RootFolders;
using NzbDrone.Core.ThingiProvider.Events;
namespace NzbDrone.Core.HealthCheck.Checks
{
[CheckOn(typeof(ProviderUpdatedEvent<IDownloadClient>))]
[CheckOn(typeof(ProviderDeletedEvent<IDownloadClient>))]
[CheckOn(typeof(ModelEvent<RootFolder>))]
[CheckOn(typeof(ModelEvent<RemotePathMapping>))]
public class DownloadClientRemovesCompletedDownloadsCheck : HealthCheckBase, IProvideHealthCheck
{
private readonly IProvideDownloadClient _downloadClientProvider;
private readonly Logger _logger;
public DownloadClientRemovesCompletedDownloadsCheck(IProvideDownloadClient downloadClientProvider,
Logger logger,
ILocalizationService localizationService)
: base(localizationService)
{
_downloadClientProvider = downloadClientProvider;
_logger = logger;
}
public override HealthCheck Check()
{
var clients = _downloadClientProvider.GetDownloadClients(true);
foreach (var client in clients)
{
try
{
var clientName = client.Definition.Name;
var status = client.GetStatus();
if (status.RemovesCompletedDownloads)
{
return new HealthCheck(GetType(),
HealthCheckResult.Warning,
string.Format(_localizationService.GetLocalizedString("DownloadClientRemovesCompletedDownloadsHealthCheckMessage"), clientName, "Readarr"),
"#download-client-removes-completed-downloads");
}
}
catch (DownloadClientException ex)
{
_logger.Debug(ex, "Unable to communicate with {0}", client.Definition.Name);
}
catch (Exception ex)
{
_logger.Error(ex, "Unknown error occurred in DownloadClientHistoryRetentionCheck HealthCheck");
}
}
return new HealthCheck(GetType());
}
}
}

View File

@@ -16,15 +16,30 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{
using var mapper = _database.OpenConnection();
mapper.Execute(@"UPDATE ""Editions""
SET ""Monitored"" = 0
WHERE ""Id"" IN (
SELECT MIN(""Id"")
FROM ""Editions""
WHERE ""Monitored"" = 1
GROUP BY ""BookId""
HAVING COUNT(""BookId"") > 1
)");
if (_database.DatabaseType == DatabaseType.PostgreSQL)
{
mapper.Execute(@"UPDATE ""Editions""
SET ""Monitored"" = true
WHERE ""Id"" IN (
SELECT MIN(""Id"")
FROM ""Editions""
WHERE ""Monitored"" = true
GROUP BY ""BookId""
HAVING COUNT(""BookId"") > 1
)");
}
else
{
mapper.Execute(@"UPDATE ""Editions""
SET ""Monitored"" = 0
WHERE ""Id"" IN (
SELECT MIN(""Id"")
FROM ""Editions""
WHERE ""Monitored"" = 1
GROUP BY ""BookId""
HAVING COUNT(""BookId"") > 1
)");
}
}
}
}

View File

@@ -67,41 +67,51 @@ namespace NzbDrone.Core.ImportLists
private List<Book> SyncAll()
{
if (_importListFactory.AutomaticAddEnabled().Empty())
{
_logger.Debug("No import lists with automatic add enabled");
return new List<Book>();
}
_logger.ProgressInfo("Starting Import List Sync");
var rssReleases = _listFetcherAndParser.Fetch();
var listItems = _listFetcherAndParser.Fetch().ToList();
var reports = rssReleases.ToList();
return ProcessReports(reports);
return ProcessListItems(listItems);
}
private List<Book> SyncList(ImportListDefinition definition)
{
_logger.ProgressInfo(string.Format("Starting Import List Refresh for List {0}", definition.Name));
_logger.ProgressInfo($"Starting Import List Refresh for List {definition.Name}");
var rssReleases = _listFetcherAndParser.FetchSingleList(definition);
var listItems = _listFetcherAndParser.FetchSingleList(definition).ToList();
var reports = rssReleases.ToList();
return ProcessReports(reports);
return ProcessListItems(listItems);
}
private List<Book> ProcessReports(List<ImportListItemInfo> reports)
private List<Book> ProcessListItems(List<ImportListItemInfo> items)
{
var processed = new List<Book>();
var authorsToAdd = new List<Author>();
var booksToAdd = new List<Book>();
_logger.ProgressInfo("Processing {0} list items", reports.Count);
if (items.Count == 0)
{
_logger.ProgressInfo("No list items to process");
return new List<Book>();
}
_logger.ProgressInfo("Processing {0} list items", items.Count);
var reportNumber = 1;
var listExclusions = _importListExclusionService.All();
foreach (var report in reports)
foreach (var report in items)
{
_logger.ProgressTrace("Processing list item {0}/{1}", reportNumber, reports.Count);
_logger.ProgressTrace("Processing list item {0}/{1}", reportNumber, items.Count);
reportNumber++;
@@ -130,7 +140,7 @@ namespace NzbDrone.Core.ImportLists
var addedAuthors = _addAuthorService.AddAuthors(authorsToAdd, false);
var addedBooks = _addBookService.AddBooks(booksToAdd, false);
var message = string.Format($"Import List Sync Completed. Items found: {reports.Count}, Authors added: {authorsToAdd.Count}, Books added: {booksToAdd.Count}");
var message = string.Format($"Import List Sync Completed. Items found: {items.Count}, Authors added: {authorsToAdd.Count}, Books added: {booksToAdd.Count}");
_logger.ProgressInfo(message);
@@ -364,7 +374,7 @@ namespace NzbDrone.Core.ImportLists
var existingAuthor = _authorService.FindById(report.AuthorGoodreadsId);
// Check to see if author excluded
var excludedAuthor = listExclusions.Where(s => s.ForeignId == report.AuthorGoodreadsId).SingleOrDefault();
var excludedAuthor = listExclusions.SingleOrDefault(s => s.ForeignId == report.AuthorGoodreadsId);
// Check to see if author in import
var existingImportAuthor = authorsToAdd.Find(i => i.ForeignAuthorId == report.AuthorGoodreadsId);
@@ -425,16 +435,7 @@ namespace NzbDrone.Core.ImportLists
public void Execute(ImportListSyncCommand message)
{
List<Book> processed;
if (message.DefinitionId.HasValue)
{
processed = SyncList(_importListFactory.Get(message.DefinitionId.Value));
}
else
{
processed = SyncAll();
}
var processed = message.DefinitionId.HasValue ? SyncList(_importListFactory.Get(message.DefinitionId.Value)) : SyncAll();
_eventAggregator.PublishEvent(new ImportListSyncCompleteEvent(processed));
}

View File

@@ -257,9 +257,17 @@ namespace NzbDrone.Core.Indexers
protected virtual bool IsValidRelease(ReleaseInfo release)
{
if (release.Title.IsNullOrWhiteSpace())
{
_logger.Trace("Invalid Release: '{0}' from indexer: {1}. No title provided.", release.InfoUrl, Definition.Name);
return false;
}
if (release.DownloadUrl.IsNullOrWhiteSpace())
{
_logger.Trace("Invalid Release: '{0}' from indexer: {1}. No Download URL provided.", release.Title, release.Indexer);
_logger.Trace("Invalid Release: '{0}' from indexer: {1}. No Download URL provided.", release.Title, Definition.Name);
return false;
}

View File

@@ -53,7 +53,6 @@ namespace NzbDrone.Core.Indexers.Newznab
yield return GetDefinition("NZBFinder.ws", GetSettings("https://nzbfinder.ws"));
yield return GetDefinition("NZBgeek", GetSettings("https://api.nzbgeek.info"));
yield return GetDefinition("nzbplanet.net", GetSettings("https://api.nzbplanet.net"));
yield return GetDefinition("OZnzb.com", GetSettings("https://api.oznzb.com"));
yield return GetDefinition("SimplyNZBs", GetSettings("https://simplynzbs.com"));
yield return GetDefinition("Tabula Rasa", GetSettings("https://www.tabula-rasa.pw", apiPath: @"/api/v1/api"));
yield return GetDefinition("Usenet Crawler", GetSettings("https://www.usenet-crawler.com"));

View File

@@ -7,6 +7,7 @@ using NzbDrone.Common.Cache;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Indexers.Exceptions;
namespace NzbDrone.Core.Indexers.Newznab
{
@@ -73,6 +74,13 @@ namespace NzbDrone.Core.Indexers.Newznab
_logger.Debug(ex, "Failed to parse newznab api capabilities for {0}", indexerSettings.BaseUrl);
throw;
}
catch (ApiKeyException ex)
{
ex.WithData(response, 128 * 1024);
_logger.Trace("Unexpected Response content ({0} bytes): {1}", response.ResponseData.Length, response.Content);
_logger.Debug(ex, "Failed to parse newznab api capabilities for {0}, invalid API key", indexerSettings.BaseUrl);
throw;
}
catch (Exception ex)
{
ex.WithData(response, 128 * 1024);

View File

@@ -85,16 +85,15 @@ namespace NzbDrone.Core.Instrumentation
log.Level = logEvent.Level.Name;
var connectionString = _connectionStringFactory.LogDbConnectionString;
var connectionInfo = _connectionStringFactory.LogDbConnection;
//TODO: Probably need more robust way to differentiate what's being used
if (connectionString.Contains(".db"))
if (connectionInfo.DatabaseType == DatabaseType.SQLite)
{
WriteSqliteLog(log, connectionString);
WriteSqliteLog(log, connectionInfo.ConnectionString);
}
else
{
WritePostgresLog(log, connectionString);
WritePostgresLog(log, connectionInfo.ConnectionString);
}
}
catch (SQLiteException ex)

View File

@@ -216,8 +216,6 @@
"FileManagement": "إدارة الملفات",
"FileDateHelpText": "تغيير تاريخ الملف عند الاستيراد / إعادة الفحص",
"FailedDownloadHandling": "فشل معالجة التنزيل",
"ExtraFileExtensionsHelpTexts2": "أمثلة: \".sub أو .nfo\" أو \"sub، nfo\"",
"ExtraFileExtensionsHelpTexts1": "قائمة مفصولة بفواصل بالملفات الإضافية المراد استيرادها (سيتم استيراد .nfo كـ .nfo-Orig)",
"Exception": "استثناء",
"ErrorLoadingPreviews": "خطأ في تحميل المعاينات",
"ErrorLoadingContents": "خطأ في تحميل المحتويات",
@@ -628,5 +626,7 @@
"AddNew": "اضف جديد",
"System": "النظام",
"AllResultsAreHiddenByTheAppliedFilter": "يتم إخفاء جميع النتائج بواسطة عامل التصفية المطبق",
"Backup": "دعم"
"Backup": "دعم",
"ExtraFileExtensionsHelpText": "قائمة مفصولة بفواصل بالملفات الإضافية المراد استيرادها (سيتم استيراد .nfo كـ .nfo-Orig)",
"ExtraFileExtensionsHelpTextsExamples": "أمثلة: \".sub أو .nfo\" أو \"sub، nfo\""
}

View File

@@ -122,8 +122,6 @@
"ErrorLoadingContents": "Грешка при зареждането на съдържанието",
"ErrorLoadingPreviews": "Грешка при зареждането на визуализациите",
"Exception": "Изключение",
"ExtraFileExtensionsHelpTexts1": "Списък с допълнителни файлове за импортиране, разделени със запетая (.nfo ще бъде импортиран като .nfo-orig)",
"ExtraFileExtensionsHelpTexts2": "Примери: '.sub, .nfo' или 'sub, nfo'",
"FailedDownloadHandling": "Неуспешно обработване на изтеглянето",
"FileDateHelpText": "Променете датата на файла при импортиране / пресканиране",
"FileManagement": "Управление на файлове",
@@ -626,5 +624,9 @@
"AddNew": "Добави нов",
"NextExecution": "Следващо изпълнение",
"AllResultsAreHiddenByTheAppliedFilter": "Всички резултати са скрити от приложения филтър",
"Backup": "Архивиране"
"Backup": "Архивиране",
"MetadataProfiles": "Добави профил на метадата",
"MetadataProfile": "Добави профил на метадата",
"ExtraFileExtensionsHelpText": "Списък с допълнителни файлове за импортиране, разделени със запетая (.nfo ще бъде импортиран като .nfo-orig)",
"ExtraFileExtensionsHelpTextsExamples": "Примери: '.sub, .nfo' или 'sub, nfo'"
}

View File

@@ -326,7 +326,6 @@
"DownloadClientStatusCheckAllClientMessage": "Tots els clients de descàrrega no estan disponibles a causa d'errors",
"DownloadClientStatusCheckSingleClientMessage": "Baixa els clients no disponibles a causa d'errors: {0}",
"Duration": "durada",
"ExtraFileExtensionsHelpTexts1": "Llista separada per comes de fitxers addicionals per importar (.nfo s'importarà com a .nfo-orig)",
"FileWasDeletedByViaUI": "El fitxer s'ha suprimit mitjançant la interfície d'usuari",
"GeneralSettingsSummary": "Port, SSL, nom d'usuari/contrasenya, servidor intermediari, analítiques i actualitzacions",
"GrabRelease": "Captura novetat",
@@ -528,7 +527,6 @@
"CutoffUnmet": "Tall no assolit",
"DeleteDelayProfileMessageText": "Esteu segur que voleu suprimir aquest perfil de retard?",
"DeleteImportListExclusionMessageText": "Esteu segur que voleu suprimir aquesta exclusió de la llista d'importació?",
"ExtraFileExtensionsHelpTexts2": "Exemples: '.sub, .nfo' o 'sub,nfo'",
"HasPendingChangesSaveChanges": "Desa els canvis",
"ICalLink": "Enllaç iCal`",
"IgnoredHelpText": "La publicació es rebutjarà si conté un o més dels termes (no distingeix entre majúscules i minúscules)",

View File

@@ -80,19 +80,19 @@
"DeleteDelayProfile": "Odstranění profilu zpoždění",
"DeleteDelayProfileMessageText": "Opravdu chcete smazat tento profil zpoždění?",
"DeleteDownloadClient": "Odstranění klienta pro stahování",
"DeleteDownloadClientMessageText": "Opravdu chcete odstranit klienta pro stahování „{0}“?",
"DeleteDownloadClientMessageText": "Opravdu chcete odstranit klienta pro stahování '{name}'?",
"DeleteEmptyFolders": "Odstraňte prázdné složky",
"DeleteEmptyFoldersHelpText": "Během skenování disku a při mazání filmových souborů odstraňte prázdné složky s filmy",
"DeleteImportListExclusion": "Odstranit vyloučení seznamu importů",
"DeleteImportListExclusionMessageText": "Opravdu chcete toto vyloučení importního seznamu smazat?",
"DeleteImportListMessageText": "Opravdu chcete smazat seznam „{0}“?",
"DeleteImportListMessageText": "Opravdu chcete smazat seznam '{name}'?",
"DeleteIndexer": "Odstranit indexer",
"DeleteIndexerMessageText": "Opravdu chcete odstranit indexer „{0}“?",
"DeleteMetadataProfileMessageText": "Opravdu chcete smazat kvalitní profil {0}",
"DeleteIndexerMessageText": "Opravdu chcete odstranit indexer '{name}'?",
"DeleteMetadataProfileMessageText": "Opravdu chcete smazat profil metadat '{name}'?",
"DeleteNotification": "Smazat oznámení",
"DeleteNotificationMessageText": "Opravdu chcete smazat oznámení „{0}“?",
"DeleteNotificationMessageText": "Opravdu chcete smazat oznámení '{name}'?",
"DeleteQualityProfile": "Smažte profil kvality",
"DeleteQualityProfileMessageText": "Opravdu chcete smazat kvalitní profil {0}",
"DeleteQualityProfileMessageText": "Opravdu chcete smazat profil kvality '{name}'?",
"DeleteReleaseProfile": "Smazat profil zpoždění",
"DeleteReleaseProfileMessageText": "Opravdu chcete smazat tento profil zpoždění?",
"DeleteSelectedBookFiles": "Odstranit vybrané filmové soubory",
@@ -128,8 +128,6 @@
"ErrorLoadingContents": "Chyba při načítání obsahu",
"ErrorLoadingPreviews": "Chyba při načítání náhledů",
"Exception": "Výjimka",
"ExtraFileExtensionsHelpTexts1": "Seznam extra souborů k importu oddělených čárkami (.nfo bude importován jako .nfo-orig)",
"ExtraFileExtensionsHelpTexts2": "Příklady: „.sub, .nfo“ nebo „sub, nfo“",
"FailedDownloadHandling": "Zpracování stahování se nezdařilo",
"FileDateHelpText": "Změnit datum souboru při importu / opětovném skenování",
"FileManagement": "Správa souborů",
@@ -588,23 +586,23 @@
"Required": "Požadované",
"NoEventsFound": "Nebyly nalezeny žádné události",
"RedownloadFailed": "Stažení se nezdařilo",
"DeleteSelectedImportListsMessageText": "Opravdu chcete odstranit indexer „{0}“?",
"DeleteSelectedImportListsMessageText": "Opravdu chcete smazat {count} vybraných seznamů k importu?",
"DeleteSelectedIndexers": "Odstranit indexer",
"ExistingTag": "Stávající značka",
"ApplyTagsHelpTextHowToApplyAuthors": "Jak použít značky na vybrané filmy",
"DeleteSelectedDownloadClientsMessageText": "Opravdu chcete odstranit indexer „{0}“?",
"DeleteSelectedDownloadClientsMessageText": "Opravdu chcete smazat {count} vybraných klientů pro stahování?",
"No": "Ne",
"NoChange": "Žádná změna",
"RemovingTag": "Odebírání značky",
"SetTags": "Nastavit značky",
"ApplyTagsHelpTextAdd": "Přidat: Přidá značky k již existujícímu seznamu",
"ApplyTagsHelpTextHowToApplyDownloadClients": "Jak použít značky na vybrané klienty pro stahování",
"ApplyTagsHelpTextHowToApplyImportLists": "Jak použít značky na vybrané importní seznamy",
"ApplyTagsHelpTextHowToApplyIndexers": "Jak použít značky na vybrané indexátory",
"ApplyTagsHelpTextHowToApplyImportLists": "Jak použít značky na vybrané seznamy k importu",
"ApplyTagsHelpTextHowToApplyIndexers": "Jak použít značky na vybrané indexery",
"ApplyTagsHelpTextRemove": "Odebrat: Odebrat zadané značky",
"ApplyTagsHelpTextReplace": "Nahradit: Nahradit značky zadanými značkami (zadáním žádné značky vymažete všechny značky)",
"DeleteSelectedDownloadClients": "Odstranit staženého klienta",
"DeleteSelectedIndexersMessageText": "Opravdu chcete odstranit indexer „{0}“?",
"ApplyTagsHelpTextReplace": "Nahradit: Nahradit značky zadanými značkami (prázdné pole vymaže všechny značky)",
"DeleteSelectedDownloadClients": "Odstranit klienta pro stahování",
"DeleteSelectedIndexersMessageText": "Opravdu chcete smazat {count} vybraný(ch) indexer(ů)?",
"Yes": "Ano",
"NotificationStatusAllClientHealthCheckMessage": "Všechny seznamy nejsou k dispozici z důvodu selhání",
"Small": "Malý",
@@ -624,7 +622,7 @@
"TotalSpace": "Celkový prostor",
"ConnectionLost": "Spojení ztraceno",
"ConnectionLostReconnect": "{appName} se pokusí připojit automaticky, nebo můžete kliknout na tlačítko znovunačtení níže.",
"ConnectionLostToBackend": "{appName} ztratila spojení s backendem a pro obnovení funkčnosti bude třeba ji znovu načíst.",
"ConnectionLostToBackend": "{appName} ztratil spojení s backendem a pro obnovení funkčnosti bude třebaho znovu načíst.",
"Large": "Velký",
"LastDuration": "lastDuration",
"Ui": "UI",
@@ -645,8 +643,8 @@
"CloneCondition": "Klonovat podmínku",
"Clone": "Klonovat",
"ApiKeyValidationHealthCheckMessage": "Aktualizujte svůj klíč API tak, aby měl alespoň {0} znaků. Můžete to provést prostřednictvím nastavení nebo konfiguračního souboru",
"ChooseImportMethod": "Vyberte mód importu",
"CatalogNumber": "katalogové číslo",
"ChooseImportMethod": "Vyberte způsob importu",
"CatalogNumber": "Katalogové číslo",
"Publisher": "Vydavatel",
"StatusEndedContinuing": "Pokračující",
"MetadataProfiles": "profil metadat",
@@ -655,12 +653,35 @@
"Label": "Etiketa",
"Library": "Knihovna",
"BypassIfAboveCustomFormatScore": "Obejít, pokud je vyšší než skóre vlastního formátu",
"AppUpdatedVersion": "{appName} byla aktualizována na verzi `{version}`, abyste získali nejnovější změny, musíte znovu načíst {appName}.",
"AppUpdatedVersion": "{appName} byl aktualizován na verzi `{version}`, abyste získali nejnovější změny, musíte znovu načíst {appName}",
"BypassIfAboveCustomFormatScoreHelpText": "Povolit obcházení, pokud má vydání vyšší skóre, než je nakonfigurované minimální skóre vlastního formátu",
"BypassIfHighestQuality": "Obejít v případě nejvyšší kvality",
"Theme": "Motiv",
"MinimumCustomFormatScoreHelpText": "Minimální skóre vlastního formátu požadované pro obejití zpoždění preferovaného protokolu",
"Series": "Seriál",
"DeleteCondition": "Odstranit podmínku",
"Database": "Databáze"
"Database": "Databáze",
"CountDownloadClientsSelected": "{count} vybraných klientů ke stahování",
"ImportListMissingRoot": "Chybí kořenový adresář pro import seznamu: {0}",
"IndexerDownloadClientHelpText": "Zvolte, který klient pro stahování bude použit pro zachytávání z toho indexeru",
"ImportListMultipleMissingRoots": "Několik kořenových adresářů chybí pro seznamy importu: {0}",
"EditSelectedDownloadClients": "Upravit vybrané klienty pro stahování",
"EditSelectedIndexers": "Upravit vybrané indexery",
"EnableProfile": "Povolit profil",
"DeleteImportList": "Smazat seznam importovaných položek",
"AddNewItem": "Přidat novou položku",
"AddMissing": "Přidat chybějící",
"EditSelectedImportLists": "Upravit vybrané seznamy k importu",
"DeleteSelectedImportLists": "Smazat seznam k importu",
"Duration": "Trvání",
"DeleteRootFolder": "Smazat kořenový adresář",
"DownloadClientTagHelpText": "Tohoto klienta pro stahování používat pouze pro filmy s alespoň jednou odpovídající značkou. Pro použití se všemi filmy ponechte prázdné pole.",
"AddedAuthorSettings": "Nastavení umělce přidáno",
"DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "Klient stahování {downloadClientName} je nastaven na odstranění dokončených stahování. To může vést k tomu, že stahování budou z klienta odstraněna dříve, než je bude moci importovat {1}.",
"IndexerTagsHelpText": "Tohoto klienta pro stahování používat pouze pro filmy s alespoň jednou odpovídající značkou. Pro použití se všemi filmy ponechte prázdné pole.",
"BlocklistReleaseHelpText": "Zabránit {appName}u v opětovném sebrání tohoto vydání",
"ListsSettingsSummary": "Seznam k importu",
"ExtraFileExtensionsHelpText": "Seznam extra souborů k importu oddělených čárkami (.nfo bude importován jako .nfo-orig)",
"ExtraFileExtensionsHelpTextsExamples": "Příklady: „.sub, .nfo“ nebo „sub, nfo“",
"ImportLists": "Seznam k importu"
}

View File

@@ -123,8 +123,6 @@
"ErrorLoadingContents": "Fejl ved indlæsning af indhold",
"ErrorLoadingPreviews": "Fejl ved indlæsning af forhåndsvisning",
"Exception": "Undtagelse",
"ExtraFileExtensionsHelpTexts1": "Kommasepareret liste over ekstra filer, der skal importeres (.nfo importeres som .nfo-orig)",
"ExtraFileExtensionsHelpTexts2": "Eksempler: '.sub, .nfo' eller 'sub, nfo'",
"FailedDownloadHandling": "Fejlet Download Håndtering",
"FileDateHelpText": "Skift fildato ved import / genscanning",
"FileManagement": "Fil Håndtering",
@@ -635,5 +633,7 @@
"Large": "Stor",
"Library": "Bibliotek",
"AllResultsAreHiddenByTheAppliedFilter": "Alle resultater skjules af det anvendte filter",
"AddNewItem": "Tilføj Ny Genstand"
"AddNewItem": "Tilføj Ny Genstand",
"ExtraFileExtensionsHelpText": "Kommasepareret liste over ekstra filer, der skal importeres (.nfo importeres som .nfo-orig)",
"ExtraFileExtensionsHelpTextsExamples": "Eksempler: '.sub, .nfo' eller 'sub, nfo'"
}

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