Compare commits

..

73 Commits

Author SHA1 Message Date
Bakerboy448 09d423848d New: Use Notifiarr API
New: Notifiarr Add Instance Name Support

Fixed: Notifiarr - Better HTTP Error Handling

also quiet sentry

(cherry picked from commit 1db690ad39ec103c0f4dc89ac4545801ef95bec7)

Fixed: Improve Notifiarr Exception Handling and Validation Errors

(cherry picked from commit 6aaa024d71b939030950460ae986ada5bbae5ad7)

also move notifiarr to header auth from url auth
2022-11-25 20:34:22 -06:00
psylenced e69371deca Fix: Trace logging postgres cleanse for large json files.
(cherry picked from commit 2ce9d099e1001eb4fccd61edcb0597782da872d4)
2022-11-25 20:30:54 -06:00
Chris 5ae8deb9d6 Fixed: Postgres secret regex now less greedy
[common]

(cherry picked from commit 812e5ac5a3d76b69c172c5611b315ea809ab4b48)
2022-11-25 20:30:54 -06:00
Chris 291674fc18 Fixed: Regex in log cleanser taking 10+ minutes on messages longer than 100k.
(cherry picked from commit d01bae92bfd68b04c54ab19bafe8af16c68ce711)
2022-11-25 20:30:54 -06:00
Qstick 774cd04d32 Fixed SeedConfigProvider cache refresh after indexer settings change
Fixes #1036

Co-Authored-By: Taloth <Taloth@users.noreply.github.com>
2022-11-25 20:24:00 -06:00
Qstick 3eb0533b17 New: Disable autocomplete of port number
Fixes #1249

Co-Authored-By: Stevie Robinson <stevie.robinson@gmail.com>
2022-11-25 20:21:10 -06:00
Qstick 2fe11ca1a9 Rename NzbSearchService to ReleaseSearchService
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2022-11-25 20:18:28 -06:00
Qstick c8ae6b0299 Fixed: Custom Script Health Issue Level
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2022-11-25 20:14:47 -06:00
Qstick 2086ae1e2c Ensure .Mono and .Windows projects have all dependencies in build output
Co-Authored-By: ta264 <ta264@users.noreply.github.com>
2022-11-25 20:10:23 -06:00
Chris 2aeec97d5e Added: Ntfy provider for notifications
(cherry picked from commit f6e6bc98f7846328d158ab2f804ca65b49632912)
2022-11-25 20:02:23 -06:00
Qstick 45e9d14916 New: Base API info endpoint
(cherry picked from commit 5e57ffbcf9ac3a346d4bf2b692248393215bad89)
2022-11-25 20:01:21 -06:00
Qstick ded45a53f3 Improve page scrollbar
Fixed: Scrolling in Firefox in small window (requires refresh)
New: Style scrollbar in Firefox
Fixed: Scrolling with click and drag
Fixes #3088
Fixes #2706
2022-11-25 20:00:56 -06:00
Mark McDowall d5cbb8f84f Replace react-custom-scrollbars
(cherry picked from commit 1c6c9a7960865d4bf8f58d5d04bef1aa1409594a)
2022-11-25 20:00:56 -06:00
Mark McDowall cac0fca0d2 Added react-hooks lint rules
(cherry picked from commit 381d64259396582de8d63ada99333e42cf5e3189)
2022-11-25 19:55:55 -06:00
Mark McDowall 9b5a050c40 Publish ApplicationStartingEvent during startup
(cherry picked from commit 5400bce1295bdc4198d2cfe0b9258bbb7ccf0852)
2022-11-25 19:54:05 -06:00
Weblate 9fa67dbe5f Translated using Weblate (German) [skip ci]
Currently translated at 100.0% (879 of 879 strings)

Translated using Weblate (Czech) [skip ci]

Currently translated at 62.4% (549 of 879 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 79.8% (702 of 879 strings)

Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Zalhera <tobias.bechen@gmail.com>
Co-authored-by: marapavelka <mara.pavelka@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fi/
Translation: Servarr/Readarr
2022-11-25 19:25:06 -06:00
Mark McDowall 505ef5ee63 Fixed: Testing SABnzbd when no categories are configured
(cherry picked from commit 0e31281828c737e3f6eecbb870960194888a970a)
2022-11-25 19:24:38 -06:00
Mark McDowall 87a2d7382e Added SECURITY.md
(cherry picked from commit 80af164385d9087a627142ca2281ae74ac0572af)

(cherry picked from commit aa8e886dab3ffedc835900db39787d47dc12465c)
2022-11-21 19:25:11 -06:00
Qstick 7c74a8df01 Fixed: UI Errors in Add Book page
Fixes #1906
2022-11-03 16:30:54 -05:00
Qstick f8324b3c92 Create CODE_OF_CONDUCT.md 2022-11-03 15:58:16 -05:00
Weblate cb03f3bf6b Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]
Currently translated at 100.0% (879 of 879 strings)

Update translation files [skip ci]

Updated by "Remove blank strings" hook in Weblate.

Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]

Currently translated at 69.0% (607 of 879 strings)

Translated using Weblate (Portuguese) [skip ci]

Currently translated at 76.7% (675 of 879 strings)

Co-authored-by: Sincejunly <qq943384135@gmail.com>
Co-authored-by: Tiago Ribeiro <tiago.err@outlook.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: libsu <libsu@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translation: Servarr/Readarr
2022-11-03 10:42:08 -05:00
ta264 d6f85ec4f9 Use wildcard for FreeBSD itemPattern 2022-11-02 21:22:34 +00:00
Qstick dd0dd921b0 Ignore brotli test on osx
(cherry picked from commit 2afe6af5a6dac26fe9d2c20e806a93b621e18bbf)
2022-10-30 19:03:10 -05:00
Mark McDowall 1f02423148 Fixed: Series list jump bar click issues
(cherry picked from commit 9c7378625112088d022239fdbdb90c0dc941d61d)
2022-10-20 20:42:53 -05:00
Qstick 7a4fba851f fixup! 2022-10-20 20:39:36 -05:00
Qstick 2d5efc268f New: Retry Postgres connection 3 times (with 5 second sleep) on Startup
(cherry picked from commit 3e700b63c26b247fcac83428ba79e53c88f797ff)

(cherry picked from commit dbca393772d7f558b45a780a6767187bf5900a23)
2022-10-20 20:39:36 -05:00
Mark McDowall cad5762f31 Sliding expiration for auth cookie and a little clean up
Fixes #1802

(cherry picked from commit 05ee4e644907b7f1e84589465ac9ab1848f5a766)
2022-10-20 20:23:15 -05:00
Qstick bcc8370d05 Cleanup dual target and mono code
Fixes #1893
Fixes #1808
2022-10-20 20:19:52 -05:00
Qstick 3481168df5 Fixed: qBittorrent Updates
Fixes #1658
Fixes #1536
2022-10-20 20:03:55 -05:00
Qstick 337a30ac0f Add thread to discord notifications 2022-10-19 22:00:13 -05:00
Weblate eaad4de4dc Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]
Currently translated at 69.0% (607 of 879 strings)

Translated using Weblate (Polish) [skip ci]

Currently translated at 65.4% (575 of 879 strings)

Translated using Weblate (Slovak) [skip ci]

Currently translated at 7.3% (65 of 879 strings)

Translated using Weblate (Portuguese) [skip ci]

Currently translated at 76.3% (671 of 879 strings)

Translated using Weblate (Portuguese) [skip ci]

Currently translated at 76.3% (671 of 879 strings)

Translated using Weblate (Chinese (Traditional) (zh_TW)) [skip ci]

Currently translated at 0.6% (6 of 879 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 79.8% (702 of 879 strings)

Translated using Weblate (Japanese) [skip ci]

Currently translated at 62.3% (548 of 879 strings)

Translated using Weblate (Italian) [skip ci]

Currently translated at 69.8% (614 of 879 strings)

Translated using Weblate (Icelandic) [skip ci]

Currently translated at 62.3% (548 of 879 strings)

Translated using Weblate (Hindi) [skip ci]

Currently translated at 62.3% (548 of 879 strings)

Translated using Weblate (Hebrew) [skip ci]

Currently translated at 63.8% (561 of 879 strings)

Translated using Weblate (French) [skip ci]

Currently translated at 75.9% (668 of 879 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 79.8% (702 of 879 strings)

Translated using Weblate (Greek) [skip ci]

Currently translated at 62.3% (548 of 879 strings)

Translated using Weblate (Danish) [skip ci]

Currently translated at 62.3% (548 of 879 strings)

Translated using Weblate (Czech) [skip ci]

Currently translated at 62.3% (548 of 879 strings)

Translated using Weblate (Bulgarian) [skip ci]

Currently translated at 62.3% (548 of 879 strings)

Translated using Weblate (Arabic) [skip ci]

Currently translated at 62.4% (549 of 879 strings)

Translated using Weblate (Spanish) [skip ci]

Currently translated at 67.2% (591 of 879 strings)

Translated using Weblate (Dutch) [skip ci]

Currently translated at 65.8% (579 of 879 strings)

Added translation using Weblate (Latvian) [skip ci]

Translated using Weblate (German) [skip ci]

Currently translated at 92.0% (809 of 879 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 100.0% (879 of 879 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 100.0% (879 of 879 strings)

Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]

Currently translated at 67.9% (597 of 879 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 99.5% (875 of 879 strings)

Translated using Weblate (French) [skip ci]

Currently translated at 75.0% (660 of 879 strings)

Translated using Weblate (German) [skip ci]

Currently translated at 92.0% (809 of 879 strings)

Translated using Weblate (Spanish) [skip ci]

Currently translated at 67.1% (590 of 879 strings)

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Dainel Amendoeira <daniel@amendoeira.eu>
Co-authored-by: Gian Klug <gian.klug@ict-scouts.ch>
Co-authored-by: Gylesie <github-anon.dasheens@aleeas.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: HiNesslio <chi.lio@shms-mail.ch>
Co-authored-by: Marcin <ml.cichy@gmail.com>
Co-authored-by: Mijail Todorovich <mijailtodorovich+git@gmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Thomas Schwery <thomas@schwery.me>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: cikyw <cikyw@vomoto.com>
Co-authored-by: fyu0h <biiigback@gmail.com>
Co-authored-by: reloxx <reloxx@interia.pl>
Co-authored-by: 麋鹿先生 <y.qiuyu@outlook.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/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/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_TW/
Translation: Servarr/Readarr
2022-10-16 10:06:07 -05:00
Mark McDowall b98ae37abf Handle redirects for 308 redirects
(cherry picked from commit 6eed7c8fed096fa1762448bc57876440f542be98)
2022-10-16 10:03:35 -05:00
Stevie Robinson 5dac84f9d8 New: Torrent Seed Ratio no longer advance settings
(cherry picked from commit c98fac65ed8e669c5e97ed3255c424b61fe0e8b3)
2022-10-16 10:03:06 -05:00
bakerboy448 ef21c73619 update feature request template
[skip ci]

(cherry picked from commit 362e664ce6d301e8283d489b3e0d945db46d22d3)
2022-10-16 10:02:34 -05:00
Joe Milazzo 14d74f2eca New: Kavita Connection (#1880)
* Added ability for Readarr to inform Kavita when a change occurs for rescan.

* Use the existing API with a POST rather than a new API.

* Updated some wording

* Fixed PR comments
2022-10-02 18:41:38 -05:00
servarr[bot] c3cbbb7627 Update Bug Report Template [skip ci] (#1842)
(cherry picked from commit 99e0d42b717b279be0f9005039025cd730f90f6c)

Co-authored-by: bakerboy448 <55419169+bakerboy448@users.noreply.github.com>
2022-09-26 21:05:36 -05:00
servarr[bot] ad3a58c422 New: Parse version with a space before 'v''
* New: Parse anime version with a space before 'v'

(cherry picked from commit e9123982f33ab35ca022f91f345da05fef23d6dc)

* Delete AnimeVersionFixture.cs

Co-authored-by: Mark McDowall <mark@mcdowall.ca>
Co-authored-by: Qstick <qstick@gmail.com>
2022-09-26 16:55:34 -05:00
Robin Dadswell 347b154882 Update Bug Report Template
[skip ci]

(cherry picked from commit b9185574f3761e125151907e3d31511689a4513e)
2022-09-26 16:42:52 -05:00
Weblate 63ccc155d6 Translated using Weblate (Russian) [skip ci]
Currently translated at 66.6% (586 of 879 strings)

Translated using Weblate (Romanian) [skip ci]

Currently translated at 60.5% (532 of 879 strings)

Translated using Weblate (Hungarian) [skip ci]

Currently translated at 100.0% (879 of 879 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 79.6% (700 of 879 strings)

Co-authored-by: AlexR-sf <omg.portal.supp@gmail.com>
Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: emanuelsipos <emanuelsipos1@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ru/
Translation: Servarr/Readarr
2022-08-18 07:12:51 -05:00
ta264 76613316fa Fixed: Book filters on author properties
Fixes #1755
2022-08-15 14:04:29 +01:00
ta264 f33e9f2bbc Fixed: Selecting edition for books in manual import 2022-08-15 14:04:29 +01:00
Robin Dadswell e4a3d7b273 Fixed: Postgres Timezone Issues
Co-authored-by: ta264 <ta264@users.noreply.github.com>
2022-08-13 19:55:48 +01:00
Robin Dadswell 46c2e0ba82 New: Postgres Support
Co-Authored-By: Qstick <376117+Qstick@users.noreply.github.com>
Co-authored-by: ta264 <ta264@users.noreply.github.com>

(cherry picked from commit 80b1aa9a2c81617bdda7ef551c19a2f114e49204)
2022-08-13 19:55:48 +01:00
Robin Dadswell 8616373f96 Bump FluentMigrator to 3.3.2 2022-08-13 19:55:48 +01:00
ta264 b3f99d8c20 Fixed: Add missing author when a book's author changes in metadata
Fixes READARR-VH
2022-08-05 11:30:13 +01:00
ta264 c0c0847963 Update packages 2022-08-05 11:30:13 +01:00
Qstick c92f9d03c0 Fixed: Releases Size filter has incorrect value type
(cherry picked from commit 46bc711558d9f3ab278125a4292eb7851e51d308)
2022-07-29 23:26:22 -04:00
bakerboy448 f7bf1e243d Fixed: List Import additions when book / author lookup required 2022-07-29 12:06:53 -05:00
Weblate 20b1f41ac1 Translated using Weblate (Catalan) [skip ci]
Currently translated at 49.4% (433 of 876 strings)

Translated using Weblate (Chinese (Traditional) (zh_TW)) [skip ci]

Currently translated at 0.5% (5 of 876 strings)

Co-authored-by: beefnoodle <acer.wang@protonmail.com>
Co-authored-by: dtalens <databio@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_TW/
Translation: Servarr/Readarr
2022-07-26 09:22:12 +01:00
bakerboy448 f703db1e00 Fixed: Better Cleansing of Tracker Announce Keys
Fixed: Cleanse Notifiarr secret from URL in logs

Fixes: #4623
(cherry picked from commit e6210aede6f7ead197e82572976bc0267d910d46)

(cherry picked from commit ec866082d44d299096112a6c7c232384b1f74505)
2022-07-26 09:21:28 +01:00
bakerboy448 20f67c8035 Fixed: Importing Readarr Lists
Fixes #1747
2022-07-26 09:20:38 +01:00
Qstick 56ae497bfa Fixed: Don't call for server notifications on event driven check 2022-07-17 13:48:12 -05:00
Weblate e760dc56c6 Translated using Weblate (French) [skip ci]
Currently translated at 75.1% (658 of 876 strings)

Co-authored-by: Sytha <tharaud.sylvain@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fr/
Translation: Servarr/Readarr
2022-07-17 13:42:13 -05:00
Weblate 7ad26b386c Translated using Weblate (Hungarian) [skip ci]
Currently translated at 100.0% (876 of 876 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 100.0% (876 of 876 strings)

Translated using Weblate (Italian) [skip ci]

Currently translated at 69.8% (612 of 876 strings)

Translated using Weblate (French) [skip ci]

Currently translated at 75.1% (658 of 876 strings)

Translated using Weblate (Ukrainian) [skip ci]

Currently translated at 10.9% (96 of 876 strings)

Translated using Weblate (Chinese (Traditional) (zh_TW)) [skip ci]

Currently translated at 0.1% (1 of 876 strings)

Translated using Weblate (Portuguese) [skip ci]

Currently translated at 76.3% (669 of 876 strings)

Translated using Weblate (Vietnamese) [skip ci]

Currently translated at 62.4% (547 of 876 strings)

Translated using Weblate (Turkish) [skip ci]

Currently translated at 62.5% (548 of 876 strings)

Translated using Weblate (Thai) [skip ci]

Currently translated at 62.4% (547 of 876 strings)

Translated using Weblate (Swedish) [skip ci]

Currently translated at 89.8% (787 of 876 strings)

Translated using Weblate (Russian) [skip ci]

Currently translated at 66.0% (579 of 876 strings)

Translated using Weblate (Romanian) [skip ci]

Currently translated at 62.4% (547 of 876 strings)

Translated using Weblate (Polish) [skip ci]

Currently translated at 65.6% (575 of 876 strings)

Translated using Weblate (Korean) [skip ci]

Currently translated at 62.6% (549 of 876 strings)

Translated using Weblate (Japanese) [skip ci]

Currently translated at 62.4% (547 of 876 strings)

Translated using Weblate (Italian) [skip ci]

Currently translated at 69.8% (612 of 876 strings)

Translated using Weblate (Icelandic) [skip ci]

Currently translated at 62.4% (547 of 876 strings)

Translated using Weblate (Hindi) [skip ci]

Currently translated at 62.4% (547 of 876 strings)

Translated using Weblate (Hebrew) [skip ci]

Currently translated at 63.8% (559 of 876 strings)

Translated using Weblate (French) [skip ci]

Currently translated at 67.1% (588 of 876 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 79.9% (700 of 876 strings)

Translated using Weblate (Greek) [skip ci]

Currently translated at 62.4% (547 of 876 strings)

Translated using Weblate (German) [skip ci]

Currently translated at 89.1% (781 of 876 strings)

Translated using Weblate (Danish) [skip ci]

Currently translated at 62.4% (547 of 876 strings)

Translated using Weblate (Czech) [skip ci]

Currently translated at 62.4% (547 of 876 strings)

Translated using Weblate (Bulgarian) [skip ci]

Currently translated at 62.4% (547 of 876 strings)

Translated using Weblate (Arabic) [skip ci]

Currently translated at 62.5% (548 of 876 strings)

Translated using Weblate (Spanish) [skip ci]

Currently translated at 67.0% (587 of 876 strings)

Translated using Weblate (Dutch) [skip ci]

Currently translated at 65.8% (577 of 876 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 79.9% (700 of 876 strings)

Added translation using Weblate (Lithuanian) [skip ci]

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Giorgio <sannagiorgio1997@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Maxent <rouaultmaxent@gmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: loksum213108 <lok3222003@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_TW/
Translation: Servarr/Readarr
2022-07-13 11:01:02 -05:00
Weblate 199085249f Translated using Weblate (Portuguese) [skip ci]
Currently translated at 75.9% (665 of 876 strings)

Translated using Weblate (Hungarian) [skip ci]

Currently translated at 100.0% (876 of 876 strings)

Translated using Weblate (German) [skip ci]

Currently translated at 88.0% (771 of 876 strings)

Translated using Weblate (Polish) [skip ci]

Currently translated at 64.2% (563 of 876 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 100.0% (876 of 876 strings)

Translated using Weblate (German) [skip ci]

Currently translated at 87.7% (769 of 876 strings)

Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Marcin <ml.cichy@gmail.com>
Co-authored-by: Vitor Brito <main@vitorbrito.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: reloxx <reloxx@interia.pl>
Co-authored-by: ηg <jonas.konrath@icloud.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hu/
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/
Translation: Servarr/Readarr
2022-06-25 20:30:02 -05:00
ta264 b84041d95f Use DryIoc for Automoqer, drop Unity dependency
(cherry picked from commit e3468daba04b52fbf41ce3004934a26b0220ec4f)
(cherry picked from commit cf4103d73d74896daa12e7a0237e8923eaa6c095)
2022-06-22 10:13:37 +01:00
Robin Dadswell 615facb3c4 fixed test due to GoodReads BookId change 2022-06-15 23:28:43 +01:00
Weblate b4fd1340c2 Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]
Currently translated at 68.1% (597 of 876 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 78.9% (692 of 876 strings)

Translated using Weblate (Polish) [skip ci]

Currently translated at 64.2% (563 of 876 strings)

Translated using Weblate (Hungarian) [skip ci]

Currently translated at 99.8% (875 of 876 strings)

Translated using Weblate (Hungarian) [skip ci]

Currently translated at 97.6% (855 of 876 strings)

Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]

Currently translated at 68.0% (596 of 876 strings)

Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]

Currently translated at 68.0% (596 of 876 strings)

Translated using Weblate (Norwegian Bokmål) [skip ci]

Currently translated at 8.5% (75 of 876 strings)

Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]

Currently translated at 67.9% (595 of 876 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 100.0% (876 of 876 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 100.0% (851 of 851 strings)

Translated using Weblate (Italian) [skip ci]

Currently translated at 69.8% (594 of 851 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 81.3% (692 of 851 strings)

Translated using Weblate (Italian) [skip ci]

Currently translated at 69.8% (594 of 851 strings)

Translated using Weblate (Italian) [skip ci]

Currently translated at 69.6% (593 of 851 strings)

Translated using Weblate (Italian) [skip ci]

Currently translated at 69.6% (593 of 851 strings)

Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Francesco <francy.ammirati@hotmail.com>
Co-authored-by: Giorgio <sannagiorgio1997@gmail.com>
Co-authored-by: Gjermund Wiggen <gjermundwiggen@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Matrixlee <matrix.alax@gmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Pan Jarek <jsawiuk@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: carreyli <laddie1987@qq.com>
Co-authored-by: un110 <13735581@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/nb_NO/
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/zh_CN/
Translation: Servarr/Readarr
2022-06-11 15:13:19 -05:00
bakerboy448 1da27b0978 Fixed: Cleanse GoodReads oauth from logs 2022-05-30 09:26:47 +01:00
ta264 b0a3ddef9c Fixed: Speed up book api 2022-05-30 09:05:25 +01:00
ta264 a59706ceb4 Fixed: Removed unnecessary author data from book endpoint 2022-05-30 09:05:25 +01:00
nitsua ce58e6ecdb Fixed: Mark as Failed errors
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>

(cherry picked from commit a9792973eef960e8310a611cf1da2d2cada57532)
2022-05-25 17:02:44 +01:00
bakerboy448 d01ce8b908 Fixed: Added new and missing translations 2022-05-25 14:08:08 +01:00
ta264 575f995916 Reinstate tests 2022-05-24 21:16:00 +01:00
ta264 c17c8815ef New: Add linux-x86 builds
(cherry picked from commit 8907b06899483d7cb3ecb9546d82af00d129f8e1)
2022-05-25 13:06:39 +01:00
ta264 8445941510 New: .NET 6.0.5 2022-05-25 13:06:39 +01:00
ta264 32ae18ae6f Fix bad translation 2022-05-25 13:06:39 +01:00
PearsonFlyer b91ec241a6 Fixed: Add translation for RestartRequiredHelpTextWarning 2022-05-15 20:18:29 +01:00
Qstick 17ce8187fd New: Instance name in System/Status API endpoint 2022-05-15 13:05:44 -05:00
Qstick 41615fe026 New: Instance name for Page Title 2022-05-15 13:05:44 -05:00
Robin Dadswell 9cff8f31e9 New: Instance Name used for Syslog 2022-05-15 13:05:44 -05:00
Robin Dadswell 21538b972d New: Set Instance Name 2022-05-15 13:05:44 -05:00
Robin Dadswell 5a7b4d41d8 New: Added UDP syslog support
(cherry picked from commit 8d856b2edb8bf46a2b516d5f7644ae3fa1151323)
2022-05-15 13:05:44 -05:00
278 changed files with 5647 additions and 2083 deletions
+4 -2
View File
@@ -5,9 +5,9 @@ body:
- type: checkboxes - type: checkboxes
attributes: attributes:
label: Is there an existing issue for this? label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the bug you encountered. description: Please search to see if an open or closed issue already exists for the bug you encountered. If a bug exists and is closed note that it may only be fixed in an unstable branch.
options: options:
- label: I have searched the existing issues - label: I have searched the existing open and closed issues
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
@@ -42,12 +42,14 @@ body:
- **Docker Install**: Yes - **Docker Install**: Yes
- **Using Reverse Proxy**: No - **Using Reverse Proxy**: No
- **Browser**: Firefox 90 (If UI related) - **Browser**: Firefox 90 (If UI related)
- **Database**: Sqlite 3.36.0
value: | value: |
- OS: - OS:
- Readarr: - Readarr:
- Docker Install: - Docker Install:
- Using Reverse Proxy: - Using Reverse Proxy:
- Browser: - Browser:
- Database:
render: markdown render: markdown
validations: validations:
required: true required: true
+2 -2
View File
@@ -5,9 +5,9 @@ body:
- type: checkboxes - type: checkboxes
attributes: attributes:
label: Is there an existing issue for this? label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the feature you are requesting. description: Please search to see if an open or closed issue already exists for the feature you are requesting. If a request exists and is closed note that it may only be fixed in an unstable branch.
options: options:
- label: I have searched the existing issues - label: I have searched the existing open and closed issues
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
+132
View File
@@ -0,0 +1,132 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of
any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email address,
without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
<development@readarr.com>.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations
+8
View File
@@ -0,0 +1,8 @@
# Security Policy
## Reporting a Vulnerability
Please report (suspected) security vulnerabilities on Discord (preferred) to
any of the Servarr Dev role holders (red names) or via email: development@servarr.com. You will receive a response from
us within 72 hours. If the issue is confirmed, we will release a patch as soon
as possible depending on complexity/severity.
+211 -69
View File
@@ -15,7 +15,7 @@ variables:
buildName: '$(Build.SourceBranchName).$(readarrVersion)' buildName: '$(Build.SourceBranchName).$(readarrVersion)'
sentryOrg: 'servarr' sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com' sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.201' dotnetVersion: '6.0.302'
innoVersion: '6.2.0' innoVersion: '6.2.0'
windowsImage: 'windows-2022' windowsImage: 'windows-2022'
linuxImage: 'ubuntu-20.04' linuxImage: 'ubuntu-20.04'
@@ -66,16 +66,13 @@ stages:
inputs: inputs:
version: $(dotnetVersion) version: $(dotnetVersion)
- bash: | - bash: |
BUNDLEDVERSIONS=${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}/Microsoft.NETCoreSdk.BundledVersions.props SDK_PATH="${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}"
echo $BUNDLEDVERSIONS BUNDLEDVERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
grep osx-x64 $BUNDLEDVERSIONS
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then if ! grep -q freebsd-x64 $BUNDLEDVERSIONS; then
echo "BSD already enabled" sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
else
echo "Enabling BSD support"
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64/' $BUNDLEDVERSIONS
fi fi
displayName: Enable FreeBSD Support displayName: Extra Platform Support
- task: Cache@2 - task: Cache@2
inputs: inputs:
key: 'nuget | "$(Agent.OS)" | $(Build.SourcesDirectory)/src/Directory.Packages.props' key: 'nuget | "$(Agent.OS)" | $(Build.SourcesDirectory)/src/Directory.Packages.props'
@@ -87,29 +84,27 @@ stages:
NUGET_PACKAGES: $(nugetCacheFolder) NUGET_PACKAGES: $(nugetCacheFolder)
- powershell: Get-ChildItem _output\net6.0*,_output\*.Update\* -Recurse | Where { $_.Fullname -notlike "*\publish\*" -and $_.attributes -notlike "*directory*" } | Remove-Item - powershell: Get-ChildItem _output\net6.0*,_output\*.Update\* -Recurse | Where { $_.Fullname -notlike "*\publish\*" -and $_.attributes -notlike "*directory*" } | Remove-Item
displayName: Clean up intermediate output displayName: Clean up intermediate output
- task: PublishPipelineArtifact@1 - publish: $(outputFolder)
inputs: artifact: '$(osName)Backend'
path: $(outputFolder)
artifact: '$(osName)Backend'
artifactType: 'pipeline'
parallel: true
parallelCount: 100
displayName: Publish Backend displayName: Publish Backend
- publish: '$(testsFolder)/net6.0/win-x64/publish' - publish: '$(testsFolder)/net6.0/win-x64/publish'
artifact: WindowsCoreTests artifact: win-x64-tests
displayName: Publish Windows Test Package displayName: Publish win-x64 Test Package
- publish: '$(testsFolder)/net6.0/linux-x64/publish' - publish: '$(testsFolder)/net6.0/linux-x64/publish'
artifact: LinuxCoreTests artifact: linux-x64-tests
displayName: Publish Linux Test Package displayName: Publish linux-x64 Test Package
- publish: '$(testsFolder)/net6.0/linux-x86/publish'
artifact: linux-x86-tests
displayName: Publish linux-x86 Test Package
- publish: '$(testsFolder)/net6.0/linux-musl-x64/publish' - publish: '$(testsFolder)/net6.0/linux-musl-x64/publish'
artifact: LinuxMuslCoreTests artifact: linux-musl-x64-tests
displayName: Publish Linux Musl Test Package displayName: Publish linux-musl-x64 Test Package
- publish: '$(testsFolder)/net6.0/freebsd-x64/publish' - publish: '$(testsFolder)/net6.0/freebsd-x64/publish'
artifact: FreebsdCoreTests artifact: freebsd-x64-tests
displayName: Publish FreeBSD Test Package displayName: Publish freebsd-x64 Test Package
- publish: '$(testsFolder)/net6.0/osx-x64/publish' - publish: '$(testsFolder)/net6.0/osx-x64/publish'
artifact: MacCoreTests artifact: osx-x64-tests
displayName: Publish MacOS Test Package displayName: Publish osx-x64 Test Package
- stage: Build_Backend_Other - stage: Build_Backend_Other
displayName: Build Backend (Other OS) displayName: Build Backend (Other OS)
@@ -141,25 +136,29 @@ stages:
inputs: inputs:
version: $(dotnetVersion) version: $(dotnetVersion)
- bash: | - bash: |
BUNDLEDVERSIONS=${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}/Microsoft.NETCoreSdk.BundledVersions.props SDK_PATH="${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}"
echo $BUNDLEDVERSIONS BUNDLEDVERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
grep osx-x64 $BUNDLEDVERSIONS
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then if ! grep -q freebsd-x64 $BUNDLEDVERSIONS; then
echo "BSD already enabled" sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
else
echo "Enabling BSD support"
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64/' $BUNDLEDVERSIONS
fi fi
displayName: Enable FreeBSD Support displayName: Extra Platform Support
- task: Cache@2 - task: Cache@2
inputs: inputs:
key: 'nuget | "$(Agent.OS)" | $(Build.SourcesDirectory)/src/Directory.Packages.props' key: 'nuget | "$(Agent.OS)" | $(Build.SourcesDirectory)/src/Directory.Packages.props'
path: $(nugetCacheFolder) path: $(nugetCacheFolder)
displayName: Cache NuGet packages displayName: Cache NuGet packages
- bash: ./build.sh --backend --enable-bsd - bash: ./build.sh --backend --enable-extra-platforms
displayName: Build Readarr Backend displayName: Build Readarr Backend
env: env:
NUGET_PACKAGES: $(nugetCacheFolder) NUGET_PACKAGES: $(nugetCacheFolder)
- bash: |
find ${OUTPUTFOLDER} -type f ! -path "*/publish/*" -exec rm -rf {} \;
find ${OUTPUTFOLDER} -depth -empty -type d -exec rm -r "{}" \;
find ${TESTSFOLDER} -type f ! -path "*/publish/*" -exec rm -rf {} \;
find ${TESTSFOLDER} -depth -empty -type d -exec rm -r "{}" \;
displayName: Clean up intermediate output
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
- stage: Build_Frontend - stage: Build_Frontend
displayName: Frontend displayName: Frontend
@@ -262,35 +261,35 @@ stages:
artifactName: WindowsFrontend artifactName: WindowsFrontend
targetPath: _output targetPath: _output
displayName: Fetch Frontend displayName: Fetch Frontend
- bash: ./build.sh --packages --enable-bsd - bash: ./build.sh --packages --enable-extra-platforms
displayName: Create Packages displayName: Create Packages
- bash: | - bash: |
find . -name "Readarr" -exec chmod a+x {} \; find . -name "Readarr" -exec chmod a+x {} \;
find . -name "Readarr.Update" -exec chmod a+x {} \; find . -name "Readarr.Update" -exec chmod a+x {} \;
displayName: Set executable bits displayName: Set executable bits
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create Windows Core zip displayName: Create win-x64 zip
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).windows-core-x64.zip' archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).windows-core-x64.zip'
archiveType: 'zip' archiveType: 'zip'
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/win-x64/net6.0 rootFolderOrFile: $(artifactsFolder)/win-x64/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create Windows x86 Core zip displayName: Create win-x86 zip
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).windows-core-x86.zip' archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).windows-core-x86.zip'
archiveType: 'zip' archiveType: 'zip'
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/win-x86/net6.0 rootFolderOrFile: $(artifactsFolder)/win-x86/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create MacOS x64 Core app displayName: Create osx-x64 app
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).osx-app-core-x64.zip' archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).osx-app-core-x64.zip'
archiveType: 'zip' archiveType: 'zip'
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-x64-app/net6.0 rootFolderOrFile: $(artifactsFolder)/osx-x64-app/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create MacOS x64 Core tar displayName: Create osx-x64 tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).osx-core-x64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).osx-core-x64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -298,14 +297,14 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-x64/net6.0 rootFolderOrFile: $(artifactsFolder)/osx-x64/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create MacOS arm64 Core app displayName: Create osx-arm64 app
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).osx-app-core-arm64.zip' archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).osx-app-core-arm64.zip'
archiveType: 'zip' archiveType: 'zip'
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-arm64-app/net6.0 rootFolderOrFile: $(artifactsFolder)/osx-arm64-app/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create MacOS arm64 Core tar displayName: Create osx-arm64 tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).osx-core-arm64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).osx-core-arm64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -313,7 +312,7 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-arm64/net6.0 rootFolderOrFile: $(artifactsFolder)/osx-arm64/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create Linux Core tar displayName: Create linux-x64 tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-core-x64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-core-x64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -321,7 +320,7 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-x64/net6.0 rootFolderOrFile: $(artifactsFolder)/linux-x64/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create Linux Musl Core tar displayName: Create linux-musl-x64 tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-musl-core-x64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-musl-core-x64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -329,7 +328,15 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net6.0 rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create ARM32 Linux Core tar displayName: Create linux-x86 tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-core-x86.tar.gz'
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-x86/net6.0
- task: ArchiveFiles@2
displayName: Create linux-arm tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-core-arm.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-core-arm.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -337,7 +344,7 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-arm/net6.0 rootFolderOrFile: $(artifactsFolder)/linux-arm/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create ARM32 Linux Musl Core tar displayName: Create linux-musl-arm tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-musl-core-arm.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-musl-core-arm.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -345,7 +352,7 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm/net6.0 rootFolderOrFile: $(artifactsFolder)/linux-musl-arm/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create Linux arm64 Core tar displayName: Create linux-arm64 tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-core-arm64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-core-arm64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -353,7 +360,7 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-arm64/net6.0 rootFolderOrFile: $(artifactsFolder)/linux-arm64/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create ARM64 Linux Musl Core tar displayName: Create linux-musl-arm64 tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-musl-core-arm64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-musl-core-arm64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -361,7 +368,7 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net6.0 rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create FreeBSD Core Core tar displayName: Create freebsd-x64 tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).freebsd-core-x64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).freebsd-core-x64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -413,22 +420,22 @@ stages:
matrix: matrix:
MacCore: MacCore:
osName: 'Mac' osName: 'Mac'
testName: 'MacCore' testName: 'osx-x64'
poolName: 'Azure Pipelines' poolName: 'Azure Pipelines'
imageName: ${{ variables.macImage }} imageName: ${{ variables.macImage }}
WindowsCore: WindowsCore:
osName: 'Windows' osName: 'Windows'
testName: 'WindowsCore' testName: 'win-x64'
poolName: 'Azure Pipelines' poolName: 'Azure Pipelines'
imageName: ${{ variables.windowsImage }} imageName: ${{ variables.windowsImage }}
LinuxCore: LinuxCore:
osName: 'Linux' osName: 'Linux'
testName: 'LinuxCore' testName: 'linux-x64'
poolName: 'Azure Pipelines' poolName: 'Azure Pipelines'
imageName: ${{ variables.linuxImage }} imageName: ${{ variables.linuxImage }}
FreebsdCore: FreebsdCore:
osName: 'Linux' osName: 'Linux'
testName: 'FreebsdCore' testName: 'freebsd-x64'
poolName: 'FreeBSD' poolName: 'FreeBSD'
imageName: imageName:
@@ -447,7 +454,7 @@ stages:
displayName: Download Test Artifact displayName: Download Test Artifact
inputs: inputs:
buildType: 'current' buildType: 'current'
artifactName: '$(testName)Tests' artifactName: '$(testName)-tests'
targetPath: $(testsFolder) targetPath: $(testsFolder)
- powershell: Set-Service SCardSvr -StartupType Manual - powershell: Set-Service SCardSvr -StartupType Manual
displayName: Enable Windows Test Service displayName: Enable Windows Test Service
@@ -475,8 +482,12 @@ stages:
matrix: matrix:
alpine: alpine:
testName: 'Musl Net Core' testName: 'Musl Net Core'
artifactName: LinuxMuslCoreTests artifactName: linux-musl-x64-tests
containerImage: ghcr.io/servarr/testimages:alpine containerImage: ghcr.io/servarr/testimages:alpine
linux-x86:
testName: 'linux-x86'
artifactName: linux-x86-tests
containerImage: ghcr.io/servarr/testimages:linux-x86
pool: pool:
vmImage: ${{ variables.linuxImage }} vmImage: ${{ variables.linuxImage }}
@@ -487,9 +498,15 @@ stages:
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Install .net core' displayName: 'Install .NET'
inputs: inputs:
version: $(dotnetVersion) version: $(dotnetVersion)
condition: and(succeeded(), ne(variables['testName'], 'linux-x86'))
- bash: |
SDKURL=$(curl -s https://api.github.com/repos/Servarr/dotnet-linux-x86/releases | jq -rc '.[].assets[].browser_download_url' | grep sdk-${DOTNETVERSION}.*gz$)
curl -fsSL $SDKURL | tar xzf - -C /opt/dotnet
displayName: 'Install .NET'
condition: and(succeeded(), eq(variables['testName'], 'linux-x86'))
- checkout: none - checkout: none
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: Download Test Artifact displayName: Download Test Artifact
@@ -512,6 +529,57 @@ stages:
testResultsFiles: '**/TestResult.xml' testResultsFiles: '**/TestResult.xml'
testRunTitle: '$(testName) Unit Tests' testRunTitle: '$(testName) Unit Tests'
failTaskOnFailedTests: true failTaskOnFailedTests: true
- job: Unit_LinuxCore_Postgres
displayName: Unit Native LinuxCore with Postgres Database
variables:
pattern: 'Readarr.*.linux-core-x64.tar.gz'
artifactName: LinuxCoreTests
Readarr__Postgres__Host: 'localhost'
Readarr__Postgres__Port: '5432'
Readarr__Postgres__User: 'readarr'
Readarr__Postgres__Password: 'readarr'
pool:
vmImage: ${{ variables.linuxImage }}
timeoutInMinutes: 10
steps:
- task: UseDotNet@2
displayName: 'Install .net core'
inputs:
version: $(dotnetVersion)
- checkout: none
- task: DownloadPipelineArtifact@2
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: 'linux-x64-Tests'
targetPath: $(testsFolder)
- bash: find ${TESTSFOLDER} -name "Readarr.Test.Dummy" -exec chmod a+x {} \;
displayName: Make Test Dummy Executable
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
- bash: |
docker run -d --name=postgres14 \
-e POSTGRES_PASSWORD=readarr \
-e POSTGRES_USER=readarr \
-p 5432:5432/tcp \
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
postgres:14
displayName: Start postgres
- bash: |
chmod a+x ${TESTSFOLDER}/test.sh
ls -lR ${TESTSFOLDER}
${TESTSFOLDER}/test.sh Linux Unit Test
displayName: Run Tests
- task: PublishTestResults@2
displayName: Publish Test Results
inputs:
testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml'
testRunTitle: 'LinuxCore Postgres Unit Tests'
failTaskOnFailedTests: true
- stage: Integration - stage: Integration
displayName: Integration displayName: Integration
@@ -523,17 +591,17 @@ stages:
matrix: matrix:
MacCore: MacCore:
osName: 'Mac' osName: 'Mac'
testName: 'MacCore' testName: 'osx-x64'
imageName: ${{ variables.macImage }} imageName: ${{ variables.macImage }}
pattern: 'Readarr.*.osx-core-x64.tar.gz' pattern: 'Readarr.*.osx-core-x64.tar.gz'
WindowsCore: WindowsCore:
osName: 'Windows' osName: 'Windows'
testName: 'WindowsCore' testName: 'win-x64'
imageName: ${{ variables.windowsImage }} imageName: ${{ variables.windowsImage }}
pattern: 'Readarr.*.windows-core-x64.zip' pattern: 'Readarr.*.windows-core-x64.zip'
LinuxCore: LinuxCore:
osName: 'Linux' osName: 'Linux'
testName: 'LinuxCore' testName: 'linux-x64'
imageName: ${{ variables.linuxImage }} imageName: ${{ variables.linuxImage }}
pattern: 'Readarr.*.linux-core-x64.tar.gz' pattern: 'Readarr.*.linux-core-x64.tar.gz'
@@ -550,7 +618,7 @@ stages:
displayName: Download Test Artifact displayName: Download Test Artifact
inputs: inputs:
buildType: 'current' buildType: 'current'
artifactName: '$(testName)Tests' artifactName: '$(testName)-tests'
targetPath: $(testsFolder) targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: Download Build Artifact displayName: Download Build Artifact
@@ -580,6 +648,66 @@ stages:
failTaskOnFailedTests: true failTaskOnFailedTests: true
displayName: Publish Test Results displayName: Publish Test Results
- job: Integration_LinuxCore_Postgres
displayName: Integration Native LinuxCore with Postgres Database
variables:
pattern: 'Readarr.*.linux-core-x64.tar.gz'
Readarr__Postgres__Host: 'localhost'
Readarr__Postgres__Port: '5432'
Readarr__Postgres__User: 'readarr'
Readarr__Postgres__Password: 'readarr'
pool:
vmImage: ${{ variables.linuxImage }}
steps:
- task: UseDotNet@2
displayName: 'Install .net core'
inputs:
version: $(dotnetVersion)
- checkout: none
- task: DownloadPipelineArtifact@2
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: 'linux-x64-tests'
targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2
displayName: Download Build Artifact
inputs:
buildType: 'current'
artifactName: Packages
itemPattern: '**/$(pattern)'
targetPath: $(Build.ArtifactStagingDirectory)
- task: ExtractFiles@1
inputs:
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
displayName: Extract Package
- bash: |
mkdir -p ./bin/
cp -r -v ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin/Readarr/. ./bin/
displayName: Move Package Contents
- bash: |
docker run -d --name=postgres14 \
-e POSTGRES_PASSWORD=readarr \
-e POSTGRES_USER=readarr \
-p 5432:5432/tcp \
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
postgres:14
displayName: Start postgres
- bash: |
chmod a+x ${TESTSFOLDER}/test.sh
${TESTSFOLDER}/test.sh Linux Integration Test
displayName: Run Integration Tests
- task: PublishTestResults@2
inputs:
testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml'
testRunTitle: 'Integration LinuxCore Postgres Database Integration Tests'
failTaskOnFailedTests: true
displayName: Publish Test Results
- job: Integration_FreeBSD - job: Integration_FreeBSD
displayName: Integration Native FreeBSD displayName: Integration Native FreeBSD
workspace: workspace:
@@ -595,14 +723,14 @@ stages:
displayName: Download Test Artifact displayName: Download Test Artifact
inputs: inputs:
buildType: 'current' buildType: 'current'
artifactName: 'FreebsdCoreTests' artifactName: 'freebsd-x64-tests'
targetPath: $(testsFolder) targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: Download Build Artifact displayName: Download Build Artifact
inputs: inputs:
buildType: 'current' buildType: 'current'
artifactName: Packages artifactName: Packages
itemPattern: '/$(pattern)' itemPattern: '**/$(pattern)'
targetPath: $(Build.ArtifactStagingDirectory) targetPath: $(Build.ArtifactStagingDirectory)
- bash: | - bash: |
mkdir -p ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin mkdir -p ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin
@@ -629,11 +757,15 @@ stages:
strategy: strategy:
matrix: matrix:
alpine: alpine:
testName: 'Musl Net Core' testName: 'linux-musl-x64'
artifactName: LinuxMuslCoreTests artifactName: linux-musl-x64-tests
containerImage: ghcr.io/servarr/testimages:alpine containerImage: ghcr.io/servarr/testimages:alpine
pattern: 'Readarr.*.linux-musl-core-x64.tar.gz' pattern: 'Readarr.*.linux-musl-core-x64.tar.gz'
linux-x86:
testName: 'linux-x86'
artifactName: linux-x86-tests
containerImage: ghcr.io/servarr/testimages:linux-x86
pattern: 'Readarr.*.linux-core-x86.tar.gz'
pool: pool:
vmImage: ${{ variables.linuxImage }} vmImage: ${{ variables.linuxImage }}
@@ -643,9 +775,15 @@ stages:
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Install .net core' displayName: 'Install .NET'
inputs: inputs:
version: $(dotnetVersion) version: $(dotnetVersion)
condition: and(succeeded(), ne(variables['testName'], 'linux-x86'))
- bash: |
SDKURL=$(curl -s https://api.github.com/repos/Servarr/dotnet-linux-x86/releases | jq -rc '.[].assets[].browser_download_url' | grep sdk-${DOTNETVERSION}.*gz$)
curl -fsSL $SDKURL | tar xzf - -C /opt/dotnet
displayName: 'Install .NET'
condition: and(succeeded(), eq(variables['testName'], 'linux-x86'))
- checkout: none - checkout: none
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: Download Test Artifact displayName: Download Test Artifact
@@ -691,14 +829,17 @@ stages:
matrix: matrix:
Linux: Linux:
osName: 'Linux' osName: 'Linux'
artifactName: 'linux-x64'
imageName: ${{ variables.linuxImage }} imageName: ${{ variables.linuxImage }}
pattern: 'Readarr.*.linux-core-x64.tar.gz' pattern: 'Readarr.*.linux-core-x64.tar.gz'
Mac: Mac:
osName: 'Mac' osName: 'Mac'
artifactName: 'osx-x64'
imageName: ${{ variables.macImage }} imageName: ${{ variables.macImage }}
pattern: 'Readarr.*.osx-core-x64.tar.gz' pattern: 'Readarr.*.osx-core-x64.tar.gz'
Windows: Windows:
osName: 'Windows' osName: 'Windows'
artifactName: 'win-x64'
imageName: ${{ variables.windowsImage }} imageName: ${{ variables.windowsImage }}
pattern: 'Readarr.*.windows-core-x64.zip' pattern: 'Readarr.*.windows-core-x64.zip'
@@ -715,7 +856,7 @@ stages:
displayName: Download Test Artifact displayName: Download Test Artifact
inputs: inputs:
buildType: 'current' buildType: 'current'
artifactName: '$(osName)CoreTests' artifactName: '$(artifactName)-tests'
targetPath: $(testsFolder) targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: Download Build Artifact displayName: Download Build Artifact
@@ -909,3 +1050,4 @@ stages:
SYSTEM_ACCESSTOKEN: $(System.AccessToken) SYSTEM_ACCESSTOKEN: $(System.AccessToken)
DISCORDCHANNELID: $(discordChannelId) DISCORDCHANNELID: $(discordChannelId)
DISCORDWEBHOOKKEY: $(discordWebhookKey) DISCORDWEBHOOKKEY: $(discordWebhookKey)
DISCORDTHREADID: $(discordThreadId)
+32 -13
View File
@@ -27,15 +27,22 @@ UpdateVersionNumber()
fi fi
} }
EnableBsdSupport() EnableExtraPlatformsInSDK()
{ {
#todo enable sdk with SDK_PATH=$(dotnet --list-sdks | grep -P '6\.\d\.\d+' | head -1 | sed 's/\(6\.[0-9]*\.[0-9]*\).*\[\(.*\)\]/\2\/\1/g')
#SDK_PATH=$(dotnet --list-sdks | grep -P '5\.\d\.\d+' | head -1 | sed 's/\(5\.[0-9]*\.[0-9]*\).*\[\(.*\)\]/\2\/\1/g') BUNDLEDVERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
# BUNDLED_VERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props" if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
echo "Extra platforms already enabled"
else
echo "Enabling extra platform support"
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
fi
}
EnableExtraPlatforms()
{
if grep -qv freebsd-x64 src/Directory.Build.props; then if grep -qv freebsd-x64 src/Directory.Build.props; then
sed -i'' -e "s^<RuntimeIdentifiers>\(.*\)</RuntimeIdentifiers>^<RuntimeIdentifiers>\1;freebsd-x64</RuntimeIdentifiers>^g" src/Directory.Build.props sed -i'' -e "s^<RuntimeIdentifiers>\(.*\)</RuntimeIdentifiers>^<RuntimeIdentifiers>\1;freebsd-x64;linux-x86</RuntimeIdentifiers>^g" src/Directory.Build.props
sed -i'' -e "s^<ExcludedRuntimeFrameworkPairs>\(.*\)</ExcludedRuntimeFrameworkPairs>^<ExcludedRuntimeFrameworkPairs>\1;freebsd-x64:net472</ExcludedRuntimeFrameworkPairs>^g" src/Directory.Build.props
fi fi
} }
@@ -292,7 +299,8 @@ if [ $# -eq 0 ]; then
PACKAGES=YES PACKAGES=YES
INSTALLER=NO INSTALLER=NO
LINT=YES LINT=YES
ENABLE_BSD=NO ENABLE_EXTRA_PLATFORMS=NO
ENABLE_EXTRA_PLATFORMS_IN_SDK=NO
fi fi
while [[ $# -gt 0 ]] while [[ $# -gt 0 ]]
@@ -304,8 +312,12 @@ case $key in
BACKEND=YES BACKEND=YES
shift # past argument shift # past argument
;; ;;
--enable-bsd) --enable-bsd|--enable-extra-platforms)
ENABLE_BSD=YES ENABLE_EXTRA_PLATFORMS=YES
shift # past argument
;;
--enable-extra-platforms-in-sdk)
ENABLE_EXTRA_PLATFORMS_IN_SDK=YES
shift # past argument shift # past argument
;; ;;
-r|--runtime) -r|--runtime)
@@ -349,12 +361,17 @@ esac
done done
set -- "${POSITIONAL[@]}" # restore positional parameters set -- "${POSITIONAL[@]}" # restore positional parameters
if [ "$ENABLE_EXTRA_PLATFORMS_IN_SDK" = "YES" ];
then
EnableExtraPlatformsInSDK
fi
if [ "$BACKEND" = "YES" ]; if [ "$BACKEND" = "YES" ];
then then
UpdateVersionNumber UpdateVersionNumber
if [ "$ENABLE_BSD" = "YES" ]; if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
then then
EnableBsdSupport EnableExtraPlatforms
fi fi
Build Build
if [[ -z "$RID" || -z "$FRAMEWORK" ]]; if [[ -z "$RID" || -z "$FRAMEWORK" ]];
@@ -364,9 +381,10 @@ then
PackageTests "net6.0" "linux-x64" PackageTests "net6.0" "linux-x64"
PackageTests "net6.0" "linux-musl-x64" PackageTests "net6.0" "linux-musl-x64"
PackageTests "net6.0" "osx-x64" PackageTests "net6.0" "osx-x64"
if [ "$ENABLE_BSD" = "YES" ]; if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
then then
PackageTests "net6.0" "freebsd-x64" PackageTests "net6.0" "freebsd-x64"
PackageTests "net6.0" "linux-x86"
fi fi
else else
PackageTests "$FRAMEWORK" "$RID" PackageTests "$FRAMEWORK" "$RID"
@@ -405,9 +423,10 @@ then
Package "net6.0" "linux-musl-arm" Package "net6.0" "linux-musl-arm"
Package "net6.0" "osx-x64" Package "net6.0" "osx-x64"
Package "net6.0" "osx-arm64" Package "net6.0" "osx-arm64"
if [ "$ENABLE_BSD" = "YES" ]; if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
then then
Package "net6.0" "freebsd-x64" Package "net6.0" "freebsd-x64"
Package "net6.0" "linux-x86"
fi fi
else else
Package "$FRAMEWORK" "$RID" Package "$FRAMEWORK" "$RID"
+4 -1
View File
@@ -39,6 +39,7 @@ module.exports = {
plugins: [ plugins: [
'filenames', 'filenames',
'react', 'react',
'react-hooks',
'simple-import-sort', 'simple-import-sort',
'import' 'import'
], ],
@@ -308,7 +309,9 @@ module.exports = {
'react/react-in-jsx-scope': 2, 'react/react-in-jsx-scope': 2,
'react/self-closing-comp': 2, 'react/self-closing-comp': 2,
'react/sort-comp': 2, 'react/sort-comp': 2,
'react/jsx-wrap-multilines': 2 'react/jsx-wrap-multilines': 2,
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'error'
}, },
overrides: [ overrides: [
{ {
@@ -8,17 +8,17 @@ function AuthorMonitorNewItemsOptionsPopoverContent() {
<DescriptionList> <DescriptionList>
<DescriptionListItem <DescriptionListItem
title={translate('AllBooks')} title={translate('AllBooks')}
data="Monitor all new books" data={translate('DataNewAllBooks')}
/> />
<DescriptionListItem <DescriptionListItem
title={translate('NewBooks')} title={translate('NewBooks')}
data="Monitor new books released after the newest existing book" data={translate('DataNewBooks')}
/> />
<DescriptionListItem <DescriptionListItem
title={translate('None')} title={translate('None')}
data="Don't monitor any new books" data={translate('DataNewNone')}
/> />
</DescriptionList> </DescriptionList>
); );
@@ -8,42 +8,42 @@ function AuthorMonitoringOptionsPopoverContent() {
return ( return (
<> <>
<Alert> <Alert>
This is a one time adjustment to set which books are monitored {translate('MonitoringOptionsHelpText')}
</Alert> </Alert>
<DescriptionList> <DescriptionList>
<DescriptionListItem <DescriptionListItem
title={translate('AllBooks')} title={translate('AllBooks')}
data="Monitor all books" data={translate('DataAllBooks')}
/> />
<DescriptionListItem <DescriptionListItem
title={translate('FutureBooks')} title={translate('FutureBooks')}
data="Monitor books that have not released yet" data={translate('DataFutureBooks')}
/> />
<DescriptionListItem <DescriptionListItem
title={translate('MissingBooks')} title={translate('MissingBooks')}
data="Monitor books that do not have files or have not released yet" data={translate('DataMissingBooks')}
/> />
<DescriptionListItem <DescriptionListItem
title={translate('ExistingBooks')} title={translate('ExistingBooks')}
data="Monitor books that have files or have not released yet" data={translate('DataExistingBooks')}
/> />
<DescriptionListItem <DescriptionListItem
title={translate('FirstBook')} title={translate('FirstBook')}
data="Monitor the first book. All other books will be ignored" data={translate('DataFirstBook')}
/> />
<DescriptionListItem <DescriptionListItem
title={translate('LatestBook')} title={translate('LatestBook')}
data="Monitor the latest book and future books" data={translate('DataLatestBook')}
/> />
<DescriptionListItem <DescriptionListItem
title={translate('None')} title={translate('None')}
data="No books will be monitored" data={translate('DataNone')}
/> />
</DescriptionList> </DescriptionList>
</> </>
+1 -1
View File
@@ -8,7 +8,7 @@ import AppRoutes from './AppRoutes';
function App({ store, history }) { function App({ store, history }) {
return ( return (
<DocumentTitle title="Readarr"> <DocumentTitle title={window.Readarr.instanceName}>
<Provider store={store}> <Provider store={store}>
<ConnectedRouter history={history}> <ConnectedRouter history={history}>
<PageConnector> <PageConnector>
@@ -267,7 +267,7 @@ class AuthorEditorFooter extends Component {
isDisabled={!selectedCount || isOrganizingAuthor || isRetaggingAuthor} isDisabled={!selectedCount || isOrganizingAuthor || isRetaggingAuthor}
onPress={onOrganizeAuthorPress} onPress={onOrganizeAuthorPress}
> >
Rename Files {translate('RenameFiles')}
</SpinnerButton> </SpinnerButton>
<SpinnerButton <SpinnerButton
@@ -277,7 +277,7 @@ class AuthorEditorFooter extends Component {
isDisabled={!selectedCount || isOrganizingAuthor || isRetaggingAuthor} isDisabled={!selectedCount || isOrganizingAuthor || isRetaggingAuthor}
onPress={onRetagAuthorPress} onPress={onRetagAuthorPress}
> >
Write Metadata Tags {translate('WriteMetadataTags')}
</SpinnerButton> </SpinnerButton>
<SpinnerButton <SpinnerButton
@@ -286,7 +286,7 @@ class AuthorEditorFooter extends Component {
isDisabled={!selectedCount || isOrganizingAuthor || isRetaggingAuthor} isDisabled={!selectedCount || isOrganizingAuthor || isRetaggingAuthor}
onPress={this.onTagsPress} onPress={this.onTagsPress}
> >
Set Readarr Tags {translate('SetReadarrTags')}
</SpinnerButton> </SpinnerButton>
<SpinnerButton <SpinnerButton
@@ -296,7 +296,7 @@ class AuthorEditorFooter extends Component {
isDisabled={!selectedCount || isDeleting} isDisabled={!selectedCount || isDeleting}
onPress={this.onDeleteSelectedPress} onPress={this.onDeleteSelectedPress}
> >
Delete {translate('Delete')}
</SpinnerButton> </SpinnerButton>
</div> </div>
@@ -8,6 +8,7 @@ import * as commandNames from 'Commands/commandNames';
import { toggleBooksMonitored } from 'Store/Actions/bookActions'; import { toggleBooksMonitored } from 'Store/Actions/bookActions';
import { clearBookFiles, fetchBookFiles } from 'Store/Actions/bookFileActions'; import { clearBookFiles, fetchBookFiles } from 'Store/Actions/bookFileActions';
import { executeCommand } from 'Store/Actions/commandActions'; import { executeCommand } from 'Store/Actions/commandActions';
import { clearEditions, fetchEditions } from 'Store/Actions/editionActions';
import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions'; import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions';
import createAllAuthorSelector from 'Store/Selectors/createAllAuthorsSelector'; import createAllAuthorSelector from 'Store/Selectors/createAllAuthorsSelector';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
@@ -43,11 +44,12 @@ function createMapStateToProps() {
(state, { titleSlug }) => titleSlug, (state, { titleSlug }) => titleSlug,
selectBookFiles, selectBookFiles,
(state) => state.books, (state) => state.books,
(state) => state.editions,
createAllAuthorSelector(), createAllAuthorSelector(),
createCommandsSelector(), createCommandsSelector(),
createUISettingsSelector(), createUISettingsSelector(),
createDimensionsSelector(), createDimensionsSelector(),
(titleSlug, bookFiles, books, authors, commands, uiSettings, dimensions) => { (titleSlug, bookFiles, books, editions, authors, commands, uiSettings, dimensions) => {
const book = books.items.find((b) => b.titleSlug === titleSlug); const book = books.items.find((b) => b.titleSlug === titleSlug);
const author = authors.find((a) => a.id === book.authorId); const author = authors.find((a) => a.id === book.authorId);
const sortedBooks = books.items.filter((b) => b.authorId === book.authorId); const sortedBooks = books.items.filter((b) => b.authorId === book.authorId);
@@ -79,8 +81,8 @@ function createMapStateToProps() {
isRefreshingCommand.body.bookId === book.id isRefreshingCommand.body.bookId === book.id
); );
const isFetching = isBookFilesFetching; const isFetching = isBookFilesFetching || editions.isFetching;
const isPopulated = isBookFilesPopulated; const isPopulated = isBookFilesPopulated && editions.isPopulated;
return { return {
...book, ...book,
@@ -104,6 +106,8 @@ const mapDispatchToProps = {
executeCommand, executeCommand,
fetchBookFiles, fetchBookFiles,
clearBookFiles, clearBookFiles,
fetchEditions,
clearEditions,
clearReleases, clearReleases,
cancelFetchReleases, cancelFetchReleases,
toggleBooksMonitored toggleBooksMonitored
@@ -121,7 +125,8 @@ class BookDetailsConnector extends Component {
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if (!_.isEqual(getMonitoredEditions(prevProps), getMonitoredEditions(this.props)) || if (prevProps.id !== this.props.id ||
!_.isEqual(getMonitoredEditions(prevProps), getMonitoredEditions(this.props)) ||
(prevProps.anyReleaseOk === false && this.props.anyReleaseOk === true)) { (prevProps.anyReleaseOk === false && this.props.anyReleaseOk === true)) {
this.unpopulate(); this.unpopulate();
this.populate(); this.populate();
@@ -140,12 +145,14 @@ class BookDetailsConnector extends Component {
const bookId = this.props.id; const bookId = this.props.id;
this.props.fetchBookFiles({ bookId }); this.props.fetchBookFiles({ bookId });
this.props.fetchEditions({ bookId });
} }
unpopulate = () => { unpopulate = () => {
this.props.cancelFetchReleases(); this.props.cancelFetchReleases();
this.props.clearReleases(); this.props.clearReleases();
this.props.clearBookFiles(); this.props.clearBookFiles();
this.props.clearEditions();
} }
// //
@@ -195,6 +202,8 @@ BookDetailsConnector.propTypes = {
titleSlug: PropTypes.string.isRequired, titleSlug: PropTypes.string.isRequired,
fetchBookFiles: PropTypes.func.isRequired, fetchBookFiles: PropTypes.func.isRequired,
clearBookFiles: PropTypes.func.isRequired, clearBookFiles: PropTypes.func.isRequired,
fetchEditions: PropTypes.func.isRequired,
clearEditions: PropTypes.func.isRequired,
clearReleases: PropTypes.func.isRequired, clearReleases: PropTypes.func.isRequired,
cancelFetchReleases: PropTypes.func.isRequired, cancelFetchReleases: PropTypes.func.isRequired,
toggleBooksMonitored: PropTypes.func.isRequired, toggleBooksMonitored: PropTypes.func.isRequired,
@@ -8,15 +8,25 @@ import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import BookDetailsHeader from './BookDetailsHeader'; import BookDetailsHeader from './BookDetailsHeader';
const selectOverview = createSelector(
(state) => state.editions,
(editions) => {
const monitored = editions.items.find((e) => e.monitored === true);
return monitored?.overview;
}
);
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
createBookSelector(), createBookSelector(),
selectOverview,
createUISettingsSelector(), createUISettingsSelector(),
createDimensionsSelector(), createDimensionsSelector(),
(book, uiSettings, dimensions) => { (book, overview, uiSettings, dimensions) => {
return { return {
...book, ...book,
overview,
shortDateFormat: uiSettings.shortDateFormat, shortDateFormat: uiSettings.shortDateFormat,
isSmallScreen: dimensions.isSmallScreen isSmallScreen: dimensions.isSmallScreen
}; };
@@ -53,7 +53,7 @@ class EditBookModalContent extends Component {
editions editions
} = item; } = item;
const hasFile = statistics ? statistics.bookFileCount : 0; const hasFile = statistics ? statistics.bookFileCount > 0 : false;
const errorMessage = getErrorMessage(error, 'Unable to load editions'); const errorMessage = getErrorMessage(error, 'Unable to load editions');
return ( return (
@@ -4,7 +4,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { saveBook, setBookValue } from 'Store/Actions/bookActions'; import { saveBook, setBookValue } from 'Store/Actions/bookActions';
import { clearEditions, fetchEditions } from 'Store/Actions/editionActions'; import { saveEditions } from 'Store/Actions/editionActions';
import createAuthorSelector from 'Store/Selectors/createAuthorSelector'; import createAuthorSelector from 'Store/Selectors/createAuthorSelector';
import createBookSelector from 'Store/Selectors/createBookSelector'; import createBookSelector from 'Store/Selectors/createBookSelector';
import selectSettings from 'Store/Selectors/selectSettings'; import selectSettings from 'Store/Selectors/selectSettings';
@@ -26,17 +26,14 @@ function createMapStateToProps() {
const { const {
isFetching, isFetching,
isPopulated, isPopulated,
error, error
items
} = editionState; } = editionState;
book.editions = items;
const bookSettings = _.pick(book, [ const bookSettings = _.pick(book, [
'monitored', 'monitored',
'anyEditionOk', 'anyEditionOk'
'editions'
]); ]);
bookSettings.editions = editionState.items;
const settings = selectSettings(bookSettings, pendingChanges, saveError); const settings = selectSettings(bookSettings, pendingChanges, saveError);
@@ -58,10 +55,9 @@ function createMapStateToProps() {
} }
const mapDispatchToProps = { const mapDispatchToProps = {
dispatchFetchEditions: fetchEditions,
dispatchClearEditions: clearEditions,
dispatchSetBookValue: setBookValue, dispatchSetBookValue: setBookValue,
dispatchSaveBook: saveBook dispatchSaveBook: saveBook,
dispatchSaveEditions: saveEditions
}; };
class EditBookModalContentConnector extends Component { class EditBookModalContentConnector extends Component {
@@ -69,20 +65,12 @@ class EditBookModalContentConnector extends Component {
// //
// Lifecycle // Lifecycle
componentDidMount() {
this.props.dispatchFetchEditions({ bookId: this.props.bookId });
}
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps, prevState) {
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) { if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
this.props.onModalClose(); this.props.onModalClose();
} }
} }
componentWillUnmount() {
this.props.dispatchClearEditions();
}
// //
// Listeners // Listeners
@@ -94,6 +82,9 @@ class EditBookModalContentConnector extends Component {
this.props.dispatchSaveBook({ this.props.dispatchSaveBook({
id: this.props.bookId id: this.props.bookId
}); });
this.props.dispatchSaveEditions({
id: this.props.bookId
});
} }
// //
@@ -114,10 +105,9 @@ EditBookModalContentConnector.propTypes = {
bookId: PropTypes.number, bookId: PropTypes.number,
isSaving: PropTypes.bool.isRequired, isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object, saveError: PropTypes.object,
dispatchFetchEditions: PropTypes.func.isRequired,
dispatchClearEditions: PropTypes.func.isRequired,
dispatchSetBookValue: PropTypes.func.isRequired, dispatchSetBookValue: PropTypes.func.isRequired,
dispatchSaveBook: PropTypes.func.isRequired, dispatchSaveBook: PropTypes.func.isRequired,
dispatchSaveEditions: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };
@@ -5,6 +5,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames'; import * as commandNames from 'Commands/commandNames';
import { executeCommand } from 'Store/Actions/commandActions'; import { executeCommand } from 'Store/Actions/commandActions';
import createBookAuthorSelector from 'Store/Selectors/createBookAuthorSelector';
import createBookQualityProfileSelector from 'Store/Selectors/createBookQualityProfileSelector'; import createBookQualityProfileSelector from 'Store/Selectors/createBookQualityProfileSelector';
import createBookSelector from 'Store/Selectors/createBookSelector'; import createBookSelector from 'Store/Selectors/createBookSelector';
import createExecutingCommandsSelector from 'Store/Selectors/createExecutingCommandsSelector'; import createExecutingCommandsSelector from 'Store/Selectors/createExecutingCommandsSelector';
@@ -32,11 +33,13 @@ function selectShowSearchAction() {
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
createBookSelector(), createBookSelector(),
createBookAuthorSelector(),
createBookQualityProfileSelector(), createBookQualityProfileSelector(),
selectShowSearchAction(), selectShowSearchAction(),
createExecutingCommandsSelector(), createExecutingCommandsSelector(),
( (
book, book,
author,
qualityProfile, qualityProfile,
showSearchAction, showSearchAction,
executingCommands executingCommands
@@ -54,7 +57,7 @@ function createMapStateToProps() {
const isRefreshingBook = executingCommands.some((command) => { const isRefreshingBook = executingCommands.some((command) => {
return ( return (
(command.name === commandNames.REFRESH_AUTHOR && (command.name === commandNames.REFRESH_AUTHOR &&
command.body.authorId === book.author.id) || command.body.authorId === book.authorId) ||
(command.name === commandNames.REFRESH_BOOK && (command.name === commandNames.REFRESH_BOOK &&
command.body.bookId === book.id) command.body.bookId === book.id)
); );
@@ -63,7 +66,7 @@ function createMapStateToProps() {
const isSearchingBook = executingCommands.some((command) => { const isSearchingBook = executingCommands.some((command) => {
return ( return (
(command.name === commandNames.AUTHOR_SEARCH && (command.name === commandNames.AUTHOR_SEARCH &&
command.body.authorId === book.author.id) || command.body.authorId === book.authorId) ||
(command.name === commandNames.BOOK_SEARCH && (command.name === commandNames.BOOK_SEARCH &&
command.body.bookIds.includes(book.id)) command.body.bookIds.includes(book.id))
); );
@@ -71,6 +74,7 @@ function createMapStateToProps() {
return { return {
...book, ...book,
author,
qualityProfile, qualityProfile,
showSearchAction, showSearchAction,
isRefreshingBook, isRefreshingBook,
@@ -12,6 +12,7 @@ import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import { icons } from 'Helpers/Props'; import { icons } from 'Helpers/Props';
import dimensions from 'Styles/Variables/dimensions'; import dimensions from 'Styles/Variables/dimensions';
import fonts from 'Styles/Variables/fonts'; import fonts from 'Styles/Variables/fonts';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import stripHtml from 'Utilities/String/stripHtml'; import stripHtml from 'Utilities/String/stripHtml';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import BookIndexOverviewInfo from './BookIndexOverviewInfo'; import BookIndexOverviewInfo from './BookIndexOverviewInfo';
@@ -42,10 +43,26 @@ class BookIndexOverview extends Component {
this.state = { this.state = {
isEditAuthorModalOpen: false, isEditAuthorModalOpen: false,
isDeleteAuthorModalOpen: false isDeleteAuthorModalOpen: false,
overview: ''
}; };
} }
componentDidMount() {
const { id } = this.props;
// Note that this component is lazy loaded by the virtualised view.
// We want to avoid storing overviews for *all* books which is
// why it's not put into the redux store
const promise = createAjaxRequest({
url: `/book/${id}/overview`
}).request;
promise.done((data) => {
this.setState({ overview: data.overview });
});
}
// //
// Listeners // Listeners
@@ -84,7 +101,6 @@ class BookIndexOverview extends Component {
const { const {
id, id,
title, title,
overview,
monitored, monitored,
titleSlug, titleSlug,
nextAiring, nextAiring,
@@ -118,6 +134,7 @@ class BookIndexOverview extends Component {
} = statistics; } = statistics;
const { const {
overview,
isEditAuthorModalOpen, isEditAuthorModalOpen,
isDeleteAuthorModalOpen isDeleteAuthorModalOpen
} = this.state; } = this.state;
@@ -267,7 +284,6 @@ class BookIndexOverview extends Component {
BookIndexOverview.propTypes = { BookIndexOverview.propTypes = {
id: PropTypes.number.isRequired, id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
overview: PropTypes.string.isRequired,
monitored: PropTypes.bool.isRequired, monitored: PropTypes.bool.isRequired,
titleSlug: PropTypes.string.isRequired, titleSlug: PropTypes.string.isRequired,
nextAiring: PropTypes.string, nextAiring: PropTypes.string,
+4 -3
View File
@@ -6,6 +6,7 @@ import SelectInput from 'Components/Form/SelectInput';
import SpinnerButton from 'Components/Link/SpinnerButton'; import SpinnerButton from 'Components/Link/SpinnerButton';
import PageContentFooter from 'Components/Page/PageContentFooter'; import PageContentFooter from 'Components/Page/PageContentFooter';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './BookshelfFooter.css'; import styles from './BookshelfFooter.css';
const NO_CHANGE = 'noChange'; const NO_CHANGE = 'noChange';
@@ -114,7 +115,7 @@ class BookshelfFooter extends Component {
<div className={styles.inputContainer}> <div className={styles.inputContainer}>
<div className={styles.label}> <div className={styles.label}>
Monitor Existing Books {translate('MonitorExistingBooks')}
</div> </div>
<MonitorBooksSelectInput <MonitorBooksSelectInput
@@ -128,7 +129,7 @@ class BookshelfFooter extends Component {
<div className={styles.inputContainer}> <div className={styles.inputContainer}>
<div className={styles.label}> <div className={styles.label}>
Monitor New Books {translate('MonitorNewBooks')}
</div> </div>
<MonitorNewItemsSelectInput <MonitorNewItemsSelectInput
@@ -152,7 +153,7 @@ class BookshelfFooter extends Component {
isDisabled={!selectedCount || noChanges} isDisabled={!selectedCount || noChanges}
onPress={this.onUpdateSelectedPress} onPress={this.onUpdateSelectedPress}
> >
Update Selected {translate('UpdateSelected')}
</SpinnerButton> </SpinnerButton>
</div> </div>
</PageContentFooter> </PageContentFooter>
@@ -12,9 +12,9 @@ import ModalBody from 'Components/Modal/ModalBody';
import Portal from 'Components/Portal'; import Portal from 'Components/Portal';
import Scroller from 'Components/Scroller/Scroller'; import Scroller from 'Components/Scroller/Scroller';
import { icons, scrollDirections, sizes } from 'Helpers/Props'; import { icons, scrollDirections, sizes } from 'Helpers/Props';
import { isMobile as isMobileUtil } from 'Utilities/browser';
import * as keyCodes from 'Utilities/Constants/keyCodes'; import * as keyCodes from 'Utilities/Constants/keyCodes';
import getUniqueElememtId from 'Utilities/getUniqueElementId'; import getUniqueElememtId from 'Utilities/getUniqueElementId';
import { isMobile as isMobileUtil } from 'Utilities/mobile';
import HintedSelectInputOption from './HintedSelectInputOption'; import HintedSelectInputOption from './HintedSelectInputOption';
import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue'; import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
import TextInput from './TextInput'; import TextInput from './TextInput';
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
import { inputTypes } from 'Helpers/Props'; import { inputTypes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
// import translate from 'Utilities/String/translate'; // import translate from 'Utilities/String/translate';
import AutoCompleteInput from './AutoCompleteInput'; import AutoCompleteInput from './AutoCompleteInput';
import BookEditionSelectInputConnector from './BookEditionSelectInputConnector'; import BookEditionSelectInputConnector from './BookEditionSelectInputConnector';
@@ -216,7 +217,7 @@ function FormInputGroup(props) {
<Link <Link
to={helpLink} to={helpLink}
> >
More Info {translate('MoreInfo')}
</Link> </Link>
} }
@@ -33,7 +33,7 @@ function ConfirmModal(props) {
return () => unbindShortcut('enter', onConfirm); return () => unbindShortcut('enter', onConfirm);
} }
}, [isOpen, onConfirm]); }, [bindShortcut, unbindShortcut, isOpen, onConfirm]);
return ( return (
<Modal <Modal
+1 -1
View File
@@ -6,9 +6,9 @@ import ReactDOM from 'react-dom';
import FocusLock from 'react-focus-lock'; import FocusLock from 'react-focus-lock';
import ErrorBoundary from 'Components/Error/ErrorBoundary'; import ErrorBoundary from 'Components/Error/ErrorBoundary';
import { sizes } from 'Helpers/Props'; import { sizes } from 'Helpers/Props';
import { isIOS } from 'Utilities/browser';
import * as keyCodes from 'Utilities/Constants/keyCodes'; import * as keyCodes from 'Utilities/Constants/keyCodes';
import getUniqueElememtId from 'Utilities/getUniqueElementId'; import getUniqueElememtId from 'Utilities/getUniqueElementId';
import { isIOS } from 'Utilities/mobile';
import { setScrollLock } from 'Utilities/scrollLock'; import { setScrollLock } from 'Utilities/scrollLock';
import ModalError from './ModalError'; import ModalError from './ModalError';
import styles from './Modal.css'; import styles from './Modal.css';
+1 -1
View File
@@ -14,7 +14,7 @@ function PageContent(props) {
return ( return (
<ErrorBoundary errorComponent={PageContentError}> <ErrorBoundary errorComponent={PageContentError}>
<DocumentTitle title={title ? `${title} - Readarr` : 'Readarr'}> <DocumentTitle title={title ? `${title} - ${window.Readarr.instanceName}` : window.Readarr.instanceName}>
<div className={className}> <div className={className}>
{children} {children}
</div> </div>
@@ -1,23 +1,12 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import OverlayScroller from 'Components/Scroller/OverlayScroller';
import Scroller from 'Components/Scroller/Scroller'; import Scroller from 'Components/Scroller/Scroller';
import { scrollDirections } from 'Helpers/Props'; import { scrollDirections } from 'Helpers/Props';
import { isMobile as isMobileUtil } from 'Utilities/mobile';
import { isLocked } from 'Utilities/scrollLock'; import { isLocked } from 'Utilities/scrollLock';
import styles from './PageContentBody.css'; import styles from './PageContentBody.css';
class PageContentBody extends Component { class PageContentBody extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this._isMobile = isMobileUtil();
}
// //
// Listeners // Listeners
@@ -41,10 +30,8 @@ class PageContentBody extends Component {
...otherProps ...otherProps
} = this.props; } = this.props;
const ScrollerComponent = this._isMobile ? Scroller : OverlayScroller;
return ( return (
<ScrollerComponent <Scroller
className={className} className={className}
scrollDirection={scrollDirections.VERTICAL} scrollDirection={scrollDirections.VERTICAL}
{...otherProps} {...otherProps}
@@ -53,7 +40,7 @@ class PageContentBody extends Component {
<div className={innerClassName}> <div className={innerClassName}>
{children} {children}
</div> </div>
</ScrollerComponent> </Scroller>
); );
} }
} }
@@ -1,4 +1,5 @@
.jumpBar { .jumpBar {
z-index: $pageJumpBarZIndex;
display: flex; display: flex;
align-content: stretch; align-content: stretch;
align-items: stretch; align-items: stretch;
@@ -1,6 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Scrollbars } from 'react-custom-scrollbars'; import { Scrollbars } from 'react-custom-scrollbars-2';
import { scrollDirections } from 'Helpers/Props'; import { scrollDirections } from 'Helpers/Props';
import styles from './OverlayScroller.css'; import styles from './OverlayScroller.css';
+1 -1
View File
@@ -1,7 +1,7 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Measure from 'Components/Measure'; import Measure from 'Components/Measure';
import { isMobile as isMobileUtil } from 'Utilities/mobile'; import { isMobile as isMobileUtil } from 'Utilities/browser';
import styles from './SwipeHeader.css'; import styles from './SwipeHeader.css';
function cursorPosition(event) { function cursorPosition(event) {
+1 -1
View File
@@ -5,7 +5,7 @@ import { Manager, Popper, Reference } from 'react-popper';
import Portal from 'Components/Portal'; import Portal from 'Components/Portal';
import { kinds, tooltipPositions } from 'Helpers/Props'; import { kinds, tooltipPositions } from 'Helpers/Props';
import dimensions from 'Styles/Variables/dimensions'; import dimensions from 'Styles/Variables/dimensions';
import { isMobile as isMobileUtil } from 'Utilities/mobile'; import { isMobile as isMobileUtil } from 'Utilities/browser';
import styles from './Tooltip.css'; import styles from './Tooltip.css';
let maxWidth = null; let maxWidth = null;
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Alert from 'Components/Alert'; import Alert from 'Components/Alert';
import Button from 'Components/Link/Button'; import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ModalBody from 'Components/Modal/ModalBody'; import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent'; import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
@@ -9,7 +10,8 @@ import ModalHeader from 'Components/Modal/ModalHeader';
import Table from 'Components/Table/Table'; import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody'; import TableBody from 'Components/Table/TableBody';
import { scrollDirections } from 'Helpers/Props'; import { scrollDirections } from 'Helpers/Props';
import SelectEditionRow from './SelectEditionRow'; import translate from 'Utilities/String/translate';
import SelectEditionRowConnector from './SelectEditionRowConnector';
import styles from './SelectEditionModalContent.css'; import styles from './SelectEditionModalContent.css';
const columns = [ const columns = [
@@ -33,15 +35,30 @@ class SelectEditionModalContent extends Component {
render() { render() {
const { const {
books, books,
isPopulated,
isFetching,
error,
onEditionSelect, onEditionSelect,
onModalClose, onModalClose,
...otherProps ...otherProps
} = this.props; } = this.props;
if (!isPopulated && !error) {
return (<LoadingIndicator />);
}
if (!isFetching && error) {
return (
<div>
{translate('LoadingEditionsFailed')}
</div>
);
}
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
Manual Import - Select Edition {translate('ManualImportSelectEdition')}
</ModalHeader> </ModalHeader>
<ModalBody <ModalBody
@@ -60,7 +77,7 @@ class SelectEditionModalContent extends Component {
{ {
books.map((item) => { books.map((item) => {
return ( return (
<SelectEditionRow <SelectEditionRowConnector
key={item.book.id} key={item.book.id}
matchedEditionId={item.matchedEditionId} matchedEditionId={item.matchedEditionId}
columns={columns} columns={columns}
@@ -76,7 +93,7 @@ class SelectEditionModalContent extends Component {
<ModalFooter> <ModalFooter>
<Button onPress={onModalClose}> <Button onPress={onModalClose}>
Cancel {translate('Cancel')}
</Button> </Button>
</ModalFooter> </ModalFooter>
</ModalContent> </ModalContent>
@@ -86,6 +103,9 @@ class SelectEditionModalContent extends Component {
SelectEditionModalContent.propTypes = { SelectEditionModalContent.propTypes = {
books: PropTypes.arrayOf(PropTypes.object).isRequired, books: PropTypes.arrayOf(PropTypes.object).isRequired,
isFetching: PropTypes.bool,
isPopulated: PropTypes.bool,
error: PropTypes.object,
onEditionSelect: PropTypes.func.isRequired, onEditionSelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };
@@ -1,27 +1,71 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { clearEditions, fetchEditions } from 'Store/Actions/editionActions';
import { import {
saveInteractiveImportItem, saveInteractiveImportItem,
updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions'; updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import SelectEditionModalContent from './SelectEditionModalContent'; import SelectEditionModalContent from './SelectEditionModalContent';
function createMapStateToProps() { function createMapStateToProps() {
return {}; return createSelector(
(state) => state.editions,
(editions) => {
const {
isFetching,
isPopulated,
error
} = editions;
return {
isFetching,
isPopulated,
error
};
}
);
} }
const mapDispatchToProps = { const mapDispatchToProps = {
fetchEditions,
clearEditions,
updateInteractiveImportItem, updateInteractiveImportItem,
saveInteractiveImportItem saveInteractiveImportItem
}; };
class SelectEditionModalContentConnector extends Component { class SelectEditionModalContentConnector extends Component {
//
// Lifecycle
componentDidMount() {
registerPagePopulator(this.populate);
this.populate();
}
componentWillUnmount() {
unregisterPagePopulator(this.populate);
this.unpopulate();
}
//
// Control
populate = () => {
const bookId = this.props.books.map((b) => b.book.id);
this.props.fetchEditions({ bookId });
}
unpopulate = () => {
this.props.clearEditions();
}
// //
// Listeners // Listeners
onEditionSelect = (bookId, foreignEditionId) => { onEditionSelect = (bookId, foreignEditionId) => {
console.log(`book: ${bookId} id: ${foreignEditionId} ${typeof foreignEditionId}`);
const ids = this.props.importIdsByBook[bookId]; const ids = this.props.importIdsByBook[bookId];
ids.forEach((id) => { ids.forEach((id) => {
@@ -55,6 +99,8 @@ class SelectEditionModalContentConnector extends Component {
SelectEditionModalContentConnector.propTypes = { SelectEditionModalContentConnector.propTypes = {
importIdsByBook: PropTypes.object.isRequired, importIdsByBook: PropTypes.object.isRequired,
books: PropTypes.arrayOf(PropTypes.object).isRequired, books: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchEditions: PropTypes.func.isRequired,
clearEditions: PropTypes.func.isRequired,
updateInteractiveImportItem: PropTypes.func.isRequired, updateInteractiveImportItem: PropTypes.func.isRequired,
saveInteractiveImportItem: PropTypes.func.isRequired, saveInteractiveImportItem: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
@@ -0,0 +1,32 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import SelectEditionRow from './SelectEditionRow';
function createMapStateToProps() {
return createSelector(
(state, { id }) => id,
(state) => state.editions,
(id, editionState) => {
const editions = editionState.items.filter((e) => e.bookId === id);
return { editions };
}
);
}
class SelectEditionRowConnector extends Component {
render() {
return (
<SelectEditionRow
{...this.props}
/>
);
}
}
SelectEditionRowConnector.PropTypes = {
editions: PropTypes.arrayOf(PropTypes.object).isRequired
};
export default connect(createMapStateToProps)(SelectEditionRowConnector);
+1 -2
View File
@@ -148,11 +148,10 @@ class AddNewItem extends Component {
); );
} else if (item.book) { } else if (item.book) {
const book = item.book; const book = item.book;
const edition = book.editions.find((x) => x.monitored);
return ( return (
<AddNewBookSearchResultConnector <AddNewBookSearchResultConnector
key={item.id} key={item.id}
isExistingBook={'id' in edition && edition.id !== 0} isExistingBook={'id' in book && book.id !== 0}
isExistingAuthor={'id' in book.author && book.author.id !== 0} isExistingAuthor={'id' in book.author && book.author.id !== 0}
{...book} {...book}
/> />
@@ -138,17 +138,20 @@ class AddNewBookSearchResult extends Component {
null null
} }
<Link {
className={styles.mbLink} editions && editions.length > 1 ?
to={`https://goodreads.com/book/show/${editions[0].foreignEditionId}`} <Link
onPress={this.onTVDBLinkPress} className={styles.mbLink}
> to={`https://goodreads.com/book/show/${editions[0].foreignEditionId}`}
<Icon onPress={this.onTVDBLinkPress}
className={styles.mbLinkIcon} >
name={icons.EXTERNAL_LINK} <Icon
size={28} className={styles.mbLinkIcon}
/> name={icons.EXTERNAL_LINK}
</Link> size={28}
/>
</Link> : null
}
</div> </div>
</div> </div>
@@ -218,7 +221,7 @@ AddNewBookSearchResult.propTypes = {
overview: PropTypes.string, overview: PropTypes.string,
ratings: PropTypes.object.isRequired, ratings: PropTypes.object.isRequired,
author: PropTypes.object, author: PropTypes.object,
editions: PropTypes.arrayOf(PropTypes.object).isRequired, editions: PropTypes.arrayOf(PropTypes.object),
images: PropTypes.arrayOf(PropTypes.object).isRequired, images: PropTypes.arrayOf(PropTypes.object).isRequired,
isExistingBook: PropTypes.bool.isRequired, isExistingBook: PropTypes.bool.isRequired,
isExistingAuthor: PropTypes.bool.isRequired, isExistingAuthor: PropTypes.bool.isRequired,
@@ -126,7 +126,7 @@ class AddAuthorOptionsForm extends Component {
<FormGroup className={showMetadataProfile ? undefined : styles.hideMetadataProfile}> <FormGroup className={showMetadataProfile ? undefined : styles.hideMetadataProfile}>
<FormLabel> <FormLabel>
Metadata Profile {translate('MetadataProfile')}
{ {
includeNoneMetadataProfile && includeNoneMetadataProfile &&
@@ -173,7 +173,7 @@ class AddAuthorOptionsForm extends Component {
AddAuthorOptionsForm.propTypes = { AddAuthorOptionsForm.propTypes = {
rootFolderPath: PropTypes.object, rootFolderPath: PropTypes.object,
monitor: PropTypes.object.isRequired, monitor: PropTypes.object.isRequired,
monitorNewItems: PropTypes.string.isRequired, monitorNewItems: PropTypes.object.isRequired,
qualityProfileId: PropTypes.object, qualityProfileId: PropTypes.object,
metadataProfileId: PropTypes.object, metadataProfileId: PropTypes.object,
showMetadataProfile: PropTypes.bool.isRequired, showMetadataProfile: PropTypes.bool.isRequired,
@@ -20,6 +20,7 @@ function HostSettings(props) {
bindAddress, bindAddress,
port, port,
urlBase, urlBase,
instanceName,
enableSsl, enableSsl,
sslPort, sslPort,
sslCertPath, sslCertPath,
@@ -57,6 +58,7 @@ function HostSettings(props) {
name="port" name="port"
min={1} min={1}
max={65535} max={65535}
autocomplete="off"
helpTextWarning={translate('PortHelpTextWarning')} helpTextWarning={translate('PortHelpTextWarning')}
onChange={onInputChange} onChange={onInputChange}
{...port} {...port}
@@ -78,6 +80,22 @@ function HostSettings(props) {
/> />
</FormGroup> </FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('InstanceName')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="instanceName"
helpText={translate('InstanceNameHelpText')}
helpTextWarning={translate('RestartRequiredHelpTextWarning')}
onChange={onInputChange}
{...instanceName}
/>
</FormGroup>
<FormGroup <FormGroup
advancedSettings={advancedSettings} advancedSettings={advancedSettings}
isAdvanced={true} isAdvanced={true}
@@ -28,17 +28,17 @@ function ImportListMonitoringOptionsPopoverContent() {
<DescriptionList> <DescriptionList>
<DescriptionListItem <DescriptionListItem
title={translate('None')} title={translate('None')}
data="Do not monitor authors or books" data={translate('DataListMonitorNone')}
/> />
<DescriptionListItem <DescriptionListItem
title={translate('SpecificBook')} title={translate('SpecificBook')}
data="Monitor authors but only monitor books explicitly included in the list" data={translate('DataListMonitorSpecificBook')}
/> />
<DescriptionListItem <DescriptionListItem
title={translate('AllAuthorBooks')} title={translate('AllAuthorBooks')}
data="Monitor authors and all books for each author included on the import list" data={translate('DataListMonitorAll')}
/> />
</DescriptionList> </DescriptionList>
); );
@@ -89,7 +89,7 @@ function EditImportListModalContent(props) {
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
{id ? 'Edit List' : 'Add List'} {id ? translate('EditList') : translate('AddList')}
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
@@ -148,7 +148,7 @@ function EditImportListModalContent(props) {
<FormGroup> <FormGroup>
<FormLabel> <FormLabel>
Monitor {translate('Monitor')}
<Popover <Popover
anchor={ anchor={
@@ -318,7 +318,7 @@ function EditImportListModalContent(props) {
kind={kinds.DANGER} kind={kinds.DANGER}
onPress={onDeleteImportListPress} onPress={onDeleteImportListPress}
> >
Delete {translate('Delete')}
</Button> </Button>
} }
@@ -327,13 +327,13 @@ function EditImportListModalContent(props) {
error={saveError} error={saveError}
onPress={onTestPress} onPress={onTestPress}
> >
Test {translate('Test')}
</SpinnerErrorButton> </SpinnerErrorButton>
<Button <Button
onPress={onModalClose} onPress={onModalClose}
> >
Cancel {translate('Cancel')}
</Button> </Button>
<SpinnerErrorButton <SpinnerErrorButton
@@ -341,7 +341,7 @@ function EditImportListModalContent(props) {
error={saveError} error={saveError}
onPress={onSavePress} onPress={onSavePress}
> >
Save {translate('Save')}
</SpinnerErrorButton> </SpinnerErrorButton>
</ModalFooter> </ModalFooter>
</ModalContent> </ModalContent>
@@ -346,7 +346,7 @@ function EditRootFolderModalContent(props) {
<FormGroup> <FormGroup>
<FormLabel> <FormLabel>
Convert to format {translate('ConvertToFormat')}
<Popover <Popover
anchor={ anchor={
<Icon <Icon
@@ -371,7 +371,7 @@ function EditRootFolderModalContent(props) {
<FormGroup> <FormGroup>
<FormLabel> <FormLabel>
Calibre Output Profile {translate('CalibreOutputProfile')}
<Popover <Popover
anchor={ anchor={
<Icon <Icon
@@ -423,14 +423,14 @@ function EditRootFolderModalContent(props) {
kind={kinds.DANGER} kind={kinds.DANGER}
onPress={onDeleteRootFolderPress} onPress={onDeleteRootFolderPress}
> >
Delete {translate('Delete')}
</Button> </Button>
} }
<Button <Button
onPress={onModalClose} onPress={onModalClose}
> >
Cancel {translate('Cancel')}
</Button> </Button>
<SpinnerErrorButton <SpinnerErrorButton
@@ -438,7 +438,7 @@ function EditRootFolderModalContent(props) {
error={saveError} error={saveError}
onPress={onSavePress} onPress={onSavePress}
> >
Save {translate('Save')}
</SpinnerErrorButton> </SpinnerErrorButton>
</ModalFooter> </ModalFooter>
</ModalContent> </ModalContent>
+1 -1
View File
@@ -19,7 +19,7 @@ function PendingChangesModal(props) {
useEffect(() => { useEffect(() => {
bindShortcut('enter', onConfirm); bindShortcut('enter', onConfirm);
}, [onConfirm]); }, [bindShortcut, onConfirm]);
return ( return (
<Modal <Modal
@@ -78,12 +78,8 @@ export const actionHandlers = handleThunks({
} = payload; } = payload;
const promise = createAjaxRequest({ const promise = createAjaxRequest({
url: '/history/failed', url: `/history/failed/${historyId}`,
method: 'POST', method: 'POST'
data: {
id: historyId
},
dataType: 'json'
}).request; }).request;
promise.done(() => { promise.done(() => {
@@ -86,12 +86,8 @@ export const actionHandlers = handleThunks({
} = payload; } = payload;
const promise = createAjaxRequest({ const promise = createAjaxRequest({
url: '/history/failed', url: `/history/failed/${historyId}`,
method: 'POST', method: 'POST'
data: {
id: historyId
},
dataType: 'json'
}).request; }).request;
promise.done(() => { promise.done(() => {
+25 -1
View File
@@ -1,5 +1,8 @@
import { createAction } from 'redux-actions'; import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions';
import { createThunk, handleThunks } from 'Store/thunks'; import { createThunk, handleThunks } from 'Store/thunks';
import getProviderState from 'Utilities/State/getProviderState';
import { updateItem } from './baseActions';
import createFetchHandler from './Creators/createFetchHandler'; import createFetchHandler from './Creators/createFetchHandler';
import createHandleActions from './Creators/createHandleActions'; import createHandleActions from './Creators/createHandleActions';
import createClearReducer from './Creators/Reducers/createClearReducer'; import createClearReducer from './Creators/Reducers/createClearReducer';
@@ -25,18 +28,39 @@ export const defaultState = {
export const FETCH_EDITIONS = 'editions/fetchEditions'; export const FETCH_EDITIONS = 'editions/fetchEditions';
export const CLEAR_EDITIONS = 'editions/clearEditions'; export const CLEAR_EDITIONS = 'editions/clearEditions';
export const SAVE_EDITIONS = 'editions/saveEditions';
// //
// Action Creators // Action Creators
export const fetchEditions = createThunk(FETCH_EDITIONS); export const fetchEditions = createThunk(FETCH_EDITIONS);
export const clearEditions = createAction(CLEAR_EDITIONS); export const clearEditions = createAction(CLEAR_EDITIONS);
export const saveEditions = createThunk(SAVE_EDITIONS);
// //
// Action Handlers // Action Handlers
export const actionHandlers = handleThunks({ export const actionHandlers = handleThunks({
[FETCH_EDITIONS]: createFetchHandler(section, '/edition') [FETCH_EDITIONS]: createFetchHandler(section, '/edition'),
[SAVE_EDITIONS]: function(getState, payload, dispatch) {
const {
id,
...otherPayload
} = payload;
const saveData = getProviderState({ id, ...otherPayload }, getState, 'books');
dispatch(batchActions([
...saveData.editions.map((edition) => {
return updateItem({
id: edition.id,
section: 'editions',
...edition
});
})
]));
}
}); });
// //
+1 -4
View File
@@ -246,11 +246,8 @@ export const actionHandlers = handleThunks({
})); }));
const promise = createAjaxRequest({ const promise = createAjaxRequest({
url: '/history/failed', url: `/history/failed/${id}`,
method: 'POST', method: 'POST',
data: {
id
},
dataType: 'json' dataType: 'json'
}).request; }).request;
+2 -1
View File
@@ -177,7 +177,8 @@ export const defaultState = {
{ {
name: 'size', name: 'size',
label: 'Size', label: 'Size',
type: filterBuilderTypes.NUMBER type: filterBuilderTypes.NUMBER,
valueType: filterBuilderValueTypes.BYTES
}, },
{ {
name: 'seeders', name: 'seeders',
@@ -0,0 +1,15 @@
import { createSelector } from 'reselect';
import createBookSelector from './createBookSelector';
function createBookAuthorSelector() {
return createSelector(
createBookSelector(),
(state) => state.authors.itemMap,
(state) => state.authors.items,
(book, authorMap, allAuthors) => {
return allAuthors[authorMap[book.authorId]];
}
);
}
export default createBookAuthorSelector;
@@ -1,10 +1,10 @@
import { createSelector, createSelectorCreator, defaultMemoize } from 'reselect'; import { createSelector, createSelectorCreator, defaultMemoize } from 'reselect';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder'; import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
import createClientSideCollectionSelector from './createClientSideCollectionSelector'; import createBooksClientSideCollectionSelector from './createBooksClientSideCollectionSelector';
function createUnoptimizedSelector(uiSection) { function createUnoptimizedSelector(uiSection) {
return createSelector( return createSelector(
createClientSideCollectionSelector('books', uiSection), createBooksClientSideCollectionSelector(uiSection),
(books) => { (books) => {
const items = books.items.map((s) => { const items = books.items.map((s) => {
const { const {
@@ -1,18 +1,16 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import createBookSelector from './createBookSelector'; import createBookAuthorSelector from './createBookAuthorSelector';
function createBookQualityProfileSelector() { function createBookQualityProfileSelector() {
return createSelector( return createSelector(
(state) => state.settings.qualityProfiles.items, (state) => state.settings.qualityProfiles.items,
createBookSelector(), createBookAuthorSelector(),
(qualityProfiles, book) => { (qualityProfiles, author) => {
if (!book) { if (!author) {
return {}; return {};
} }
return qualityProfiles.find((profile) => { return qualityProfiles.find((profile) => profile.id === author.qualityProfileId);
return profile.id === book.author.qualityProfileId;
});
} }
); );
} }
@@ -0,0 +1,35 @@
import _ from 'lodash';
import { createSelector } from 'reselect';
import filterCollection from 'Utilities/Array/filterCollection';
import sortCollection from 'Utilities/Array/sortCollection';
import createCustomFiltersSelector from './createCustomFiltersSelector';
function createBooksClientSideCollectionSelector(uiSection) {
return createSelector(
(state) => _.get(state, 'books'),
(state) => _.get(state, 'authors'),
(state) => _.get(state, uiSection),
createCustomFiltersSelector('books', uiSection),
(bookState, authorState, uiSectionState = {}, customFilters) => {
const state = Object.assign({}, bookState, uiSectionState, { customFilters });
const books = state.items;
for (const book of books) {
book.author = authorState.items[authorState.itemMap[book.authorId]];
}
const filtered = filterCollection(books, state);
const sorted = sortCollection(filtered, state);
return {
...bookState,
...uiSectionState,
customFilters,
items: sorted,
totalItems: state.items.length
};
}
);
}
export default createBooksClientSideCollectionSelector;
@@ -1,123 +1,8 @@
import _ from 'lodash'; import _ from 'lodash';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { filterTypePredicates, filterTypes, sortDirections } from 'Helpers/Props'; import filterCollection from 'Utilities/Array/filterCollection';
import findSelectedFilters from 'Utilities/Filter/findSelectedFilters'; import sortCollection from 'Utilities/Array/sortCollection';
import createCustomFiltersSelector from './createCustomFiltersSelector';
function getSortClause(sortKey, sortDirection, sortPredicates) {
if (sortPredicates && sortPredicates.hasOwnProperty(sortKey)) {
return function(item) {
return sortPredicates[sortKey](item, sortDirection);
};
}
return function(item) {
return item[sortKey];
};
}
function filter(items, state) {
const {
selectedFilterKey,
filters,
customFilters,
filterPredicates
} = state;
if (!selectedFilterKey) {
return items;
}
const selectedFilters = findSelectedFilters(selectedFilterKey, filters, customFilters);
return _.filter(items, (item) => {
let i = 0;
let accepted = true;
while (accepted && i < selectedFilters.length) {
const {
key,
value,
type = filterTypes.EQUAL
} = selectedFilters[i];
if (filterPredicates && filterPredicates.hasOwnProperty(key)) {
const predicate = filterPredicates[key];
if (Array.isArray(value)) {
if (
type === filterTypes.NOT_CONTAINS ||
type === filterTypes.NOT_EQUAL
) {
accepted = value.every((v) => predicate(item, v, type));
} else {
accepted = value.some((v) => predicate(item, v, type));
}
} else {
accepted = predicate(item, value, type);
}
} else if (item.hasOwnProperty(key)) {
const predicate = filterTypePredicates[type];
if (Array.isArray(value)) {
if (
type === filterTypes.NOT_CONTAINS ||
type === filterTypes.NOT_EQUAL
) {
accepted = value.every((v) => predicate(item[key], v));
} else {
accepted = value.some((v) => predicate(item[key], v));
}
} else {
accepted = predicate(item[key], value);
}
} else {
// Default to false if the filter can't be tested
accepted = false;
}
i++;
}
return accepted;
});
}
function sort(items, state) {
const {
sortKey,
sortDirection,
sortPredicates,
secondarySortKey,
secondarySortDirection
} = state;
const clauses = [];
const orders = [];
clauses.push(getSortClause(sortKey, sortDirection, sortPredicates));
orders.push(sortDirection === sortDirections.ASCENDING ? 'asc' : 'desc');
if (secondarySortKey &&
secondarySortDirection &&
(sortKey !== secondarySortKey ||
sortDirection !== secondarySortDirection)) {
clauses.push(getSortClause(secondarySortKey, secondarySortDirection, sortPredicates));
orders.push(secondarySortDirection === sortDirections.ASCENDING ? 'asc' : 'desc');
}
return _.orderBy(items, clauses, orders);
}
function createCustomFiltersSelector(type, alternateType) {
return createSelector(
(state) => state.customFilters.items,
(customFilters) => {
return customFilters.filter((customFilter) => {
return customFilter.type === type || customFilter.type === alternateType;
});
}
);
}
function createClientSideCollectionSelector(section, uiSection) { function createClientSideCollectionSelector(section, uiSection) {
return createSelector( return createSelector(
@@ -127,8 +12,8 @@ function createClientSideCollectionSelector(section, uiSection) {
(sectionState, uiSectionState = {}, customFilters) => { (sectionState, uiSectionState = {}, customFilters) => {
const state = Object.assign({}, sectionState, uiSectionState, { customFilters }); const state = Object.assign({}, sectionState, uiSectionState, { customFilters });
const filtered = filter(state.items, state); const filtered = filterCollection(state.items, state);
const sorted = sort(filtered, state); const sorted = sortCollection(filtered, state);
return { return {
...sectionState, ...sectionState,
@@ -0,0 +1,14 @@
import { createSelector } from 'reselect';
function createCustomFiltersSelector(type, alternateType) {
return createSelector(
(state) => state.customFilters.items,
(customFilters) => {
return customFilters.filter((customFilter) => {
return customFilter.type === type || customFilter.type === alternateType;
});
}
);
}
export default createCustomFiltersSelector;
+3
View File
@@ -1,4 +1,7 @@
@define-mixin scrollbar { @define-mixin scrollbar {
scrollbar-color: var(--scrollbarBackgroundColor) transparent;
scrollbar-width: thin;
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: 10px; width: 10px;
height: 10px; height: 10px;
@@ -1,4 +1,5 @@
module.exports = { module.exports = {
pageJumpBarZIndex: 10,
modalZIndex: 1000, modalZIndex: 1000,
popperZIndex: 2000 popperZIndex: 2000
}; };
+9 -10
View File
@@ -20,10 +20,11 @@ class About extends Component {
packageVersion, packageVersion,
packageAuthor, packageAuthor,
isNetCore, isNetCore,
isMono,
isDocker, isDocker,
runtimeVersion, runtimeVersion,
migrationVersion, migrationVersion,
databaseVersion,
databaseType,
appData, appData,
startupPath, startupPath,
mode, mode,
@@ -48,14 +49,6 @@ class About extends Component {
/> />
} }
{
isMono &&
<DescriptionListItem
title={translate('MonoVersion')}
data={runtimeVersion}
/>
}
{ {
isNetCore && isNetCore &&
<DescriptionListItem <DescriptionListItem
@@ -77,6 +70,11 @@ class About extends Component {
data={migrationVersion} data={migrationVersion}
/> />
<DescriptionListItem
title={translate('Database')}
data={`${titleCase(databaseType)} ${databaseVersion}`}
/>
<DescriptionListItem <DescriptionListItem
title={translate('AppDataDirectory')} title={translate('AppDataDirectory')}
data={appData} data={appData}
@@ -114,10 +112,11 @@ About.propTypes = {
packageVersion: PropTypes.string, packageVersion: PropTypes.string,
packageAuthor: PropTypes.string, packageAuthor: PropTypes.string,
isNetCore: PropTypes.bool.isRequired, isNetCore: PropTypes.bool.isRequired,
isMono: PropTypes.bool.isRequired,
runtimeVersion: PropTypes.string.isRequired, runtimeVersion: PropTypes.string.isRequired,
isDocker: PropTypes.bool.isRequired, isDocker: PropTypes.bool.isRequired,
migrationVersion: PropTypes.number.isRequired, migrationVersion: PropTypes.number.isRequired,
databaseType: PropTypes.string.isRequired,
databaseVersion: PropTypes.string.isRequired,
appData: PropTypes.string.isRequired, appData: PropTypes.string.isRequired,
startupPath: PropTypes.string.isRequired, startupPath: PropTypes.string.isRequired,
mode: PropTypes.string.isRequired, mode: PropTypes.string.isRequired,
@@ -0,0 +1,72 @@
import _ from 'lodash';
import { filterTypePredicates, filterTypes } from 'Helpers/Props';
import findSelectedFilters from 'Utilities/Filter/findSelectedFilters';
function filterCollection(items, state) {
const {
selectedFilterKey,
filters,
customFilters,
filterPredicates
} = state;
if (!selectedFilterKey) {
return items;
}
const selectedFilters = findSelectedFilters(selectedFilterKey, filters, customFilters);
return _.filter(items, (item) => {
let i = 0;
let accepted = true;
while (accepted && i < selectedFilters.length) {
const {
key,
value,
type = filterTypes.EQUAL
} = selectedFilters[i];
if (filterPredicates && filterPredicates.hasOwnProperty(key)) {
const predicate = filterPredicates[key];
if (Array.isArray(value)) {
if (
type === filterTypes.NOT_CONTAINS ||
type === filterTypes.NOT_EQUAL
) {
accepted = value.every((v) => predicate(item, v, type));
} else {
accepted = value.some((v) => predicate(item, v, type));
}
} else {
accepted = predicate(item, value, type);
}
} else if (item.hasOwnProperty(key)) {
const predicate = filterTypePredicates[type];
if (Array.isArray(value)) {
if (
type === filterTypes.NOT_CONTAINS ||
type === filterTypes.NOT_EQUAL
) {
accepted = value.every((v) => predicate(item[key], v));
} else {
accepted = value.some((v) => predicate(item[key], v));
}
} else {
accepted = predicate(item[key], value);
}
} else {
// Default to false if the filter can't be tested
accepted = false;
}
i++;
}
return accepted;
});
}
export default filterCollection;
@@ -0,0 +1,42 @@
import _ from 'lodash';
import { sortDirections } from 'Helpers/Props';
function getSortClause(sortKey, sortDirection, sortPredicates) {
if (sortPredicates && sortPredicates.hasOwnProperty(sortKey)) {
return function(item) {
return sortPredicates[sortKey](item, sortDirection);
};
}
return function(item) {
return item[sortKey];
};
}
function sortCollection(items, state) {
const {
sortKey,
sortDirection,
sortPredicates,
secondarySortKey,
secondarySortDirection
} = state;
const clauses = [];
const orders = [];
clauses.push(getSortClause(sortKey, sortDirection, sortPredicates));
orders.push(sortDirection === sortDirections.ASCENDING ? 'asc' : 'desc');
if (secondarySortKey &&
secondarySortDirection &&
(sortKey !== secondarySortKey ||
sortDirection !== secondarySortDirection)) {
clauses.push(getSortClause(secondarySortKey, secondarySortDirection, sortPredicates));
orders.push(secondarySortDirection === sortDirections.ASCENDING ? 'asc' : 'desc');
}
return _.orderBy(items, clauses, orders);
}
export default sortCollection;
@@ -10,3 +10,7 @@ export function isMobile() {
export function isIOS() { export function isIOS() {
return mobileDetect.is('iOS'); return mobileDetect.is('iOS');
} }
export function isFirefox() {
return window.navigator.userAgent.toLowerCase().indexOf('firefox/') >= 0;
}
+3 -2
View File
@@ -33,7 +33,7 @@
"@fortawesome/free-regular-svg-icons": "5.15.3", "@fortawesome/free-regular-svg-icons": "5.15.3",
"@fortawesome/free-solid-svg-icons": "5.15.3", "@fortawesome/free-solid-svg-icons": "5.15.3",
"@fortawesome/react-fontawesome": "0.1.14", "@fortawesome/react-fontawesome": "0.1.14",
"@microsoft/signalr": "6.0.3", "@microsoft/signalr": "6.0.7",
"@sentry/browser": "6.18.2", "@sentry/browser": "6.18.2",
"@sentry/integrations": "6.18.2", "@sentry/integrations": "6.18.2",
"ansi-colors": "4.1.1", "ansi-colors": "4.1.1",
@@ -58,7 +58,7 @@
"react-addons-shallow-compare": "15.6.3", "react-addons-shallow-compare": "15.6.3",
"react-async-script": "1.2.0", "react-async-script": "1.2.0",
"react-autosuggest": "10.1.0", "react-autosuggest": "10.1.0",
"react-custom-scrollbars": "4.2.1", "react-custom-scrollbars-2": "4.5.0",
"react-dnd": "14.0.2", "react-dnd": "14.0.2",
"react-dnd-html5-backend": "14.0.0", "react-dnd-html5-backend": "14.0.0",
"react-dnd-multi-backend": "6.0.2", "react-dnd-multi-backend": "6.0.2",
@@ -109,6 +109,7 @@
"eslint-plugin-filenames": "1.3.2", "eslint-plugin-filenames": "1.3.2",
"eslint-plugin-import": "2.23.4", "eslint-plugin-import": "2.23.4",
"eslint-plugin-react": "7.24.0", "eslint-plugin-react": "7.24.0",
"eslint-plugin-react-hooks": "4.6.0",
"eslint-plugin-simple-import-sort": "7.0.0", "eslint-plugin-simple-import-sort": "7.0.0",
"esprint": "3.1.0", "esprint": "3.1.0",
"file-loader": "6.2.0", "file-loader": "6.2.0",
+20 -18
View File
@@ -4,60 +4,62 @@
<PackageVersion Include="AutoFixture" Version="4.17.0" /> <PackageVersion Include="AutoFixture" Version="4.17.0" />
<PackageVersion Include="coverlet.collector" Version="3.0.4-preview.27.ge7cb7c3b40" PrivateAssets="all" /> <PackageVersion Include="coverlet.collector" Version="3.0.4-preview.27.ge7cb7c3b40" PrivateAssets="all" />
<PackageVersion Include="Dapper" Version="2.0.123" /> <PackageVersion Include="Dapper" Version="2.0.123" />
<PackageVersion Include="DryIoc.dll" Version="4.8.7" /> <PackageVersion Include="DryIoc.dll" Version="5.2.0" />
<PackageVersion Include="DryIoc.Microsoft.DependencyInjection" Version="5.1.0" /> <PackageVersion Include="DryIoc.Microsoft.DependencyInjection" Version="6.0.2" />
<PackageVersion Include="Equ" Version="2.3.0" /> <PackageVersion Include="Equ" Version="2.3.0" />
<PackageVersion Include="FluentAssertions" Version="5.10.3" /> <PackageVersion Include="FluentAssertions" Version="5.10.3" />
<PackageVersion Include="FluentMigrator.Runner.SQLite" Version="4.0.0-alpha.289" /> <PackageVersion Include="Servarr.FluentMigrator.Runner" Version="3.3.2.9" />
<PackageVersion Include="FluentMigrator.Runner" Version="4.0.0-alpha.289" /> <PackageVersion Include="Servarr.FluentMigrator.Runner.SQLite" Version="3.3.2.9" />
<PackageVersion Include="Servarr.FluentMigrator.Runner.Postgres" Version="3.3.2.9" />
<PackageVersion Include="FluentValidation" Version="8.6.2" /> <PackageVersion Include="FluentValidation" Version="8.6.2" />
<PackageVersion Include="Ical.Net" Version="4.2.0" /> <PackageVersion Include="Ical.Net" Version="4.2.0" />
<PackageVersion Include="ImpromptuInterface" Version="7.0.1" /> <PackageVersion Include="ImpromptuInterface" Version="7.0.1" />
<PackageVersion Include="LazyCache" Version="2.4.0" /> <PackageVersion Include="LazyCache" Version="2.4.0" />
<PackageVersion Include="Mailkit" Version="3.1.1" /> <PackageVersion Include="Mailkit" Version="3.3.0" />
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.3" /> <PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.7" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" /> <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="6.0.1" /> <PackageVersion Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.0" /> <PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="6.0.0" /> <PackageVersion Include="Microsoft.Extensions.Logging" Version="6.0.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.1.0" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
<PackageVersion Include="Microsoft.Win32.Registry" Version="5.0.0" /> <PackageVersion Include="Microsoft.Win32.Registry" Version="5.0.0" />
<PackageVersion Include="Mono.Posix.NETStandard" Version="5.20.1.34-servarr18" /> <PackageVersion Include="Mono.Posix.NETStandard" Version="5.20.1.34-servarr22" />
<PackageVersion Include="MonoTorrent" Version="2.0.4" />
<PackageVersion Include="Moq" Version="4.17.2" /> <PackageVersion Include="Moq" Version="4.17.2" />
<PackageVersion Include="MonoTorrent" Version="2.0.6" />
<PackageVersion Include="NBuilder" Version="6.1.0" /> <PackageVersion Include="NBuilder" Version="6.1.0" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.1" /> <PackageVersion Include="Newtonsoft.Json" Version="13.0.1" />
<PackageVersion Include="NLog.Extensions.Logging" Version="1.7.4" /> <PackageVersion Include="NLog.Extensions.Logging" Version="1.7.4" />
<PackageVersion Include="NLog" Version="4.7.14" /> <PackageVersion Include="NLog" Version="4.7.14" />
<PackageVersion Include="NLog.Targets.Syslog" Version="6.0.2" />
<PackageVersion Include="Npgsql" Version="6.0.3" />
<PackageVersion Include="NUnit3TestAdapter" Version="4.2.1" /> <PackageVersion Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageVersion Include="NUnit" Version="3.13.2" /> <PackageVersion Include="NUnit" Version="3.13.3" />
<PackageVersion Include="NunitXml.TestLogger" Version="3.0.117" /> <PackageVersion Include="NunitXml.TestLogger" Version="3.0.117" />
<PackageVersion Include="PdfSharpCore" Version="1.3.13" /> <PackageVersion Include="PdfSharpCore" Version="1.3.32" />
<PackageVersion Include="RestSharp.Serializers.SystemTextJson" Version="106.15.0" /> <PackageVersion Include="RestSharp.Serializers.SystemTextJson" Version="106.15.0" />
<PackageVersion Include="RestSharp" Version="106.15.0" /> <PackageVersion Include="RestSharp" Version="106.15.0" />
<PackageVersion Include="Selenium.Support" Version="3.141.0" /> <PackageVersion Include="Selenium.Support" Version="3.141.0" />
<PackageVersion Include="Selenium.WebDriver.ChromeDriver" Version="91.0.4472.10100" /> <PackageVersion Include="Selenium.WebDriver.ChromeDriver" Version="91.0.4472.10100" />
<PackageVersion Include="Sentry" Version="3.14.1" /> <PackageVersion Include="Sentry" Version="3.20.1" />
<PackageVersion Include="SharpZipLib" Version="1.3.3" /> <PackageVersion Include="SharpZipLib" Version="1.3.3" />
<PackageVersion Include="SixLabors.ImageSharp" Version="1.0.4" /> <PackageVersion Include="SixLabors.ImageSharp" Version="2.1.3" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.1.118" /> <PackageVersion Include="StyleCop.Analyzers" Version="1.1.118" />
<PackageVersion Include="System.Buffers" Version="4.5.1" /> <PackageVersion Include="System.Buffers" Version="4.5.1" />
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="6.0.0" /> <PackageVersion Include="System.Configuration.ConfigurationManager" Version="6.0.0" />
<PackageVersion Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" /> <PackageVersion Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="16.1.20" /> <PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="17.0.24" />
<PackageVersion Include="System.IO.Abstractions" Version="16.1.20" /> <PackageVersion Include="System.IO.Abstractions" Version="17.0.24" />
<PackageVersion Include="System.IO.FileSystem.AccessControl" Version="5.0.0" /> <PackageVersion Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
<PackageVersion Include="System.Memory" Version="4.5.4" /> <PackageVersion Include="System.Memory" Version="4.5.5" />
<PackageVersion Include="System.Reflection.TypeExtensions" Version="4.7.0" /> <PackageVersion Include="System.Reflection.TypeExtensions" Version="4.7.0" />
<PackageVersion Include="System.Resources.Extensions" Version="6.0.0" /> <PackageVersion Include="System.Resources.Extensions" Version="6.0.0" />
<PackageVersion Include="System.Runtime.Loader" Version="4.3.0" /> <PackageVersion Include="System.Runtime.Loader" Version="4.3.0" />
<PackageVersion Include="System.Security.Principal.Windows" Version="5.0.0" /> <PackageVersion Include="System.Security.Principal.Windows" Version="5.0.0" />
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="6.0.0" /> <PackageVersion Include="System.ServiceProcess.ServiceController" Version="6.0.0" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="6.0.0" /> <PackageVersion Include="System.Text.Encoding.CodePages" Version="6.0.0" />
<PackageVersion Include="System.Text.Json" Version="6.0.2" /> <PackageVersion Include="System.Text.Json" Version="6.0.5" />
<PackageVersion Include="System.ValueTuple" Version="4.5.0" /> <PackageVersion Include="System.ValueTuple" Version="4.5.0" />
<PackageVersion Include="TagLibSharp-Lidarr" Version="2.2.0.19" /> <PackageVersion Include="TagLibSharp-Lidarr" Version="2.2.0.19" />
<PackageVersion Include="Unity" Version="5.11.10" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+23 -2
View File
@@ -1,12 +1,33 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<configuration> <configuration>
<packageSources> <packageSources>
<clear /> <clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" /> <add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="FluentMigrator" value="https://pkgs.dev.azure.com/fluentmigrator/fluentmigrator/_packaging/fluentmigrator/nuget/v3/index.json" />
<add key="dotnet-bsd-crossbuild" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/dotnet-bsd-crossbuild/nuget/v3/index.json" /> <add key="dotnet-bsd-crossbuild" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/dotnet-bsd-crossbuild/nuget/v3/index.json" />
<add key="Mono.Posix.NETStandard" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/Mono.Posix.NETStandard/nuget/v3/index.json" /> <add key="Mono.Posix.NETStandard" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/Mono.Posix.NETStandard/nuget/v3/index.json" />
<add key="SQLite" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/SQLite/nuget/v3/index.json" /> <add key="SQLite" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/SQLite/nuget/v3/index.json" />
<add key="coverlet-nightly" value="https://pkgs.dev.azure.com/Servarr/coverlet/_packaging/coverlet-nightly/nuget/v3/index.json" /> <add key="coverlet-nightly" value="https://pkgs.dev.azure.com/Servarr/coverlet/_packaging/coverlet-nightly/nuget/v3/index.json" />
<add key="FluentMigrator" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/FluentMigrator/nuget/v3/index.json" />
</packageSources> </packageSources>
<packageSourceMapping>
<!-- key value for <packageSource> should match key values from <packageSources> element -->
<packageSource key="nuget.org">
<package pattern="*" />
</packageSource>
<packageSource key="dotnet-bsd-crossbuild">
<package pattern="*" />
</packageSource>
<packageSource key="Mono.Posix.NETStandard">
<package pattern="Mono.Posix.NETStandard" />
</packageSource>
<packageSource key="SQLite">
<package pattern="System.Data.SQLite.Core.Servarr" />
</packageSource>
<packageSource key="coverlet-nightly">
<package pattern="coverlet.*" />
</packageSource>
<packageSource key="FluentMigrator">
<package pattern="Servarr.FluentMigrator*"/>
</packageSource>
</packageSourceMapping>
</configuration> </configuration>
@@ -44,7 +44,7 @@ namespace NzbDrone.Automation.Test
driver.Manage().Window.Size = new System.Drawing.Size(1920, 1080); driver.Manage().Window.Size = new System.Drawing.Size(1920, 1080);
_runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger()); _runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger(), null);
_runner.KillAll(); _runner.KillAll();
_runner.Start(); _runner.Start();
@@ -1,5 +1,6 @@
using System; using System;
using System.IO; using System.IO;
using System.IO.Abstractions;
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
@@ -10,6 +11,12 @@ namespace NzbDrone.Common.Test.DiskTests
public abstract class DiskProviderFixtureBase<TSubject> : TestBase<TSubject> public abstract class DiskProviderFixtureBase<TSubject> : TestBase<TSubject>
where TSubject : class, IDiskProvider where TSubject : class, IDiskProvider
{ {
[SetUp]
public void BaseSetup()
{
Mocker.SetConstant<IFileSystem>(new FileSystem());
}
[Test] [Test]
public void writealltext_should_truncate_existing() public void writealltext_should_truncate_existing()
{ {
@@ -437,24 +437,6 @@ namespace NzbDrone.Common.Test.DiskTests
Assert.Throws<IOException>(() => Subject.TransferFolder(source.FullName, destination.FullName, TransferMode.Copy)); Assert.Throws<IOException>(() => Subject.TransferFolder(source.FullName, destination.FullName, TransferMode.Copy));
} }
[Test]
public void CopyFolder_should_not_copy_casesensitive_folder()
{
MonoOnly();
WithRealDiskProvider();
var original = GetFilledTempFolder();
var root = new DirectoryInfo(GetTempFilePath());
var source = new DirectoryInfo(root.FullName + "A/series");
var destination = new DirectoryInfo(root.FullName + "A/Series");
Subject.TransferFolder(original.FullName, source.FullName, TransferMode.Copy);
// Note: Although technically possible top copy to different case, we're not allowing it
Assert.Throws<IOException>(() => Subject.TransferFolder(source.FullName, destination.FullName, TransferMode.Copy));
}
[Test] [Test]
public void CopyFolder_should_ignore_nfs_temp_file() public void CopyFolder_should_ignore_nfs_temp_file()
{ {
@@ -540,26 +522,6 @@ namespace NzbDrone.Common.Test.DiskTests
source.FullName.GetActualCasing().Should().Be(destination.FullName); source.FullName.GetActualCasing().Should().Be(destination.FullName);
} }
[Test]
public void MoveFolder_should_rename_casesensitive_folder()
{
MonoOnly();
WithRealDiskProvider();
var original = GetFilledTempFolder();
var root = new DirectoryInfo(GetTempFilePath());
var source = new DirectoryInfo(root.FullName + "A/series");
var destination = new DirectoryInfo(root.FullName + "A/Series");
Subject.TransferFolder(original.FullName, source.FullName, TransferMode.Copy);
Subject.TransferFolder(source.FullName, destination.FullName, TransferMode.Move);
Directory.Exists(source.FullName).Should().Be(false);
Directory.Exists(destination.FullName).Should().Be(true);
}
[Test] [Test]
public void should_throw_if_destination_is_readonly() public void should_throw_if_destination_is_readonly()
{ {
@@ -1,5 +1,6 @@
using System; using System;
using System.IO; using System.IO;
using System.IO.Abstractions;
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
@@ -10,6 +11,12 @@ namespace NzbDrone.Common.Test.DiskTests
public abstract class FreeSpaceFixtureBase<TSubject> : TestBase<TSubject> public abstract class FreeSpaceFixtureBase<TSubject> : TestBase<TSubject>
where TSubject : class, IDiskProvider where TSubject : class, IDiskProvider
{ {
[SetUp]
public void BaseSetup()
{
Mocker.SetConstant<IFileSystem>(new FileSystem());
}
[Test] [Test]
public void should_get_free_space_for_folder() public void should_get_free_space_for_folder()
{ {
@@ -207,6 +207,7 @@ namespace NzbDrone.Common.Test.Http
} }
[Test] [Test]
[Platform(Exclude = "MacOsX", Reason = "Azure agent update prevents brotli on OSX")]
public void should_execute_get_using_brotli() public void should_execute_get_using_brotli()
{ {
var request = new HttpRequest($"https://{_httpBinHost}/brotli"); var request = new HttpRequest($"https://{_httpBinHost}/brotli");
@@ -307,11 +308,6 @@ namespace NzbDrone.Common.Test.Http
[Test] [Test]
public void should_follow_redirects_to_https() public void should_follow_redirects_to_https()
{ {
if (typeof(TDispatcher) == typeof(ManagedHttpDispatcher) && PlatformInfo.IsMono)
{
Assert.Ignore("Will fail on tls1.2 via managed dispatcher, ignore.");
}
var request = new HttpRequestBuilder($"https://{_httpBinHost}/redirect-to") var request = new HttpRequestBuilder($"https://{_httpBinHost}/redirect-to")
.AddQueryParam("url", $"https://readarr.com/") .AddQueryParam("url", $"https://readarr.com/")
.Build(); .Build();
@@ -1,4 +1,4 @@
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common.Instrumentation; using NzbDrone.Common.Instrumentation;
@@ -60,6 +60,8 @@ namespace NzbDrone.Common.Test.InstrumentationTests
[TestCase("Hardlink '/home/mySecret/Downloads/abs.mkv' to '/media/abc.mkv' failed.")] [TestCase("Hardlink '/home/mySecret/Downloads/abs.mkv' to '/media/abc.mkv' failed.")]
[TestCase("https://notifiarr.com/notifier.php: api=1234530f-422f-4aac-b6b3-01233210aaaa&radarr_health_issue_message=Download")] [TestCase("https://notifiarr.com/notifier.php: api=1234530f-422f-4aac-b6b3-01233210aaaa&radarr_health_issue_message=Download")]
[TestCase("/readarr/signalr/messages/negotiate?access_token=1234530f422f4aacb6b301233210aaaa&negotiateVersion=1")] [TestCase("/readarr/signalr/messages/negotiate?access_token=1234530f422f4aacb6b301233210aaaa&negotiateVersion=1")]
[TestCase(@"[Info] MigrationController: *** Migrating Database=readarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;Enlist=False ***")]
[TestCase(@"[Info] MigrationController: *** Migrating Database=readarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;token=mySecret;Enlist=False&username=mySecret;mypassword=mySecret;mypass=shouldkeep1;test_token=mySecret;password=123%@%_@!#^#@;use_password=mySecret;get_token=shouldkeep2;usetoken=shouldkeep3;passwrd=mySecret;")]
// Announce URLs (passkeys) Magnet & Tracker // Announce URLs (passkeys) Magnet & Tracker
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2f9pr04sg601233210imaveql2tyu8xyui%2fannounce""}")] [TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2f9pr04sg601233210imaveql2tyu8xyui%2fannounce""}")]
@@ -70,16 +72,41 @@ namespace NzbDrone.Common.Test.InstrumentationTests
[TestCase(@"tracker"":""https://xxx.yyy/tracker.php/9pr04sg601233210imaveql2tyu8xyui/announce""}")] [TestCase(@"tracker"":""https://xxx.yyy/tracker.php/9pr04sg601233210imaveql2tyu8xyui/announce""}")]
[TestCase(@"tracker"":""https://xxx.yyy/announce/9pr04sg601233210imaveql2tyu8xyui""}")] [TestCase(@"tracker"":""https://xxx.yyy/announce/9pr04sg601233210imaveql2tyu8xyui""}")]
[TestCase(@"tracker"":""https://xxx.yyy/announce.php?passkey=9pr04sg601233210imaveql2tyu8xyui""}")] [TestCase(@"tracker"":""https://xxx.yyy/announce.php?passkey=9pr04sg601233210imaveql2tyu8xyui""}")]
[TestCase(@"tracker"":""http://xxx.yyy/announce.php?passkey=9pr04sg601233210imaveql2tyu8xyui"",""info"":""http://xxx.yyy/info?a=b""")]
// Notifiarr
[TestCase(@"https://xxx.yyy/api/v1/notification/readarr/9pr04sg6-0123-3210-imav-eql2tyu8xyui")]
// Discord
[TestCase(@"https://discord.com/api/webhooks/mySecret")]
[TestCase(@"https://discord.com/api/webhooks/mySecret/01233210")]
public void should_clean_message(string message) public void should_clean_message(string message)
{ {
var cleansedMessage = CleanseLogMessage.Cleanse(message); var cleansedMessage = CleanseLogMessage.Cleanse(message);
cleansedMessage.Should().NotContain("mySecret"); cleansedMessage.Should().NotContain("mySecret");
cleansedMessage.Should().NotContain("123%@%_@!#^#@");
cleansedMessage.Should().NotContain("01233210"); cleansedMessage.Should().NotContain("01233210");
} }
[TestCase(@"[Info] MigrationController: *** Migrating Database=radarr-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;")]
public void should_keep_message(string message)
{
var cleansedMessage = CleanseLogMessage.Cleanse(message);
cleansedMessage.Should().NotContain("mySecret");
cleansedMessage.Should().NotContain("123%@%_@!#^#@");
cleansedMessage.Should().NotContain("01233210");
cleansedMessage.Should().Contain("shouldkeep1");
cleansedMessage.Should().Contain("shouldkeep2");
cleansedMessage.Should().Contain("shouldkeep3");
}
//GoodReads //GoodReads
[TestCase(@"{""signatureMethod"": ""hmacSha1"",""signatureTreatment"": ""escaped"",""type"": ""protectedResource"",""method"": ""GET"",""token"": ""mytoken"",""tokenSecret"": ""mytokensecret"",""requestUrl"": ""https://www.goodreads.com/review/list.xml"",""parameters"": { ""_nc"": ""1"", ""v"": ""2"", ""id"": ""999999999"", ""shelf"": ""currently-reading"", ""per_page"": ""200"", ""page"": ""1""}")] [TestCase(@"{""signatureMethod"": ""hmacSha1"",""signatureTreatment"": ""escaped"",""type"": ""protectedResource"",""method"": ""GET"",""token"": ""mytoken"",""tokenSecret"": ""mytokensecret"",""requestUrl"": ""https://www.goodreads.com/review/list.xml"",""parameters"": { ""_nc"": ""1"", ""v"": ""2"", ""id"": ""999999999"", ""shelf"": ""currently-reading"", ""per_page"": ""200"", ""page"": ""1""}")]
[TestCase(@"https://www.goodreads.com/series/311911?key=1234530f422f4aacb6b301233210aaaa&_nc=1&format=xml")]
public void should_cleanGoodRead_message(string message) public void should_cleanGoodRead_message(string message)
{ {
var cleansedMessage = CleanseLogMessage.Cleanse(message); var cleansedMessage = CleanseLogMessage.Cleanse(message);
@@ -279,7 +279,7 @@ namespace NzbDrone.Common.Test
[Test] [Test]
public void GetUpdateClientExePath() public void GetUpdateClientExePath()
{ {
GetIAppDirectoryInfo().GetUpdateClientExePath(PlatformType.DotNet).Should().BeEquivalentTo(@"C:\Temp\readarr_update\Readarr.Update.exe".AsOsAgnostic()); GetIAppDirectoryInfo().GetUpdateClientExePath().Should().BeEquivalentTo(@"C:\Temp\readarr_update\Readarr.Update".AsOsAgnostic().ProcessNameToExe());
} }
[Test] [Test]
@@ -1,4 +1,4 @@
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
@@ -171,7 +171,7 @@ namespace NzbDrone.Common.Test
var processStarted = new ManualResetEventSlim(); var processStarted = new ManualResetEventSlim();
string suffix; string suffix;
if (OsInfo.IsWindows || PlatformInfo.IsMono) if (OsInfo.IsWindows)
{ {
suffix = ".exe"; suffix = ".exe";
} }
@@ -4,11 +4,13 @@ using DryIoc.Microsoft.DependencyInjection;
using FluentAssertions; using FluentAssertions;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Moq; using Moq;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common.Composition.Extensions; using NzbDrone.Common.Composition.Extensions;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Instrumentation.Extensions; using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Datastore.Extensions; using NzbDrone.Core.Datastore.Extensions;
using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
@@ -29,7 +31,8 @@ namespace NzbDrone.Common.Test
.AddDummyDatabase() .AddDummyDatabase()
.AddStartupContext(new StartupContext("first", "second")); .AddStartupContext(new StartupContext("first", "second"));
container.RegisterInstance<IHostLifetime>(new Mock<IHostLifetime>().Object); container.RegisterInstance(new Mock<IHostLifetime>().Object);
container.RegisterInstance(new Mock<IOptions<PostgresOptions>>().Object);
var serviceProvider = container.GetServiceProvider(); var serviceProvider = container.GetServiceProvider();
serviceProvider.GetRequiredService<IAppFolderFactory>().Register(); serviceProvider.GetRequiredService<IAppFolderFactory>().Register();
@@ -1,17 +1,7 @@
using System; using System;
using System.Reflection;
using System.Text.RegularExpressions;
using Microsoft.Win32;
namespace NzbDrone.Common.EnvironmentInfo namespace NzbDrone.Common.EnvironmentInfo
{ {
public enum PlatformType
{
DotNet = 0,
Mono = 1,
NetCore = 2
}
public interface IPlatformInfo public interface IPlatformInfo
{ {
Version Version { get; } Version Version { get; }
@@ -19,38 +9,18 @@ namespace NzbDrone.Common.EnvironmentInfo
public class PlatformInfo : IPlatformInfo public class PlatformInfo : IPlatformInfo
{ {
private static readonly Regex MonoVersionRegex = new Regex(@"(?<=\W|^)(?<version>\d+\.\d+(\.\d+)?(\.\d+)?)(?=\W)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static PlatformType _platform;
private static Version _version; private static Version _version;
static PlatformInfo() static PlatformInfo()
{ {
_platform = PlatformType.NetCore;
_version = Environment.Version; _version = Environment.Version;
} }
public static PlatformType Platform => _platform;
public static bool IsMono => Platform == PlatformType.Mono;
public static bool IsDotNet => Platform == PlatformType.DotNet;
public static bool IsNetCore => Platform == PlatformType.NetCore;
public static string PlatformName public static string PlatformName
{ {
get get
{ {
if (IsDotNet) return ".NET";
{
return ".NET";
}
else if (IsMono)
{
return "Mono";
}
else
{
return ".NET Core";
}
} }
} }
@@ -60,107 +30,5 @@ namespace NzbDrone.Common.EnvironmentInfo
{ {
return _version; return _version;
} }
private static Version GetMonoVersion()
{
try
{
var type = Type.GetType("Mono.Runtime");
if (type != null)
{
var displayNameMethod = type.GetMethod("GetDisplayName", BindingFlags.NonPublic | BindingFlags.Static);
if (displayNameMethod != null)
{
var displayName = displayNameMethod.Invoke(null, null).ToString();
var versionMatch = MonoVersionRegex.Match(displayName);
if (versionMatch.Success)
{
return new Version(versionMatch.Groups["version"].Value);
}
}
}
}
catch (Exception ex)
{
Console.WriteLine("Couldnt get Mono version: " + ex.ToString());
}
return new Version();
}
private static Version GetDotNetVersion()
{
try
{
const string subkey = @"SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full\";
using (var ndpKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32).OpenSubKey(subkey))
{
if (ndpKey == null)
{
return new Version(4, 0);
}
var releaseKey = (int)ndpKey.GetValue("Release");
if (releaseKey >= 528040)
{
return new Version(4, 8, 0);
}
if (releaseKey >= 461808)
{
return new Version(4, 7, 2);
}
if (releaseKey >= 461308)
{
return new Version(4, 7, 1);
}
if (releaseKey >= 460798)
{
return new Version(4, 7);
}
if (releaseKey >= 394802)
{
return new Version(4, 6, 2);
}
if (releaseKey >= 394254)
{
return new Version(4, 6, 1);
}
if (releaseKey >= 393295)
{
return new Version(4, 6);
}
if (releaseKey >= 379893)
{
return new Version(4, 5, 2);
}
if (releaseKey >= 378675)
{
return new Version(4, 5, 1);
}
if (releaseKey >= 378389)
{
return new Version(4, 5);
}
}
}
catch (Exception ex)
{
Console.WriteLine("Couldnt get .NET framework version: " + ex.ToString());
}
return new Version(4, 0);
}
} }
} }
@@ -240,9 +240,9 @@ namespace NzbDrone.Common.Extensions
return null; return null;
} }
public static string ProcessNameToExe(this string processName, PlatformType runtime) public static string ProcessNameToExe(this string processName)
{ {
if (OsInfo.IsWindows || runtime != PlatformType.NetCore) if (OsInfo.IsWindows)
{ {
processName += ".exe"; processName += ".exe";
} }
@@ -250,11 +250,6 @@ namespace NzbDrone.Common.Extensions
return processName; return processName;
} }
public static string ProcessNameToExe(this string processName)
{
return processName.ProcessNameToExe(PlatformInfo.Platform);
}
public static string GetLongestCommonPath(this List<string> paths) public static string GetLongestCommonPath(this List<string> paths)
{ {
var firstPath = paths.First(); var firstPath = paths.First();
@@ -347,9 +342,9 @@ namespace NzbDrone.Common.Extensions
return Path.Combine(GetUpdatePackageFolder(appFolderInfo), UPDATE_CLIENT_FOLDER_NAME); return Path.Combine(GetUpdatePackageFolder(appFolderInfo), UPDATE_CLIENT_FOLDER_NAME);
} }
public static string GetUpdateClientExePath(this IAppFolderInfo appFolderInfo, PlatformType runtime) public static string GetUpdateClientExePath(this IAppFolderInfo appFolderInfo)
{ {
return Path.Combine(GetUpdateSandboxFolder(appFolderInfo), UPDATE_CLIENT_EXE_NAME).ProcessNameToExe(runtime); return Path.Combine(GetUpdateSandboxFolder(appFolderInfo), UPDATE_CLIENT_EXE_NAME).ProcessNameToExe();
} }
public static string GetDatabase(this IAppFolderInfo appFolderInfo) public static string GetDatabase(this IAppFolderInfo appFolderInfo)
+2 -1
View File
@@ -55,7 +55,8 @@ namespace NzbDrone.Common.Http
StatusCode == HttpStatusCode.Found || StatusCode == HttpStatusCode.Found ||
StatusCode == HttpStatusCode.TemporaryRedirect || StatusCode == HttpStatusCode.TemporaryRedirect ||
StatusCode == HttpStatusCode.RedirectMethod || StatusCode == HttpStatusCode.RedirectMethod ||
StatusCode == HttpStatusCode.SeeOther; StatusCode == HttpStatusCode.SeeOther ||
StatusCode == HttpStatusCode.PermanentRedirect;
public string[] GetCookieHeaders() public string[] GetCookieHeaders()
{ {
@@ -1,4 +1,4 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@@ -11,13 +11,14 @@ namespace NzbDrone.Common.Instrumentation
private static readonly Regex[] CleansingRules = new[] private static readonly Regex[] CleansingRules = new[]
{ {
// Url // Url
new Regex(@"(?<=\?|&|: )(apikey|(?:access[-_]?)?token|passkey|auth|authkey|user|uid|api|[a-z_]*apikey|account|passwd)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"(?<=\?|&|: )((?:api|auth|pass)?key|(?:access[-_]?)?token|auth|user|uid|api|[a-z_]*apikey|account|passwd)=(?<secret>[^&=""]+?)(?=[ ""&=]|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=\?|&)[^=]*?(username|password)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"(?<=\?|&)[^=]*?(username|password)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"torrentleech\.org/(?!rss)(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"torrentleech\.org/(?!rss)(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"torrentleech\.org/rss/download/[0-9]+/(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"torrentleech\.org/rss/download/[0-9]+/(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"iptorrents\.com/[/a-z0-9?&;]*?(?:[?&;](u|tp)=(?<secret>[^&=;]+?))+(?= |;|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"iptorrents\.com/[/a-z0-9?&;]*?(?:[?&;](u|tp)=(?<secret>[^&=;]+?))+(?= |;|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"/fetch/[a-z0-9]{32}/(?<secret>[a-z0-9]{32})", RegexOptions.Compiled), new Regex(@"/fetch/[a-z0-9]{32}/(?<secret>[a-z0-9]{32})", RegexOptions.Compiled),
new Regex(@"getnzb.*?(?<=\?|&)(r)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"getnzb.*?(?<=\?|&)(r)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"\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 // Trackers Announce Keys; Designed for Qbit Json; should work for all in theory
new Regex(@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?<secret>[a-z0-9]{16,})|(?<secret>[a-z0-9]{16,})(/|%2f)announce"), new Regex(@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?<secret>[a-z0-9]{16,})|(?<secret>[a-z0-9]{16,})(/|%2f)announce"),
@@ -46,7 +47,14 @@ namespace NzbDrone.Common.Instrumentation
new Regex(@"(?<=\?|&)(authkey|torrent_pass)=(?<secret>[^&=]+?)(?=""|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"(?<=\?|&)(authkey|torrent_pass)=(?<secret>[^&=]+?)(?=""|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Good Reads // Good Reads
new Regex(@"(?<=""(token|tokensecret)"":\s)""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase) new Regex(@"(?<=""(token|tokensecret)"":\s)""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Webhooks
// Notifiarr
new Regex(@"api/v[0-9]/notification/readarr/(?<secret>[\w-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Discord
new Regex(@"discord.com/api/webhooks/((?<secret>[\w-]+)/)?(?<secret>[\w-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase)
}; };
private static readonly Regex CleanseRemoteIPRegex = new Regex(@"(?:Auth-\w+(?<!Failure|Unauthorized) ip|from) (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})", RegexOptions.Compiled); private static readonly Regex CleanseRemoteIPRegex = new Regex(@"(?:Auth-\w+(?<!Failure|Unauthorized) ip|from) (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})", RegexOptions.Compiled);
@@ -38,16 +38,6 @@ namespace NzbDrone.Common.Instrumentation
return; return;
} }
if (PlatformInfo.IsMono)
{
if ((exception is TypeInitializationException && exception.InnerException is DllNotFoundException) ||
exception is DllNotFoundException)
{
Logger.Debug(exception, "Minor Fail: " + exception.Message);
return;
}
}
Console.WriteLine("EPIC FAIL: {0}", exception); Console.WriteLine("EPIC FAIL: {0}", exception);
Logger.Fatal(exception, "EPIC FAIL."); Logger.Fatal(exception, "EPIC FAIL.");
} }
@@ -109,13 +109,6 @@ namespace NzbDrone.Common.Instrumentation.Sentry
o.Debug = false; o.Debug = false;
o.DiagnosticLevel = SentryLevel.Debug; o.DiagnosticLevel = SentryLevel.Debug;
o.Release = BuildInfo.Release; o.Release = BuildInfo.Release;
if (PlatformInfo.IsMono)
{
// Mono 6.0 broke GzipStream.WriteAsync
// TODO: Check specific version
o.RequestBodyCompressionLevel = System.IO.Compression.CompressionLevel.NoCompression;
}
o.BeforeSend = x => SentryCleanser.CleanseEvent(x); o.BeforeSend = x => SentryCleanser.CleanseEvent(x);
o.BeforeBreadcrumb = x => SentryCleanser.CleanseBreadcrumb(x); o.BeforeBreadcrumb = x => SentryCleanser.CleanseBreadcrumb(x);
o.Environment = BuildInfo.Branch; o.Environment = BuildInfo.Branch;
@@ -158,14 +151,6 @@ namespace NzbDrone.Common.Instrumentation.Sentry
SentrySdk.ConfigureScope(scope => SentrySdk.ConfigureScope(scope =>
{ {
scope.SetTag("is_docker", $"{osInfo.IsDocker}"); scope.SetTag("is_docker", $"{osInfo.IsDocker}");
if (osInfo.Name != null && PlatformInfo.IsMono)
{
// Sentry auto-detection of non-Windows platforms isn't that accurate on certain devices.
scope.Contexts.OperatingSystem.Name = osInfo.Name.FirstCharToUpper();
scope.Contexts.OperatingSystem.RawDescription = osInfo.FullName;
scope.Contexts.OperatingSystem.Version = osInfo.Version.ToString();
}
}); });
} }
@@ -127,7 +127,18 @@ namespace NzbDrone.Common.Processes
try try
{ {
_logger.Trace("Setting environment variable '{0}' to '{1}'", environmentVariable.Key, environmentVariable.Value); _logger.Trace("Setting environment variable '{0}' to '{1}'", environmentVariable.Key, environmentVariable.Value);
startInfo.EnvironmentVariables.Add(environmentVariable.Key.ToString(), environmentVariable.Value.ToString());
var key = environmentVariable.Key.ToString();
var value = environmentVariable.Value?.ToString();
if (startInfo.EnvironmentVariables.ContainsKey(key))
{
startInfo.EnvironmentVariables[key] = value;
}
else
{
startInfo.EnvironmentVariables.Add(key, value);
}
} }
catch (Exception e) catch (Exception e)
{ {
@@ -366,11 +377,6 @@ namespace NzbDrone.Common.Processes
private (string Path, string Args) GetPathAndArgs(string path, string args) private (string Path, string Args) GetPathAndArgs(string path, string args)
{ {
if (PlatformInfo.IsMono && path.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase))
{
return ("mono", $"--debug {path} {args}");
}
if (OsInfo.IsWindows && path.EndsWith(".bat", StringComparison.InvariantCultureIgnoreCase)) if (OsInfo.IsWindows && path.EndsWith(".bat", StringComparison.InvariantCultureIgnoreCase))
{ {
return ("cmd.exe", $"/c {path} {args}"); return ("cmd.exe", $"/c {path} {args}");
@@ -15,7 +15,7 @@ namespace NzbDrone.Core.Test.Datastore
public void SingleOrDefault_should_return_null_on_empty_db() public void SingleOrDefault_should_return_null_on_empty_db()
{ {
Mocker.Resolve<IDatabase>() Mocker.Resolve<IDatabase>()
.OpenConnection().Query<Author>("SELECT * FROM Authors") .OpenConnection().Query<Author>("SELECT * FROM \"Authors\"")
.SingleOrDefault(c => c.CleanName == "SomeTitle") .SingleOrDefault(c => c.CleanName == "SomeTitle")
.Should() .Should()
.BeNull(); .BeNull();
@@ -78,7 +78,7 @@ namespace NzbDrone.Core.Test.Datastore
public void should_lazy_load_author_for_trackfile() public void should_lazy_load_author_for_trackfile()
{ {
var db = Mocker.Resolve<IDatabase>(); var db = Mocker.Resolve<IDatabase>();
var tracks = db.Query<BookFile>(new SqlBuilder()).ToList(); var tracks = db.Query<BookFile>(new SqlBuilder(db.DatabaseType)).ToList();
Assert.IsNotEmpty(tracks); Assert.IsNotEmpty(tracks);
foreach (var track in tracks) foreach (var track in tracks)
@@ -94,7 +94,7 @@ namespace NzbDrone.Core.Test.Datastore
public void should_lazy_load_trackfile_if_not_joined() public void should_lazy_load_trackfile_if_not_joined()
{ {
var db = Mocker.Resolve<IDatabase>(); var db = Mocker.Resolve<IDatabase>();
var tracks = db.Query<Book>(new SqlBuilder()).ToList(); var tracks = db.Query<Book>(new SqlBuilder(db.DatabaseType)).ToList();
foreach (var track in tracks) foreach (var track in tracks)
{ {
@@ -109,7 +109,7 @@ namespace NzbDrone.Core.Test.Datastore
{ {
var db = Mocker.Resolve<IDatabase>(); var db = Mocker.Resolve<IDatabase>();
var files = MediaFileRepository.Query(db, var files = MediaFileRepository.Query(db,
new SqlBuilder() new SqlBuilder(db.DatabaseType)
.Join<BookFile, Edition>((t, a) => t.EditionId == a.Id) .Join<BookFile, Edition>((t, a) => t.EditionId == a.Id)
.Join<Edition, Book>((e, b) => e.BookId == b.Id) .Join<Edition, Book>((e, b) => e.BookId == b.Id)
.Join<Book, Author>((book, author) => book.AuthorMetadataId == author.AuthorMetadataId) .Join<Book, Author>((book, author) => book.AuthorMetadataId == author.AuthorMetadataId)
@@ -0,0 +1,211 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Books;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Datastore
{
[TestFixture]
public class WhereBuilderPostgresFixture : CoreTest
{
private WhereBuilderPostgres _subject;
[OneTimeSetUp]
public void MapTables()
{
// Generate table mapping
Mocker.Resolve<DbFactory>();
}
private WhereBuilderPostgres Where(Expression<Func<Author, bool>> filter)
{
return new WhereBuilderPostgres(filter, true, 0);
}
private WhereBuilderPostgres WhereMetadata(Expression<Func<AuthorMetadata, bool>> filter)
{
return new WhereBuilderPostgres(filter, true, 0);
}
[Test]
public void postgres_where_equal_const()
{
_subject = Where(x => x.Id == 10);
_subject.ToString().Should().Be($"(\"Authors\".\"Id\" = @Clause1_P1)");
_subject.Parameters.Get<int>("Clause1_P1").Should().Be(10);
}
[Test]
public void postgres_where_equal_variable()
{
var id = 10;
_subject = Where(x => x.Id == id);
_subject.ToString().Should().Be($"(\"Authors\".\"Id\" = @Clause1_P1)");
_subject.Parameters.Get<int>("Clause1_P1").Should().Be(id);
}
[Test]
public void postgres_where_equal_property()
{
var author = new Author { Id = 10 };
_subject = Where(x => x.Id == author.Id);
_subject.Parameters.ParameterNames.Should().HaveCount(1);
_subject.ToString().Should().Be($"(\"Authors\".\"Id\" = @Clause1_P1)");
_subject.Parameters.Get<int>("Clause1_P1").Should().Be(author.Id);
}
[Test]
public void postgres_where_equal_joined_property()
{
_subject = Where(x => x.QualityProfile.Value.Id == 1);
_subject.Parameters.ParameterNames.Should().HaveCount(1);
_subject.ToString().Should().Be($"(\"QualityProfiles\".\"Id\" = @Clause1_P1)");
_subject.Parameters.Get<int>("Clause1_P1").Should().Be(1);
}
[Test]
public void postgres_where_throws_without_concrete_condition_if_requiresConcreteCondition()
{
Expression<Func<Author, Author, bool>> filter = (x, y) => x.Id == y.Id;
_subject = new WhereBuilderPostgres(filter, true, 0);
Assert.Throws<InvalidOperationException>(() => _subject.ToString());
}
[Test]
public void postgres_where_allows_abstract_condition_if_not_requiresConcreteCondition()
{
Expression<Func<Author, Author, bool>> filter = (x, y) => x.Id == y.Id;
_subject = new WhereBuilderPostgres(filter, false, 0);
_subject.ToString().Should().Be($"(\"Authors\".\"Id\" = \"Authors\".\"Id\")");
}
[Test]
public void postgres_where_string_is_null()
{
_subject = Where(x => x.CleanName == null);
_subject.ToString().Should().Be($"(\"Authors\".\"CleanName\" IS NULL)");
}
[Test]
public void postgres_where_string_is_null_value()
{
string cleanName = null;
_subject = Where(x => x.CleanName == cleanName);
_subject.ToString().Should().Be($"(\"Authors\".\"CleanName\" IS NULL)");
}
[Test]
public void postgres_where_equal_null_property()
{
var author = new Author { CleanName = null };
_subject = Where(x => x.CleanName == author.CleanName);
_subject.ToString().Should().Be($"(\"Authors\".\"CleanName\" IS NULL)");
}
[Test]
public void postgres_where_column_contains_string()
{
var test = "small";
_subject = Where(x => x.CleanName.Contains(test));
_subject.ToString().Should().Be($"(\"Authors\".\"CleanName\" ILIKE '%' || @Clause1_P1 || '%')");
_subject.Parameters.Get<string>("Clause1_P1").Should().Be(test);
}
[Test]
public void postgres_where_string_contains_column()
{
var test = "small";
_subject = Where(x => test.Contains(x.CleanName));
_subject.ToString().Should().Be($"(@Clause1_P1 ILIKE '%' || \"Authors\".\"CleanName\" || '%')");
_subject.Parameters.Get<string>("Clause1_P1").Should().Be(test);
}
[Test]
public void postgres_where_column_starts_with_string()
{
var test = "small";
_subject = Where(x => x.CleanName.StartsWith(test));
_subject.ToString().Should().Be($"(\"Authors\".\"CleanName\" ILIKE @Clause1_P1 || '%')");
_subject.Parameters.Get<string>("Clause1_P1").Should().Be(test);
}
[Test]
public void postgres_where_column_ends_with_string()
{
var test = "small";
_subject = Where(x => x.CleanName.EndsWith(test));
_subject.ToString().Should().Be($"(\"Authors\".\"CleanName\" ILIKE '%' || @Clause1_P1)");
_subject.Parameters.Get<string>("Clause1_P1").Should().Be(test);
}
[Test]
public void postgres_where_in_list()
{
var list = new List<int> { 1, 2, 3 };
_subject = Where(x => list.Contains(x.Id));
_subject.ToString().Should().Be($"(\"Authors\".\"Id\" = ANY (('{{1, 2, 3}}')))");
}
[Test]
public void postgres_where_in_list_2()
{
var list = new List<int> { 1, 2, 3 };
_subject = Where(x => x.CleanName == "test" && list.Contains(x.Id));
_subject.ToString().Should().Be($"((\"Authors\".\"CleanName\" = @Clause1_P1) AND (\"Authors\".\"Id\" = ANY (('{{1, 2, 3}}'))))");
}
[Test]
public void postgres_where_in_string_list()
{
var list = new List<string> { "first", "second", "third" };
_subject = Where(x => list.Contains(x.CleanName));
_subject.ToString().Should().Be($"(\"Authors\".\"CleanName\" = ANY (@Clause1_P1))");
}
[Test]
public void enum_as_int()
{
_subject = WhereMetadata(x => x.Status == AuthorStatusType.Continuing);
_subject.ToString().Should().Be($"(\"AuthorMetadata\".\"Status\" = @Clause1_P1)");
}
[Test]
public void enum_in_list()
{
var allowed = new List<AuthorStatusType> { AuthorStatusType.Continuing, AuthorStatusType.Ended };
_subject = WhereMetadata(x => allowed.Contains(x.Status));
_subject.ToString().Should().Be($"(\"AuthorMetadata\".\"Status\" = ANY (@Clause1_P1))");
}
[Test]
public void enum_in_array()
{
var allowed = new AuthorStatusType[] { AuthorStatusType.Continuing, AuthorStatusType.Ended };
_subject = WhereMetadata(x => allowed.Contains(x.Status));
_subject.ToString().Should().Be($"(\"AuthorMetadata\".\"Status\" = ANY (@Clause1_P1))");
}
}
}
@@ -11,9 +11,9 @@ using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Datastore namespace NzbDrone.Core.Test.Datastore
{ {
[TestFixture] [TestFixture]
public class WhereBuilderFixture : CoreTest public class WhereBuilderSqliteFixture : CoreTest
{ {
private WhereBuilder _subject; private WhereBuilderSqlite _subject;
[OneTimeSetUp] [OneTimeSetUp]
public void MapTables() public void MapTables()
@@ -22,14 +22,14 @@ namespace NzbDrone.Core.Test.Datastore
Mocker.Resolve<DbFactory>(); Mocker.Resolve<DbFactory>();
} }
private WhereBuilder Where(Expression<Func<Author, bool>> filter) private WhereBuilderSqlite Where(Expression<Func<Author, bool>> filter)
{ {
return new WhereBuilder(filter, true, 0); return new WhereBuilderSqlite(filter, true, 0);
} }
private WhereBuilder WhereMetadata(Expression<Func<AuthorMetadata, bool>> filter) private WhereBuilderSqlite WhereMetadata(Expression<Func<AuthorMetadata, bool>> filter)
{ {
return new WhereBuilder(filter, true, 0); return new WhereBuilderSqlite(filter, true, 0);
} }
[Test] [Test]
@@ -76,7 +76,7 @@ namespace NzbDrone.Core.Test.Datastore
public void where_throws_without_concrete_condition_if_requiresConcreteCondition() public void where_throws_without_concrete_condition_if_requiresConcreteCondition()
{ {
Expression<Func<Author, Author, bool>> filter = (x, y) => x.Id == y.Id; Expression<Func<Author, Author, bool>> filter = (x, y) => x.Id == y.Id;
_subject = new WhereBuilder(filter, true, 0); _subject = new WhereBuilderSqlite(filter, true, 0);
Assert.Throws<InvalidOperationException>(() => _subject.ToString()); Assert.Throws<InvalidOperationException>(() => _subject.ToString());
} }
@@ -84,7 +84,7 @@ namespace NzbDrone.Core.Test.Datastore
public void where_allows_abstract_condition_if_not_requiresConcreteCondition() public void where_allows_abstract_condition_if_not_requiresConcreteCondition()
{ {
Expression<Func<Author, Author, bool>> filter = (x, y) => x.Id == y.Id; Expression<Func<Author, Author, bool>> filter = (x, y) => x.Id == y.Id;
_subject = new WhereBuilder(filter, false, 0); _subject = new WhereBuilderSqlite(filter, false, 0);
_subject.ToString().Should().Be($"(\"Authors\".\"Id\" = \"Authors\".\"Id\")"); _subject.ToString().Should().Be($"(\"Authors\".\"Id\" = \"Authors\".\"Id\")");
} }
+82 -3
View File
@@ -1,12 +1,18 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data.SQLite; using System.Data.SQLite;
using System.IO;
using System.Linq; using System.Linq;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Npgsql;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
using NzbDrone.Core.Datastore.Migration.Framework; using NzbDrone.Core.Datastore.Migration.Framework;
using NzbDrone.Test.Common.Datastore;
namespace NzbDrone.Core.Test.Framework namespace NzbDrone.Core.Test.Framework
{ {
@@ -47,6 +53,7 @@ namespace NzbDrone.Core.Test.Framework
public abstract class DbTest : CoreTest public abstract class DbTest : CoreTest
{ {
private ITestDatabase _db; private ITestDatabase _db;
private DatabaseType _databaseType;
protected virtual MigrationType MigrationType => MigrationType.Main; protected virtual MigrationType MigrationType => MigrationType.Main;
@@ -65,8 +72,7 @@ namespace NzbDrone.Core.Test.Framework
protected virtual ITestDatabase WithTestDb(MigrationContext migrationContext) protected virtual ITestDatabase WithTestDb(MigrationContext migrationContext)
{ {
var factory = Mocker.Resolve<DbFactory>(); var database = CreateDatabase(migrationContext);
var database = factory.Create(migrationContext);
Mocker.SetConstant(database); Mocker.SetConstant(database);
switch (MigrationType) switch (MigrationType)
@@ -98,6 +104,65 @@ namespace NzbDrone.Core.Test.Framework
return testDb; return testDb;
} }
private IDatabase CreateDatabase(MigrationContext migrationContext)
{
if (_databaseType == DatabaseType.PostgreSQL)
{
CreatePostgresDb();
}
var factory = Mocker.Resolve<DbFactory>();
// If a special migration test or log migration then create new
if (migrationContext.BeforeMigration != null || _databaseType == DatabaseType.PostgreSQL)
{
return factory.Create(migrationContext);
}
return CreateSqliteDatabase(factory, migrationContext);
}
private void CreatePostgresDb()
{
var options = Mocker.Resolve<IOptions<PostgresOptions>>().Value;
PostgresDatabase.Create(options, MigrationType);
}
private void DropPostgresDb()
{
var options = Mocker.Resolve<IOptions<PostgresOptions>>().Value;
PostgresDatabase.Drop(options, MigrationType);
}
private IDatabase CreateSqliteDatabase(IDbFactory factory, MigrationContext migrationContext)
{
// Otherwise try to use a cached migrated db
var cachedDb = SqliteDatabase.GetCachedDb(migrationContext.MigrationType);
var testDb = GetTestSqliteDb(migrationContext.MigrationType);
if (File.Exists(cachedDb))
{
TestLogger.Info($"Using cached initial database {cachedDb}");
File.Copy(cachedDb, testDb);
return factory.Create(migrationContext);
}
else
{
var db = factory.Create(migrationContext);
GC.Collect();
GC.WaitForPendingFinalizers();
SQLiteConnection.ClearAllPools();
TestLogger.Info("Caching database");
File.Copy(testDb, cachedDb);
return db;
}
}
private string GetTestSqliteDb(MigrationType type)
{
return type == MigrationType.Main ? TestFolderInfo.GetDatabase() : TestFolderInfo.GetLogDatabase();
}
protected virtual void SetupLogging() protected virtual void SetupLogging()
{ {
Mocker.SetConstant<ILoggerProvider>(NullLoggerProvider.Instance); Mocker.SetConstant<ILoggerProvider>(NullLoggerProvider.Instance);
@@ -108,6 +173,13 @@ namespace NzbDrone.Core.Test.Framework
WithTempAsAppPath(); WithTempAsAppPath();
SetupLogging(); SetupLogging();
// populate the possible postgres options
var postgresOptions = PostgresDatabase.GetTestOptions();
_databaseType = postgresOptions.Host.IsNotNullOrWhiteSpace() ? DatabaseType.PostgreSQL : DatabaseType.SQLite;
// Set up remaining container services
Mocker.SetConstant(Options.Create(postgresOptions));
Mocker.SetConstant<IConfigFileProvider>(Mocker.Resolve<ConfigFileProvider>());
Mocker.SetConstant<IConnectionStringFactory>(Mocker.Resolve<ConnectionStringFactory>()); Mocker.SetConstant<IConnectionStringFactory>(Mocker.Resolve<ConnectionStringFactory>());
Mocker.SetConstant<IMigrationController>(Mocker.Resolve<MigrationController>()); Mocker.SetConstant<IMigrationController>(Mocker.Resolve<MigrationController>());
@@ -127,12 +199,19 @@ namespace NzbDrone.Core.Test.Framework
// Make sure there are no lingering connections. (When this happens it means we haven't disposed something properly) // Make sure there are no lingering connections. (When this happens it means we haven't disposed something properly)
GC.Collect(); GC.Collect();
GC.WaitForPendingFinalizers(); GC.WaitForPendingFinalizers();
SQLiteConnection.ClearAllPools(); SQLiteConnection.ClearAllPools();
NpgsqlConnection.ClearAllPools();
if (TestFolderInfo != null) if (TestFolderInfo != null)
{ {
DeleteTempFolder(TestFolderInfo.AppDataFolder); DeleteTempFolder(TestFolderInfo.AppDataFolder);
} }
if (_databaseType == DatabaseType.PostgreSQL)
{
DropPostgresDb();
}
} }
} }
} }
@@ -1,7 +1,8 @@
using System.IO.Abstractions.TestingHelpers; using System.IO.Abstractions;
using System.IO.Abstractions.TestingHelpers;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using Unity.Resolution; using NzbDrone.Test.Common.AutoMoq;
namespace NzbDrone.Core.Test.Framework namespace NzbDrone.Core.Test.Framework
{ {
@@ -14,12 +15,9 @@ namespace NzbDrone.Core.Test.Framework
[SetUp] [SetUp]
public void FileSystemTestSetup() public void FileSystemTestSetup()
{ {
FileSystem = new MockFileSystem(); FileSystem = (MockFileSystem)Mocker.Resolve<IFileSystem>(FileSystemType.Mock);
DiskProvider = Mocker.Resolve<IDiskProvider>("ActualDiskProvider", new ResolverOverride[] DiskProvider = Mocker.Resolve<IDiskProvider>(FileSystemType.Mock);
{
new ParameterOverride("fileSystem", FileSystem)
});
} }
} }
} }
@@ -23,6 +23,7 @@ namespace NzbDrone.Core.Test.Framework
where T : ModelBase, new(); where T : ModelBase, new();
IDirectDataMapper GetDirectDataMapper(); IDirectDataMapper GetDirectDataMapper();
IDbConnection OpenConnection(); IDbConnection OpenConnection();
DatabaseType DatabaseType { get; }
} }
public class TestDatabase : ITestDatabase public class TestDatabase : ITestDatabase
@@ -30,6 +31,8 @@ namespace NzbDrone.Core.Test.Framework
private readonly IDatabase _dbConnection; private readonly IDatabase _dbConnection;
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
public DatabaseType DatabaseType => _dbConnection.DatabaseType;
public TestDatabase(IDatabase dbConnection) public TestDatabase(IDatabase dbConnection)
{ {
_eventAggregator = new Mock<IEventAggregator>().Object; _eventAggregator = new Mock<IEventAggregator>().Object;
@@ -25,7 +25,7 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
.Setup(s => s.GetAuthor(It.IsAny<int>())) .Setup(s => s.GetAuthor(It.IsAny<int>()))
.Returns(_author); .Returns(_author);
Mocker.GetMock<ISearchForNzb>() Mocker.GetMock<ISearchForReleases>()
.Setup(s => s.AuthorSearch(_author.Id, false, true, false)) .Setup(s => s.AuthorSearch(_author.Id, false, true, false))
.Returns(new List<DownloadDecision>()); .Returns(new List<DownloadDecision>());
@@ -45,7 +45,7 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
Subject.Execute(new AuthorSearchCommand { AuthorId = _author.Id, Trigger = CommandTrigger.Manual }); Subject.Execute(new AuthorSearchCommand { AuthorId = _author.Id, Trigger = CommandTrigger.Manual });
Mocker.GetMock<ISearchForNzb>() Mocker.GetMock<ISearchForReleases>()
.Verify(v => v.AuthorSearch(_author.Id, false, true, false), .Verify(v => v.AuthorSearch(_author.Id, false, true, false),
Times.Exactly(_author.Books.Value.Count(s => s.Monitored))); Times.Exactly(_author.Books.Value.Count(s => s.Monitored)));
} }
@@ -16,11 +16,6 @@ namespace NzbDrone.Core.Test.MediaCoverTests
[SetUp] [SetUp]
public void SetUp() public void SetUp()
{ {
if (PlatformInfo.IsMono && PlatformInfo.GetVersion() < new Version(5, 8))
{
Assert.Inconclusive("Not supported on Mono < 5.8");
}
Mocker.GetMock<IDiskProvider>() Mocker.GetMock<IDiskProvider>()
.Setup(v => v.FileExists(It.IsAny<string>())) .Setup(v => v.FileExists(It.IsAny<string>()))
.Returns<string>(s => File.Exists(s)); .Returns<string>(s => File.Exists(s));
@@ -1,5 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.IO.Abstractions;
using System.Linq; using System.Linq;
using FizzWare.NBuilder; using FizzWare.NBuilder;
using FluentAssertions; using FluentAssertions;
@@ -133,6 +135,13 @@ namespace NzbDrone.Core.Test.MediaCoverTests
} }
}; };
var path = Path.Combine(TestContext.CurrentContext.TestDirectory, "Files", "Media", "NonExistant.mp4");
var fileInfo = new FileInfo(path);
Mocker.GetMock<IDiskProvider>()
.Setup(c => c.GetFileInfo(It.IsAny<string>()))
.Returns((FileInfoBase)fileInfo);
Subject.ConvertToLocalUrls(12, MediaCoverEntity.Author, covers); Subject.ConvertToLocalUrls(12, MediaCoverEntity.Author, covers);
covers.Single().Url.Should().Be("/MediaCover/12/banner" + extension); covers.Single().Url.Should().Be("/MediaCover/12/banner" + extension);
@@ -13,6 +13,7 @@ using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common; using NzbDrone.Test.Common;
using NzbDrone.Test.Common.AutoMoq;
namespace NzbDrone.Core.Test.MediaFiles.AudioTagServiceFixture namespace NzbDrone.Core.Test.MediaFiles.AudioTagServiceFixture
{ {
@@ -55,9 +56,7 @@ namespace NzbDrone.Core.Test.MediaFiles.AudioTagServiceFixture
[SetUp] [SetUp]
public void Setup() public void Setup()
{ {
_diskProvider = Mocker.Resolve<IDiskProvider>("ActualDiskProvider"); _diskProvider = Mocker.Resolve<IDiskProvider>(FileSystemType.Actual);
Mocker.SetConstant<IDiskProvider>(_diskProvider);
Mocker.GetMock<IConfigService>() Mocker.GetMock<IConfigService>()
.Setup(x => x.WriteAudioTags) .Setup(x => x.WriteAudioTags)
@@ -39,7 +39,7 @@ namespace NzbDrone.Core.Test.MetadataSource.Goodreads
} }
[TestCase("Harry Potter and the sorcerer's stone", 3)] [TestCase("Harry Potter and the sorcerer's stone", 3)]
[TestCase("B0192CTMYG", 42844155)] [TestCase("B0192CTMYG", 61209488)]
[TestCase("9780439554930", 48517161)] [TestCase("9780439554930", 48517161)]
public void successful_book_search(string title, int expected) public void successful_book_search(string title, int expected)
{ {
@@ -3,8 +3,10 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using FizzWare.NBuilder; using FizzWare.NBuilder;
using FluentAssertions; using FluentAssertions;
using FluentAssertions.Equivalency;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Core.Books; using NzbDrone.Core.Books;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.MusicTests.BookRepositoryTests namespace NzbDrone.Core.Test.MusicTests.BookRepositoryTests
@@ -21,6 +23,13 @@ namespace NzbDrone.Core.Test.MusicTests.BookRepositoryTests
[SetUp] [SetUp]
public void Setup() public void Setup()
{ {
AssertionOptions.AssertEquivalencyUsing(options =>
{
options.Using<DateTime>(ctx => ctx.Subject.Should().BeCloseTo(ctx.Expectation.ToUniversalTime())).WhenTypeIs<DateTime>();
options.Using<DateTime?>(ctx => ctx.Subject.Should().BeCloseTo(ctx.Expectation.Value.ToUniversalTime())).WhenTypeIs<DateTime?>();
return options;
});
_author = new Author _author = new Author
{ {
Name = "Alien Ant Farm", Name = "Alien Ant Farm",
@@ -143,7 +152,7 @@ namespace NzbDrone.Core.Test.MusicTests.BookRepositoryTests
GivenMultipleBooks(); GivenMultipleBooks();
var result = _bookRepo.GetNextBooks(new[] { _author.AuthorMetadataId }); var result = _bookRepo.GetNextBooks(new[] { _author.AuthorMetadataId });
result.Should().BeEquivalentTo(_books.Take(1)); result.Should().BeEquivalentTo(_books.Take(1), BookComparerOptions);
} }
[Test] [Test]
@@ -152,7 +161,11 @@ namespace NzbDrone.Core.Test.MusicTests.BookRepositoryTests
GivenMultipleBooks(); GivenMultipleBooks();
var result = _bookRepo.GetLastBooks(new[] { _author.AuthorMetadataId }); var result = _bookRepo.GetLastBooks(new[] { _author.AuthorMetadataId });
result.Should().BeEquivalentTo(_books.Skip(2).Take(1)); result.Should().BeEquivalentTo(_books.Skip(2).Take(1), BookComparerOptions);
} }
private EquivalencyAssertionOptions<Book> BookComparerOptions(EquivalencyAssertionOptions<Book> opts) => opts.ComparingByMembers<Book>()
.Excluding(ctx => ctx.SelectedMemberInfo.MemberType.IsGenericType && ctx.SelectedMemberInfo.MemberType.GetGenericTypeDefinition() == typeof(LazyLoaded<>))
.Excluding(x => x.AuthorId);
} }
} }
@@ -3,9 +3,11 @@ using System.Collections.Generic;
using System.Data.SQLite; using System.Data.SQLite;
using FizzWare.NBuilder; using FizzWare.NBuilder;
using FluentAssertions; using FluentAssertions;
using Npgsql;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Books; using NzbDrone.Core.Books;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Profiles.Metadata; using NzbDrone.Core.Profiles.Metadata;
using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Profiles.Qualities;
using NzbDrone.Core.Qualities; using NzbDrone.Core.Qualities;
@@ -145,7 +147,14 @@ namespace NzbDrone.Core.Test.MusicTests.AuthorRepositoryTests
_authorRepo.Insert(author1); _authorRepo.Insert(author1);
Action insertDupe = () => _authorRepo.Insert(author2); Action insertDupe = () => _authorRepo.Insert(author2);
insertDupe.Should().Throw<SQLiteException>(); if (Db.DatabaseType == DatabaseType.PostgreSQL)
{
insertDupe.Should().Throw<PostgresException>();
}
else
{
insertDupe.Should().Throw<SQLiteException>();
}
} }
} }
} }
@@ -106,7 +106,7 @@ namespace NzbDrone.Core.Test.MusicTests
private void GivenBooksForRefresh(List<Book> books) private void GivenBooksForRefresh(List<Book> books)
{ {
Mocker.GetMock<IBookService>(MockBehavior.Strict) Mocker.GetMock<IBookService>(MockBehavior.Strict)
.Setup(s => s.GetBooksForRefresh(It.IsAny<int>(), It.IsAny<IEnumerable<string>>())) .Setup(s => s.GetBooksForRefresh(It.IsAny<int>(), It.IsAny<List<string>>()))
.Returns(books); .Returns(books);
} }
@@ -238,7 +238,7 @@ namespace NzbDrone.Core.Test.MusicTests
Mocker.GetMock<IBookService>(MockBehavior.Strict) Mocker.GetMock<IBookService>(MockBehavior.Strict)
.InSequence(seq) .InSequence(seq)
.Setup(x => x.GetBooksForRefresh(It.IsAny<int>(), It.IsAny<IEnumerable<string>>())) .Setup(x => x.GetBooksForRefresh(It.IsAny<int>(), It.IsAny<List<string>>()))
.Returns(new List<Book>()); .Returns(new List<Book>());
// Update called twice for a move/merge // Update called twice for a move/merge
@@ -298,7 +298,7 @@ namespace NzbDrone.Core.Test.MusicTests
Mocker.GetMock<IBookService>(MockBehavior.Strict) Mocker.GetMock<IBookService>(MockBehavior.Strict)
.InSequence(seq) .InSequence(seq)
.Setup(x => x.GetBooksForRefresh(clash.AuthorMetadataId, It.IsAny<IEnumerable<string>>())) .Setup(x => x.GetBooksForRefresh(clash.AuthorMetadataId, It.IsAny<List<string>>()))
.Returns(_books); .Returns(_books);
// Update called twice for a move/merge // Update called twice for a move/merge
@@ -7,7 +7,6 @@ using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.ParserTests namespace NzbDrone.Core.Test.ParserTests
{ {
[TestFixture] [TestFixture]
public class QualityParserFixture : CoreTest public class QualityParserFixture : CoreTest
{ {
public static object[] SelfQualityParserCases = public static object[] SelfQualityParserCases =
@@ -27,7 +27,6 @@ namespace NzbDrone.Core.Test.UpdateTests
[Test] [Test]
public void finds_update_when_version_lower() public void finds_update_when_version_lower()
{ {
NotBsd();
UseRealHttp(); UseRealHttp();
Subject.GetLatestUpdate("nightly", new Version(0, 1)).Should().NotBeNull(); Subject.GetLatestUpdate("nightly", new Version(0, 1)).Should().NotBeNull();
} }
@@ -43,8 +42,6 @@ namespace NzbDrone.Core.Test.UpdateTests
[Test] [Test]
public void should_get_recent_updates() public void should_get_recent_updates()
{ {
NotBsd();
const string branch = "nightly"; const string branch = "nightly";
UseRealHttp(); UseRealHttp();
var recent = Subject.GetRecentUpdates(branch, new Version(0, 1)); var recent = Subject.GetRecentUpdates(branch, new Version(0, 1));
@@ -69,7 +69,7 @@ namespace NzbDrone.Core.Test.UpdateTests
.Returns(true); .Returns(true);
Mocker.GetMock<IDiskProvider>() Mocker.GetMock<IDiskProvider>()
.Setup(v => v.FileExists(It.Is<string>(s => s.EndsWith("Readarr.Update.exe")))) .Setup(v => v.FileExists(It.Is<string>(s => s.EndsWith("Readarr.Update".ProcessNameToExe()))))
.Returns(true); .Returns(true);
_sandboxFolder = Mocker.GetMock<IAppFolderInfo>().Object.GetUpdateSandboxFolder(); _sandboxFolder = Mocker.GetMock<IAppFolderInfo>().Object.GetUpdateSandboxFolder();
@@ -165,7 +165,7 @@ namespace NzbDrone.Core.Test.UpdateTests
public void should_return_with_warning_if_updater_doesnt_exists() public void should_return_with_warning_if_updater_doesnt_exists()
{ {
Mocker.GetMock<IDiskProvider>() Mocker.GetMock<IDiskProvider>()
.Setup(v => v.FileExists(It.Is<string>(s => s.EndsWith("Readarr.Update.exe")))) .Setup(v => v.FileExists(It.Is<string>(s => s.EndsWith("Readarr.Update".ProcessNameToExe()))))
.Returns(false); .Returns(false);
Subject.Execute(new ApplicationUpdateCommand()); Subject.Execute(new ApplicationUpdateCommand());
@@ -16,7 +16,7 @@ namespace NzbDrone.Core.AuthorStats
public class AuthorStatisticsRepository : IAuthorStatisticsRepository public class AuthorStatisticsRepository : IAuthorStatisticsRepository
{ {
private const string _selectTemplate = "SELECT /**select**/ FROM Editions /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/ /**orderby**/"; private const string _selectTemplate = "SELECT /**select**/ FROM \"Editions\" /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/ /**orderby**/";
private readonly IMainDatabase _database; private readonly IMainDatabase _database;
@@ -45,14 +45,14 @@ namespace NzbDrone.Core.AuthorStats
} }
} }
private SqlBuilder Builder() => new SqlBuilder() private SqlBuilder Builder() => new SqlBuilder(_database.DatabaseType)
.Select(@"Authors.Id AS AuthorId, .Select(@"""Authors"".""Id"" AS ""AuthorId"",
Books.Id AS BookId, ""Books"".""Id"" AS ""BookId"",
SUM(COALESCE(BookFiles.Size, 0)) AS SizeOnDisk, SUM(COALESCE(""BookFiles"".""Size"", 0)) AS ""SizeOnDisk"",
1 AS TotalBookCount, 1 AS ""TotalBookCount"",
CASE WHEN BookFiles.Id IS NULL THEN 0 ELSE 1 END AS AvailableBookCount, CASE WHEN MIN(""BookFiles"".""Id"") IS NULL THEN 0 ELSE 1 END AS ""AvailableBookCount"",
CASE WHEN (Books.Monitored = 1 AND (Books.ReleaseDate < @currentDate) OR Books.ReleaseDate IS NULL) OR BookFiles.Id IS NOT NULL THEN 1 ELSE 0 END AS BookCount, CASE WHEN (""Books"".""Monitored"" = true AND (""Books"".""ReleaseDate"" < @currentDate) OR ""Books"".""ReleaseDate"" IS NULL) OR MIN(""BookFiles"".""Id"") IS NOT NULL THEN 1 ELSE 0 END AS ""BookCount"",
CASE WHEN BookFiles.Id IS NULL THEN 0 ELSE COUNT(BookFiles.Id) END AS BookFileCount") CASE WHEN MIN(""BookFiles"".""Id"") IS NULL THEN 0 ELSE COUNT(""BookFiles"".""Id"") END AS ""BookFileCount""")
.Join<Edition, Book>((e, b) => e.BookId == b.Id) .Join<Edition, Book>((e, b) => e.BookId == b.Id)
.Join<Book, Author>((book, author) => book.AuthorMetadataId == author.AuthorMetadataId) .Join<Book, Author>((book, author) => book.AuthorMetadataId == author.AuthorMetadataId)
.LeftJoin<Edition, BookFile>((t, f) => t.Id == f.EditionId) .LeftJoin<Edition, BookFile>((t, f) => t.Id == f.EditionId)
@@ -15,12 +15,14 @@ namespace NzbDrone.Core.AuthorStats
} }
public class AuthorStatisticsService : IAuthorStatisticsService, public class AuthorStatisticsService : IAuthorStatisticsService,
IHandle<AuthorAddedEvent>,
IHandle<AuthorUpdatedEvent>, IHandle<AuthorUpdatedEvent>,
IHandle<AuthorDeletedEvent>, IHandle<AuthorDeletedEvent>,
IHandle<BookAddedEvent>, IHandle<BookAddedEvent>,
IHandle<BookDeletedEvent>, IHandle<BookDeletedEvent>,
IHandle<BookImportedEvent>, IHandle<BookImportedEvent>,
IHandle<BookEditedEvent>, IHandle<BookEditedEvent>,
IHandle<BookUpdatedEvent>,
IHandle<BookFileDeletedEvent> IHandle<BookFileDeletedEvent>
{ {
private readonly IAuthorStatisticsRepository _authorStatisticsRepository; private readonly IAuthorStatisticsRepository _authorStatisticsRepository;
@@ -68,6 +70,13 @@ namespace NzbDrone.Core.AuthorStats
return authorStatistics; return authorStatistics;
} }
[EventHandleOrder(EventHandleOrder.First)]
public void Handle(AuthorAddedEvent message)
{
_cache.Remove("AllAuthors");
_cache.Remove(message.Author.Id.ToString());
}
[EventHandleOrder(EventHandleOrder.First)] [EventHandleOrder(EventHandleOrder.First)]
public void Handle(AuthorUpdatedEvent message) public void Handle(AuthorUpdatedEvent message)
{ {
@@ -110,6 +119,13 @@ namespace NzbDrone.Core.AuthorStats
_cache.Remove(message.Book.AuthorId.ToString()); _cache.Remove(message.Book.AuthorId.ToString());
} }
[EventHandleOrder(EventHandleOrder.First)]
public void Handle(BookUpdatedEvent message)
{
_cache.Remove("AllAuthors");
_cache.Remove(message.Book.AuthorId.ToString());
}
[EventHandleOrder(EventHandleOrder.First)] [EventHandleOrder(EventHandleOrder.First)]
public void Handle(BookFileDeletedEvent message) public void Handle(BookFileDeletedEvent message)
{ {
+5 -2
View File
@@ -183,9 +183,12 @@ namespace NzbDrone.Core.Backup
private void BackupDatabase() private void BackupDatabase()
{ {
_logger.ProgressDebug("Backing up database"); if (_maindDb.DatabaseType == DatabaseType.SQLite)
{
_logger.ProgressDebug("Backing up database");
_makeDatabaseBackup.BackupDatabase(_maindDb, _backupTempFolder); _makeDatabaseBackup.BackupDatabase(_maindDb, _backupTempFolder);
}
} }
private void BackupConfigFile() private void BackupConfigFile()
@@ -34,7 +34,7 @@ namespace NzbDrone.Core.Blocklisting
return Query(b => b.AuthorId == authorId); return Query(b => b.AuthorId == authorId);
} }
protected override SqlBuilder PagedBuilder() => new SqlBuilder() protected override SqlBuilder PagedBuilder() => new SqlBuilder(_database.DatabaseType)
.Join<Blocklist, Author>((b, m) => b.AuthorId == m.Id) .Join<Blocklist, Author>((b, m) => b.AuthorId == m.Id)
.Join<Author, AuthorMetadata>((l, r) => l.AuthorMetadataId == r.Id); .Join<Author, AuthorMetadata>((l, r) => l.AuthorMetadataId == r.Id);
protected override IEnumerable<Blocklist> PagedQuery(SqlBuilder builder) => _database.QueryJoined<Blocklist, Author, AuthorMetadata>(builder, protected override IEnumerable<Blocklist> PagedQuery(SqlBuilder builder) => _database.QueryJoined<Blocklist, Author, AuthorMetadata>(builder,
@@ -25,7 +25,7 @@ namespace NzbDrone.Core.Books
{ {
} }
protected override SqlBuilder Builder() => new SqlBuilder() protected override SqlBuilder Builder() => new SqlBuilder(_database.DatabaseType)
.Join<Author, AuthorMetadata>((a, m) => a.AuthorMetadataId == m.Id); .Join<Author, AuthorMetadata>((a, m) => a.AuthorMetadataId == m.Id);
protected override List<Author> Query(SqlBuilder builder) => Query(_database, builder).ToList(); protected override List<Author> Query(SqlBuilder builder) => Query(_database, builder).ToList();
@@ -60,7 +60,7 @@ namespace NzbDrone.Core.Books
{ {
using (var conn = _database.OpenConnection()) using (var conn = _database.OpenConnection())
{ {
var strSql = "SELECT Id AS [Key], Path AS [Value] FROM Authors"; var strSql = "SELECT \"Id\" AS \"Key\", \"Path\" AS \"Value\" FROM \"Authors\"";
return conn.Query<KeyValuePair<int, string>>(strSql).ToDictionary(x => x.Key, x => x.Value); return conn.Query<KeyValuePair<int, string>>(strSql).ToDictionary(x => x.Key, x => x.Value);
} }
} }
@@ -15,7 +15,7 @@ namespace NzbDrone.Core.Books
List<Book> GetLastBooks(IEnumerable<int> authorMetadataIds); List<Book> GetLastBooks(IEnumerable<int> authorMetadataIds);
List<Book> GetNextBooks(IEnumerable<int> authorMetadataIds); List<Book> GetNextBooks(IEnumerable<int> authorMetadataIds);
List<Book> GetBooksByAuthorMetadataId(int authorMetadataId); List<Book> GetBooksByAuthorMetadataId(int authorMetadataId);
List<Book> GetBooksForRefresh(int authorMetadataId, IEnumerable<string> foreignIds); List<Book> GetBooksForRefresh(int authorMetadataId, List<string> foreignIds);
List<Book> GetBooksByFileIds(IEnumerable<int> fileIds); List<Book> GetBooksByFileIds(IEnumerable<int> fileIds);
Book FindByTitle(int authorMetadataId, string title); Book FindByTitle(int authorMetadataId, string title);
Book FindById(string foreignBookId); Book FindById(string foreignBookId);
@@ -44,17 +44,35 @@ namespace NzbDrone.Core.Books
public List<Book> GetLastBooks(IEnumerable<int> authorMetadataIds) public List<Book> GetLastBooks(IEnumerable<int> authorMetadataIds)
{ {
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
return Query(Builder().Where<Book>(x => authorMetadataIds.Contains(x.AuthorMetadataId) && x.ReleaseDate < now)
.GroupBy<Book>(x => x.AuthorMetadataId) var inner = Builder()
.Having("Books.ReleaseDate = MAX(Books.ReleaseDate)")); .Select("MIN(\"Books\".\"Id\") as id, MAX(\"Books\".\"ReleaseDate\") as date")
.Where<Book>(x => authorMetadataIds.Contains(x.AuthorMetadataId) && x.ReleaseDate < now)
.GroupBy<Book>(x => x.AuthorMetadataId)
.AddSelectTemplate(typeof(Book));
var outer = Builder()
.Join($"({inner.RawSql}) ids on ids.id = \"Books\".\"Id\" and ids.date = \"Books\".\"ReleaseDate\"")
.AddParameters(inner.Parameters);
return Query(outer);
} }
public List<Book> GetNextBooks(IEnumerable<int> authorMetadataIds) public List<Book> GetNextBooks(IEnumerable<int> authorMetadataIds)
{ {
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
return Query(Builder().Where<Book>(x => authorMetadataIds.Contains(x.AuthorMetadataId) && x.ReleaseDate > now)
.GroupBy<Book>(x => x.AuthorMetadataId) var inner = Builder()
.Having("Books.ReleaseDate = MIN(Books.ReleaseDate)")); .Select("MIN(\"Books\".\"Id\") as id, MIN(\"Books\".\"ReleaseDate\") as date")
.Where<Book>(x => authorMetadataIds.Contains(x.AuthorMetadataId) && x.ReleaseDate > now)
.GroupBy<Book>(x => x.AuthorMetadataId)
.AddSelectTemplate(typeof(Book));
var outer = Builder()
.Join($"({inner.RawSql}) ids on ids.id = \"Books\".\"Id\" and ids.date = \"Books\".\"ReleaseDate\"")
.AddParameters(inner.Parameters);
return Query(outer);
} }
public List<Book> GetBooksByAuthorMetadataId(int authorMetadataId) public List<Book> GetBooksByAuthorMetadataId(int authorMetadataId)
@@ -62,14 +80,14 @@ namespace NzbDrone.Core.Books
return Query(s => s.AuthorMetadataId == authorMetadataId); return Query(s => s.AuthorMetadataId == authorMetadataId);
} }
public List<Book> GetBooksForRefresh(int authorMetadataId, IEnumerable<string> foreignIds) public List<Book> GetBooksForRefresh(int authorMetadataId, List<string> foreignIds)
{ {
return Query(a => a.AuthorMetadataId == authorMetadataId || foreignIds.Contains(a.ForeignBookId)); return Query(a => a.AuthorMetadataId == authorMetadataId || foreignIds.Contains(a.ForeignBookId));
} }
public List<Book> GetBooksByFileIds(IEnumerable<int> fileIds) public List<Book> GetBooksByFileIds(IEnumerable<int> fileIds)
{ {
return Query(new SqlBuilder() return Query(new SqlBuilder(_database.DatabaseType)
.Join<Book, Edition>((b, e) => b.Id == e.BookId) .Join<Book, Edition>((b, e) => b.Id == e.BookId)
.Join<Edition, BookFile>((l, r) => l.Id == r.EditionId) .Join<Edition, BookFile>((l, r) => l.Id == r.EditionId)
.Where<BookFile>(f => fileIds.Contains(f.Id))) .Where<BookFile>(f => fileIds.Contains(f.Id)))
@@ -125,7 +143,7 @@ namespace NzbDrone.Core.Books
{ {
foreach (var belowCutoff in profile.QualityIds) foreach (var belowCutoff in profile.QualityIds)
{ {
clauses.Add(string.Format("(Authors.[QualityProfileId] = {0} AND BookFiles.Quality LIKE '%_quality_: {1},%')", profile.ProfileId, belowCutoff)); clauses.Add(string.Format("(\"Authors\".\"QualityProfileId\" = {0} AND \"BookFiles\".\"Quality\" LIKE '%_quality_: {1},%')", profile.ProfileId, belowCutoff));
} }
} }
@@ -136,7 +154,7 @@ namespace NzbDrone.Core.Books
{ {
pagingSpec.Records = GetPagedRecords(BooksWhereCutoffUnmetBuilder(qualitiesBelowCutoff), pagingSpec, PagedQuery); pagingSpec.Records = GetPagedRecords(BooksWhereCutoffUnmetBuilder(qualitiesBelowCutoff), pagingSpec, PagedQuery);
var countTemplate = $"SELECT COUNT(*) FROM (SELECT /**select**/ FROM {TableMapping.Mapper.TableNameMapping(typeof(Book))} /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/)"; var countTemplate = $"SELECT COUNT(*) FROM (SELECT /**select**/ FROM \"{TableMapping.Mapper.TableNameMapping(typeof(Book))}\" /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/) AS \"Inner\"";
pagingSpec.TotalRecords = GetPagedRecordCount(BooksWhereCutoffUnmetBuilder(qualitiesBelowCutoff).Select(typeof(Book)), pagingSpec, countTemplate); pagingSpec.TotalRecords = GetPagedRecordCount(BooksWhereCutoffUnmetBuilder(qualitiesBelowCutoff).Select(typeof(Book)), pagingSpec, countTemplate);
return pagingSpec; return pagingSpec;
@@ -11,11 +11,11 @@ namespace NzbDrone.Core.Books
{ {
List<Edition> GetAllMonitoredEditions(); List<Edition> GetAllMonitoredEditions();
Edition FindByForeignEditionId(string foreignEditionId); Edition FindByForeignEditionId(string foreignEditionId);
List<Edition> FindByBook(int id); List<Edition> FindByBook(IEnumerable<int> ids);
List<Edition> FindByAuthor(int id); List<Edition> FindByAuthor(int id);
List<Edition> FindByAuthorMetadataId(int id, bool onlyMonitored); List<Edition> FindByAuthorMetadataId(int id, bool onlyMonitored);
Edition FindByTitle(int authorMetadataId, string title); Edition FindByTitle(int authorMetadataId, string title);
List<Edition> GetEditionsForRefresh(int bookId, IEnumerable<string> foreignEditionIds); List<Edition> GetEditionsForRefresh(int bookId, List<string> foreignEditionIds);
List<Edition> SetMonitored(Edition edition); List<Edition> SetMonitored(Edition edition);
} }
@@ -38,19 +38,19 @@ namespace NzbDrone.Core.Books
return edition; return edition;
} }
public List<Edition> GetEditionsForRefresh(int bookId, IEnumerable<string> foreignEditionIds) public List<Edition> GetEditionsForRefresh(int bookId, List<string> foreignEditionIds)
{ {
return Query(r => r.BookId == bookId || foreignEditionIds.Contains(r.ForeignEditionId)); return Query(r => r.BookId == bookId || foreignEditionIds.Contains(r.ForeignEditionId));
} }
public List<Edition> FindByBook(int id) public List<Edition> FindByBook(IEnumerable<int> ids)
{ {
// populate the books and author metadata also // populate the books and author metadata also
// this hopefully speeds up the track matching a lot // this hopefully speeds up the track matching a lot
var builder = new SqlBuilder() var builder = new SqlBuilder(_database.DatabaseType)
.LeftJoin<Edition, Book>((e, b) => e.BookId == b.Id) .LeftJoin<Edition, Book>((e, b) => e.BookId == b.Id)
.LeftJoin<Book, AuthorMetadata>((b, a) => b.AuthorMetadataId == a.Id) .LeftJoin<Book, AuthorMetadata>((b, a) => b.AuthorMetadataId == a.Id)
.Where<Edition>(r => r.BookId == id); .Where<Edition>(r => ids.Contains(r.BookId));
return _database.QueryJoined<Edition, Book, AuthorMetadata>(builder, (edition, book, metadata) => return _database.QueryJoined<Edition, Book, AuthorMetadata>(builder, (edition, book, metadata) =>
{ {
@@ -96,7 +96,7 @@ namespace NzbDrone.Core.Books
public List<Edition> SetMonitored(Edition edition) public List<Edition> SetMonitored(Edition edition)
{ {
var allEditions = FindByBook(edition.BookId); var allEditions = FindByBook(new[] { edition.BookId });
allEditions.ForEach(r => r.Monitored = r.Id == edition.Id); allEditions.ForEach(r => r.Monitored = r.Id == edition.Id);
Ensure.That(allEditions.Count(x => x.Monitored) == 1).IsTrue(); Ensure.That(allEditions.Count(x => x.Monitored) == 1).IsTrue();
UpdateMany(allEditions); UpdateMany(allEditions);
@@ -8,7 +8,7 @@ namespace NzbDrone.Core.Books
public interface ISeriesRepository : IBasicRepository<Series> public interface ISeriesRepository : IBasicRepository<Series>
{ {
Series FindById(string foreignSeriesId); Series FindById(string foreignSeriesId);
List<Series> FindById(IEnumerable<string> foreignSeriesId); List<Series> FindById(List<string> foreignSeriesId);
List<Series> GetByAuthorMetadataId(int authorMetadataId); List<Series> GetByAuthorMetadataId(int authorMetadataId);
List<Series> GetByAuthorId(int authorId); List<Series> GetByAuthorId(int authorId);
} }
@@ -25,7 +25,7 @@ namespace NzbDrone.Core.Books
return Query(x => x.ForeignSeriesId == foreignSeriesId).SingleOrDefault(); return Query(x => x.ForeignSeriesId == foreignSeriesId).SingleOrDefault();
} }
public List<Series> FindById(IEnumerable<string> foreignSeriesId) public List<Series> FindById(List<string> foreignSeriesId)
{ {
return Query(x => foreignSeriesId.Contains(x.ForeignSeriesId)); return Query(x => foreignSeriesId.Contains(x.ForeignSeriesId));
} }

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