1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-04-13 20:44:52 -04:00

Compare commits

..

103 Commits

Author SHA1 Message Date
Weblate
ada33dc065 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Michael5564445 <michaelvelosk@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2024-04-06 16:34:41 +03:00
Till Krüss
badb68b817 Improve text for file deleted through UI/API (#9882) 2024-04-06 16:32:25 +03:00
Mark McDowall
3bd1b3e972 Fixed: Sending ntfy.sh notifications with unicode characters
(cherry picked from commit a169ebff2adda5c8585c6aae6249b1c1f7c12264)
2024-04-06 16:27:30 +03:00
Stevie Robinson
6851de42a7 New: Informational text on Custom Formats modal
(cherry picked from commit 238ba85f0a2639608d9890292dfe0b96c0212f10)
2024-04-06 16:27:19 +03:00
Cuki
dd0b7c91f9 Fixed: Use widely supported display mode for PWA
(cherry picked from commit 1562d3bae3002947f9e428321d2b162ad69c3309)
2024-04-06 16:27:08 +03:00
Mark McDowall
45ac69e2d9 Fixed: Cleanse BHD RSS key in log files
(cherry picked from commit 60ee7cc716d344fc904fa6fb28f7be0386ae710d)
2024-04-06 16:26:49 +03:00
Bogdan
9ccf0ecdb1 Fix translation token for Include Health Warnings 2024-04-06 03:31:36 +03:00
Servarr
48a3467572 Automated API Docs update 2024-03-29 14:53:19 +02:00
Mark McDowall
d0a10379f9 Fixed: Use custom formats from import during rename
(cherry picked from commit d338425951af50a710c6c4411a72f05d14737ddd)

Closes #9867
2024-03-28 12:42:21 +02:00
Bogdan
caab5e3614 Add missing import after 4e4769 2024-03-28 12:38:28 +02:00
Alex Cortelyou
4e47695f89 New: Add additional fields to Webhook Manual Interaction Required events
(cherry picked from commit 1ec1ce58e9f095222e7fe4a8c74a0720fed71558)

Closes #9874
2024-03-28 11:15:56 +02:00
Bogdan
83bd4d0686 New: Advanced settings toggle in import list, notification and download client modals
(cherry picked from commit 13c925b3418d1d48ec041e3d97ab51aaf2b8977a)

Closes #9869
2024-03-28 11:14:26 +02:00
Mark McDowall
a75619c8ef Fixed: Task with removed movie causing error
(cherry picked from commit fc6494c569324c839debdb1d08dde23b8f1b8d76)

Closes #9866
2024-03-28 11:08:32 +02:00
Louis R
28689006fb Fixed: Exceptions when checking for routable IPv4 addresses
(cherry picked from commit 060b789bc6f10f667795697eb536d4bd3851da49)
2024-03-28 10:29:30 +02:00
Carlos Gustavo Sarmiento
43b0589bea Fixed: qBittorrent not correctly handling retention during testing
(cherry picked from commit 588372fd950fc85f5e9a4275fbcb423b247ed0ee)
2024-03-28 10:29:17 +02:00
Stevie Robinson
c4aad5800c Fixed: Handling torrents with relative path in rTorrent
(cherry picked from commit 35d0e6a6f806c68756450a7d199600d7fb49d6c5)
2024-03-28 10:28:53 +02:00
Bogdan
0c998dac5c New: Allow HEAD requests to ping endpoint
(cherry picked from commit 7353fe479dbb8d0dab76993ebed92d48e1b05524)
2024-03-28 10:28:41 +02:00
Bogdan
d41c0f0ab7 Fixed: Movie search label on overflow views
Fixed #9865
2024-03-27 15:16:56 +02:00
Weblate
85b13b7e41 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Altair <villagermd@outlook.com>
Co-authored-by: Casselluu <jack10193@163.com>
Co-authored-by: Dani Talens <databio@gmail.com>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Jason54 <jason54700.jg@gmail.com>
Co-authored-by: Stanislav <prekop3@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: shimmyx <shimmygodx@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2024-03-27 13:16:17 +02:00
Bogdan
2a545a84b4 Bump version to 5.4.5 2024-03-24 17:45:46 +02:00
Mark McDowall
280083f4d7 Fixed: Task progress messages in the UI
(cherry picked from commit c6417337812f3578a27f9dc1e44fdad80f557271)

Closes #9855
2024-03-22 11:15:16 +02:00
Mark McDowall
d6dcae3d6a Fixed: Plex Watchlist import list
(cherry picked from commit 88de9274358d7005fa9c677bb8c86f046a2a23a9)
2024-03-22 11:06:39 +02:00
Bogdan
ebde4d3bc8 New: Critic Rating for Kodi/Emby metadata 2024-03-21 19:22:51 +02:00
Yurii
1ee30290ef Use branded message title for Telegram nitifications 2024-03-17 13:20:57 +00:00
Bogdan
d303eae7c6 New: Company filters for TMDb Popular List 2024-03-17 13:50:41 +02:00
Bogdan
584910514a Bump version to 5.4.4 2024-03-17 13:50:03 +02:00
Weblate
a253181d7d Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dennis Langthjem <dennis@langthjem.dk>
Co-authored-by: DimitriDR <dimitridroeck@gmail.com>
Co-authored-by: Gianmarco Novelli <rinogaetano94@live.it>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Ihor Mudryi <mudryy33@gmail.com>
Co-authored-by: MadaxDeLuXe <madaxdeluxe@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: infoaitek24 <info@aitekph.com>
Co-authored-by: reloxx <reloxx@interia.pl>
Co-authored-by: vfaergestad <vgf@hotmail.no>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/uk/
Translation: Servarr/Radarr
2024-03-16 18:15:57 +02:00
Bogdan
7ea6918327 Remove leftover QueuedTasks.js 2024-03-14 15:40:13 +02:00
Bogdan
953d3ad3fb Ensure not allowed cursor is shown for disabled select inputs 2024-03-14 14:31:09 +02:00
Mark McDowall
b9f4073514 Fixed: Disabled select option still selectable
(cherry picked from commit 063dba22a803295adee4fdcbe42718af3e85ca78)

Closes #9838
2024-03-14 13:20:33 +02:00
Stevie Robinson
86a17e7984 Fixed: Wrapping of naming tokens with alternate separators
(cherry picked from commit 80630bf97f5bb3b49d4824dc039d2edfc74e4797)

Closes #9743
Closes #9836
2024-03-14 11:33:54 +02:00
Bogdan
f38545f852 Ensure movies are populated in PageConnector 2024-03-14 11:21:56 +02:00
Mark McDowall
a7720e829d New: Show movie titles after task name when applicable
(cherry picked from commit 6d552f2a60f44052079b5e8944f5e1bbabac56e0)

Closes #9837
2024-03-14 11:15:39 +02:00
Mark McDowall
3a4eac4d59 Fixed: Release push with only Magnet URL
(cherry picked from commit 9f705e4161af3f4dd55b399d56b0b9c5a36e181b)
2024-03-14 07:12:12 +02:00
Bogdan
04f792c55a Fixed: Map covers to local for Movie Editor 2024-03-12 22:48:27 +02:00
Stevie Robinson
ada326e4dd Update release profile download client warning
(cherry picked from commit 2ec071a5ecab8f5056d179feaaef0147abb944ca)

Closes #9828
2024-03-10 20:16:09 +02:00
Bogdan
cae58d620b New: Collection Refresh Complete Event to trigger root folder check for collections 2024-03-10 20:13:35 +02:00
Bogdan
e84df18e8d Bump ImageSharp, Polly 2024-03-10 13:06:26 +02:00
Bogdan
a51ae70938 Bump version to 5.4.3 2024-03-10 09:08:55 +02:00
Weblate
7cc04245ec Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Jason54 <jason54700.jg@gmail.com>
Co-authored-by: Mark Martines <mark-martines@hotmail.com>
Co-authored-by: Maxence Winandy <maxence.winandy@gmail.com>
Co-authored-by: Stevie Robinson <stevie.robinson@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: linkin931 <931linkin@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2024-03-08 11:08:04 +02:00
Mark McDowall
2caf3c6725 Fixed: Error sending Manual Interaction Required notification
(cherry picked from commit a12cdb34bc0ab78937e3c3677012bf030923aebf)
2024-03-08 08:52:48 +02:00
Bogdan
41ff9352b9 Prevent NullRef in naming when truncating a null Release Group
(cherry picked from commit 13e29bd257ccfccb09e66c940ffabeb6503c05b5)
2024-03-08 08:52:48 +02:00
Helvio Pedreschi
d7b9b2ccb2 Fixed: WebApp functionality on Apple devices
(cherry picked from commit c7dd7abf892eead7796fcc482aa2f2aabaf88712)
2024-03-08 08:52:48 +02:00
Mark McDowall
e90a50a3aa Fixed: Overly aggressive exception release group parsing
(cherry picked from commit 0183812cc58dad0e555125ddd8b33a85cbdecbf2)
2024-03-08 08:52:48 +02:00
Bogdan
a0dd26c353 Configurable URL Base setting for Kodi connections 2024-03-04 03:16:23 +02:00
Bogdan
2286055d6a Fixed: URL Base setting for Kodi connections 2024-03-03 13:48:25 +02:00
Mark McDowall
0a5a4e0a6f New: URL Base setting for Media Server connections
(cherry picked from commit 9fd193d2a82d5c2cdc0f36c1f984e4b6b68aaa8d)
2024-03-03 12:29:47 +02:00
Mark McDowall
619c38c493 Queue Manual Import commands at high priority
(cherry picked from commit 64c6a8879beb1b17122c8f6f74bf7b3cf4dd1570)
2024-03-03 12:29:45 +02:00
Louis R
0b8694c627 Fixed: Don't disable IPv6 in IPv6-only Environment
(cherry picked from commit 13af6f57796e54c3949cf340e03f020e6f8575c4)
2024-03-03 12:23:59 +02:00
Bogdan
e2793e56e9 Bump version to 5.4.2 2024-03-03 12:23:41 +02:00
nopoz
68f61da321 New: Add download directory & move completed for Deluge 2024-03-03 12:23:22 +02:00
Weblate
8edb541e21 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Nicolò Castagnola <nipica@outlook.it>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translation: Servarr/Radarr
2024-03-03 02:34:23 +02:00
Mark McDowall
d441becc74 New: Bypass archived history for failed downloads in SABnzbd
(cherry picked from commit c99d81e79ba5e6ecec01ddd942440d8a48a1c23b)
2024-03-02 04:01:40 +02:00
Mark McDowall
a97b2ee2ed Increase migration timeout to 5 minutes
(cherry picked from commit 086d3b5afaa7680d22835ca66da2afcb6dd5865e)
2024-03-02 04:00:35 +02:00
Bogdan
e70c61e24e Update caniuse-lite 2024-02-29 02:35:06 +02:00
Bogdan
d1f96746e0 Fix RSS translation token 2024-02-29 02:26:35 +02:00
Weblate
35893697bd Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: EDUYO <eduardoestabiel@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Magyar <kochnorbert@icloud.com>
Co-authored-by: Sadi A. Nogueira <contato@sadi.eti.br>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Xupix <colinaubert25@gmail.com>
Co-authored-by: 闫锦彪 <yanjinbiaohere@163.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2024-02-29 02:25:08 +02:00
Bogdan
540c150b93 Wrap external links tooltip in Movie Details with flex
Co-authored-by: ricci2511 <ricardo.christmann@protonmail.com>

Fixes #8003
2024-02-28 00:31:57 +02:00
Bogdan
48f819caee Bump version to 5.4.1 2024-02-27 02:25:03 +02:00
Bogdan
4ad7b60d9d New: Add 'cn' language code as Chinese language 2024-02-25 04:09:45 +02:00
Bogdan
7e4231fc0e Fixed: Selection of last added custom filter
Plus some translations and typos
2024-02-23 05:43:48 +02:00
bakerboy448
94287d9427 Update name for errors with metadata API 2024-02-22 01:03:08 +02:00
Servarr
8ec6b5dd4d Automated API Docs update 2024-02-21 22:05:02 +02:00
Mark McDowall
4be43c9f2b Fixed: Multi-word genres in Auto Tags
Fixed #6488

(cherry picked from commit 5c4f82999368edfedd038a0a27d323e04b81a400)
2024-02-21 20:53:34 +02:00
servarr[bot]
c388cf968b Bump node to v20.x on builder 2024-02-21 20:52:43 +02:00
Bogdan
b6b809f473 Fixed: Reprocessing custom formats for file in Manual Import 2024-02-21 20:46:16 +02:00
Bogdan
9dd31be7b3 New: Set Indexer flags in Manual Import 2024-02-21 20:46:16 +02:00
Bogdan
25ab396a2c Fixed: Notifications with only On Movie Add enabled being labeled as disabled 2024-02-21 17:05:39 +02:00
Bogdan
145cd74969 Avoid saving Indexer Flags in Last RSS Release info 2024-02-19 20:55:50 +02:00
Bogdan
b9c76d9bed Bump version to 5.4.0 2024-02-18 19:27:49 +02:00
Weblate
63f16924b1 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Chaoshuai Lü <lcs@meta.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Magyar <kochnorbert@icloud.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2024-02-17 21:35:15 +02:00
Bogdan
a91a9f7fd9 Fixed: Movie titles using default language after using movie editor 2024-02-17 09:08:56 +02:00
Bogdan
4c8e9f204e Fix movie queue status for download client unavailable 2024-02-16 03:49:23 +02:00
Bogdan
d64ee6681f Fixed: Avoid upgrades for custom formats cut-off already met 2024-02-15 02:21:21 +02:00
Bogdan
2ecc57cd31 Translations for Discover list 2024-02-15 00:34:35 +02:00
Bogdan
9620207503 Improve add/loading error notices
(cherry picked from commit dd704579df43b0dd835f8bb618c4b4412561a888)

Closes #9767
2024-02-15 00:06:28 +02:00
bakerboy448
0b090e5f39 Improve Custom Format rejection messaging
(cherry picked from commit cac97c057faa44c1656e02681cb9ba668faca488)

Closes #9747
2024-02-14 19:21:11 +02:00
Bogdan
51cb0920ed Fix translation token for DL client directory help text
Closes #9769
2024-02-14 19:15:31 +02:00
Bogdan
a90d6682d3 Update Custom Format Deletion confirmation message
Consistency with the rest of the Delete*MessageText

Closes #9766
2024-02-14 19:12:58 +02:00
Bogdan
db62eddf5a Fixed: Allow selection of Cast/Crew names
Fixed #9781
2024-02-14 19:09:19 +02:00
Weblate
ac2b2e6215 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Magyar <kochnorbert@icloud.com>
Co-authored-by: Steve Hansen <steve@hansenconsultancy.be>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translation: Servarr/Radarr
2024-02-14 19:09:05 +02:00
Bogdan
9581dd9764 Show download client ID as hint in select options
(cherry picked from commit c0b17d9345367ab6500b7cca6bb70c1e3b930284)
2024-02-14 02:55:47 +02:00
Bogdan
6c459c744a Improve messaging on indexer specified download client is not available
(cherry picked from commit 84e657482d37eed35f09c6dab3c2b8b5ebd5bac4)
2024-02-14 02:55:31 +02:00
abcasada
4676ecfce9 Hints for week column and short dates in UI settings
(cherry picked from commit 4558f552820b52bb1f9cd97fdabe03654ce9924a)

(cherry picked from commit f1d343218cdbd5a63abeb2eb97bba1105dc8035d)
2024-02-14 02:55:21 +02:00
Bogdan
ef92af9dd8 Fix translation for list exclusion on movie deletion 2024-02-14 02:54:06 +02:00
Qstick
b144482d68 Bump version to 5.3.6 2024-02-11 18:21:31 -06:00
Qstick
173b1d6a4c Fixed: Align DownloadClientInfo in ManualInteractionRequiredMessage with DownloadMessage
Also handle null ref exceptions when DownloadClientInfo is null in notification service
2024-02-11 17:56:35 -06:00
Servarr
5f624a147b Automated API Docs update 2024-02-10 23:10:20 -05:00
Stevie Robinson
af066da4ff New: Ignore 'Other' subfolder when scanning disk
Closes #9718

(cherry picked from commit e1c6722aad1e35a1e2371bc47f399b57e4f96f27)
2024-02-10 22:05:06 -06:00
Alex Herbig
937ebcdac3 New: Add RZeroX to release group parsing exceptions
Closes #9569
Clsoes #9719

(cherry picked from commit e2210228b34a4d98ef64965e810689d39733734e)
2024-02-10 21:54:59 -06:00
Mark McDowall
67f5199667 Fixed: Parsing Hungarian anime releases
Closes #9673

(cherry picked from commit 9ba5850fcaf0a5fb73dec7d7f8f1d8d3de0b3fb9)
2024-02-10 21:40:46 -06:00
Qstick
38cd130da5 Fixed: Remove old naming config from API responses
Closes #9741
2024-02-10 21:32:55 -06:00
Weblate
ed340be2b1 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Magyar <kochnorbert@icloud.com>
Co-authored-by: aghus <aghus.m@outlook.com>
Co-authored-by: bogdan-rgb <b.hmelniczky@yandex.ru>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/it/
Translation: Servarr/Radarr
2024-02-11 05:12:22 +02:00
Weblate
34cfb58b39 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Hicabi Erdem <bilgi@hicabierdem.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: aghus <aghus.m@outlook.com>
Co-authored-by: bai0012 <baicongrui@gmail.com>
Co-authored-by: gr0sz <joshuatg727@gmail.com>
Co-authored-by: savin-msk <ns@a77.io>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2024-02-09 15:26:25 +02:00
Bogdan
3d0f22ca7c Fixed: Ignore invalid tags in CustomScript/Discord/Webhook 2024-02-09 15:17:05 +02:00
Bogdan
2510f44c25 Fixed: Refresh tags state to clear removed tags by housekeeping 2024-02-07 23:20:25 +02:00
Bogdan
c0bf75cae3 Fixed: Recommendations on Postgres 2024-02-07 18:22:45 +02:00
Mark McDowall
a63ab1ddd6 Fixed: Don't use sub folder to check for free disk space for update
(cherry picked from commit f722d49b3a9efefa65bef1b24d90be9332ca62ea)

Closes #9748
2024-02-07 08:53:37 +02:00
Mark McDowall
41cb020ff0 New: Log database engine version on startup
(cherry picked from commit 6ab1d8e16b29e98b4d2ebb68e0356f6f2d3a2c10)
2024-02-07 08:51:39 +02:00
Mark McDowall
d660309b5a Fixed: Redirecting after login
(cherry picked from commit 745b92daf4bf4b9562ffe52dad84a12a5561add5)
2024-02-07 08:51:08 +02:00
Bogdan
222c19e4b3 Fixed: Edit button for import list exclusion on mobile
Fixes #9736
2024-02-06 01:19:58 +02:00
Mark McDowall
b08981dee0 Sort movie files on movie details page
(cherry picked from commit 113b0864b8e92b7b768acc8341bdf4c9e2e1a47f)
2024-02-05 00:24:21 +02:00
Bogdan
4a9c0b2240 Bump version to 5.3.5 2024-02-04 12:54:19 +02:00
218 changed files with 3765 additions and 1836 deletions

View File

@@ -9,14 +9,14 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '5.3.4'
majorVersion: '5.4.5'
minorVersion: $[counter('minorVersion', 2000)]
radarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.417'
nodeVersion: '16.X'
nodeVersion: '20.X'
innoVersion: '6.2.2'
windowsImage: 'windows-2022'
linuxImage: 'ubuntu-20.04'

View File

@@ -20,7 +20,7 @@ const monitoredOptions = [
get value() {
return translate('NoChange');
},
disabled: true
isDisabled: true
},
{
key: 'monitored',
@@ -42,7 +42,7 @@ const searchOnAddOptions = [
get value() {
return translate('NoChange');
},
disabled: true
isDisabled: true
},
{
key: 'yes',

View File

@@ -13,6 +13,7 @@ export interface CommandBody {
trigger: string;
suppressMessages: boolean;
movieId?: number;
movieIds?: number[];
}
interface Command extends ModelBase {

View File

@@ -1,3 +1,4 @@
import { maxBy } from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import FormInputGroup from 'Components/Form/FormInputGroup';
@@ -50,7 +51,7 @@ class FilterBuilderModalContent extends Component {
if (id) {
dispatchSetFilter({ selectedFilterKey: id });
} else {
const last = customFilters[customFilters.length -1];
const last = maxBy(customFilters, 'id');
dispatchSetFilter({ selectedFilterKey: last.id });
}
@@ -108,7 +109,7 @@ class FilterBuilderModalContent extends Component {
this.setState({
labelErrors: [
{
message: 'Label is required'
message: translate('LabelIsRequired')
}
]
});
@@ -146,13 +147,13 @@ class FilterBuilderModalContent extends Component {
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Custom Filter
{translate('CustomFilter')}
</ModalHeader>
<ModalBody>
<div className={styles.labelContainer}>
<div className={styles.label}>
Label
{translate('Label')}
</div>
<div className={styles.labelInputContainer}>
@@ -166,9 +167,7 @@ class FilterBuilderModalContent extends Component {
</div>
</div>
<div className={styles.label}>
{translate('Filters')}
</div>
<div className={styles.label}>{translate('Filters')}</div>
<div className={styles.rows}>
{

View File

@@ -37,8 +37,8 @@ class CustomFilter extends Component {
dispatchSetFilter
} = this.props;
// Assume that delete and then unmounting means the delete was successful.
// Moving this check to a ancestor would be more accurate, but would have
// Assume that delete and then unmounting means the deletion was successful.
// Moving this check to an ancestor would be more accurate, but would have
// more boilerplate.
if (this.state.isDeleting && id === selectedFilterKey) {
dispatchSetFilter({ selectedFilterKey: 'all' });

View File

@@ -36,7 +36,7 @@ function AvailabilitySelectInput(props) {
values.unshift({
key: 'noChange',
value: translate('NoChange'),
disabled: true
isDisabled: true
});
}
@@ -44,7 +44,7 @@ function AvailabilitySelectInput(props) {
values.unshift({
key: 'mixed',
value: '(Mixed)',
disabled: true
isDisabled: true
});
}

View File

@@ -25,7 +25,8 @@ function createMapStateToProps() {
const values = _.map(filteredItems.sort(sortByName), (downloadClient) => {
return {
key: downloadClient.id,
value: downloadClient.name
value: downloadClient.name,
hint: `(${downloadClient.id})`
};
});

View File

@@ -19,7 +19,7 @@
.isDisabled {
opacity: 0.7;
cursor: not-allowed;
cursor: not-allowed !important;
}
.dropdownArrowContainer {

View File

@@ -282,6 +282,7 @@ FormInputGroup.propTypes = {
includeNoChange: PropTypes.bool,
includeNoChangeDisabled: PropTypes.bool,
selectedValueOptions: PropTypes.object,
indexerFlags: PropTypes.number,
pending: PropTypes.bool,
errors: PropTypes.arrayOf(PropTypes.object),
warnings: PropTypes.arrayOf(PropTypes.object),

View File

@@ -4,22 +4,18 @@ import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import EnhancedSelectInput from './EnhancedSelectInput';
interface IndexerFlagsSelectInputProps {
name: string;
indexerFlags: number;
onChange(payload: object): void;
}
const selectIndexerFlagsValues = (selectedFlags: number) =>
createSelector(
(state: AppState) => state.settings.indexerFlags,
(indexerFlags) => {
const value = indexerFlags.items
.filter(
// eslint-disable-next-line no-bitwise
(item) => (selectedFlags & item.id) === item.id
)
.map(({ id }) => id);
const value = indexerFlags.items.reduce((acc: number[], { id }) => {
// eslint-disable-next-line no-bitwise
if ((selectedFlags & id) === id) {
acc.push(id);
}
return acc;
}, []);
const values = indexerFlags.items.map(({ id, name }) => ({
key: id,
@@ -33,6 +29,12 @@ const selectIndexerFlagsValues = (selectedFlags: number) =>
}
);
interface IndexerFlagsSelectInputProps {
name: string;
indexerFlags: number;
onChange(payload: object): void;
}
function IndexerFlagsSelectInput(props: IndexerFlagsSelectInputProps) {
const { indexerFlags, onChange } = props;

View File

@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import monitorOptions from 'Utilities/Movie/monitorOptions';
import translate from 'Utilities/String/translate';
import SelectInput from './SelectInput';
import EnhancedSelectInput from './EnhancedSelectInput';
function MovieMonitoredSelectInput(props) {
const values = [...monitorOptions];
@@ -16,7 +16,7 @@ function MovieMonitoredSelectInput(props) {
values.unshift({
key: 'noChange',
value: translate('NoChange'),
disabled: true
isDisabled: true
});
}
@@ -24,12 +24,12 @@ function MovieMonitoredSelectInput(props) {
values.unshift({
key: 'mixed',
value: '(Mixed)',
disabled: true
isDisabled: true
});
}
return (
<SelectInput
<EnhancedSelectInput
{...props}
values={values}
/>

View File

@@ -26,7 +26,7 @@ function createMapStateToProps() {
values.unshift({
key: 'noChange',
value: translate('NoChange'),
disabled: includeNoChangeDisabled
isDisabled: includeNoChangeDisabled
});
}
@@ -34,7 +34,7 @@ function createMapStateToProps() {
values.unshift({
key: 'mixed',
value: '(Mixed)',
disabled: true
isDisabled: true
});
}

View File

@@ -91,6 +91,7 @@ class TextTagInputConnector extends Component {
render() {
return (
<TagInput
delimiters={['Tab', 'Enter', ',']}
tagList={[]}
onTagAdd={this.onTagAdd}
onTagDelete={this.onTagDelete}

View File

@@ -45,6 +45,7 @@ const selectAppProps = createSelector(
);
const selectIsPopulated = createSelector(
(state) => state.movies.isPopulated,
(state) => state.customFilters.isPopulated,
(state) => state.tags.isPopulated,
(state) => state.settings.ui.isPopulated,
@@ -56,6 +57,7 @@ const selectIsPopulated = createSelector(
(state) => state.movieCollections.isPopulated,
(state) => state.app.translations.isPopulated,
(
moviesIsPopulated,
customFiltersIsPopulated,
tagsIsPopulated,
uiSettingsIsPopulated,
@@ -68,6 +70,7 @@ const selectIsPopulated = createSelector(
translationsIsPopulated
) => {
return (
moviesIsPopulated &&
customFiltersIsPopulated &&
tagsIsPopulated &&
uiSettingsIsPopulated &&
@@ -83,6 +86,7 @@ const selectIsPopulated = createSelector(
);
const selectErrors = createSelector(
(state) => state.movies.error,
(state) => state.customFilters.error,
(state) => state.tags.error,
(state) => state.settings.ui.error,
@@ -94,6 +98,7 @@ const selectErrors = createSelector(
(state) => state.movieCollections.error,
(state) => state.app.translations.error,
(
moviesError,
customFiltersError,
tagsError,
uiSettingsError,
@@ -106,6 +111,7 @@ const selectErrors = createSelector(
translationsError
) => {
const hasError = !!(
moviesError ||
customFiltersError ||
tagsError ||
uiSettingsError ||

View File

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

View File

@@ -7,6 +7,7 @@ import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import DiscoverMovieTableOptionsConnector from './DiscoverMovieTableOptionsConnector';
import styles from './DiscoverMovieHeader.css';
@@ -98,6 +99,7 @@ class DiscoverMovieHeader extends Component {
<Icon
name={icons.RECOMMENDED}
size={12}
title={translate('Recommendation')}
/>
</VirtualTableHeaderCell>
);
@@ -115,6 +117,7 @@ class DiscoverMovieHeader extends Component {
<Icon
name={icons.TRENDING}
size={12}
title={translate('Trending')}
/>
</VirtualTableHeaderCell>
);
@@ -132,6 +135,7 @@ class DiscoverMovieHeader extends Component {
<Icon
name={icons.POPULAR}
size={12}
title={translate('Popular')}
/>
</VirtualTableHeaderCell>
);

View File

@@ -0,0 +1,17 @@
import { useCallback, useState } from 'react';
export default function useModalOpenState(
initialState: boolean
): [boolean, () => void, () => void] {
const [isOpen, setOpen] = useState(initialState);
const setModalOpen = useCallback(() => {
setOpen(true);
}, [setOpen]);
const setModalClosed = useCallback(() => {
setOpen(false);
}, [setOpen]);
return [isOpen, setModalOpen, setModalClosed];
}

View File

@@ -0,0 +1,34 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import SelectIndexerFlagsModalContent from './SelectIndexerFlagsModalContent';
interface SelectIndexerFlagsModalProps {
isOpen: boolean;
indexerFlags: number;
modalTitle: string;
onIndexerFlagsSelect(indexerFlags: number): void;
onModalClose(): void;
}
function SelectIndexerFlagsModal(props: SelectIndexerFlagsModalProps) {
const {
isOpen,
indexerFlags,
modalTitle,
onIndexerFlagsSelect,
onModalClose,
} = props;
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<SelectIndexerFlagsModalContent
indexerFlags={indexerFlags}
modalTitle={modalTitle}
onIndexerFlagsSelect={onIndexerFlagsSelect}
onModalClose={onModalClose}
/>
</Modal>
);
}
export default SelectIndexerFlagsModal;

View File

@@ -0,0 +1,7 @@
.modalBody {
composes: modalBody from '~Components/Modal/ModalBody.css';
display: flex;
flex: 1 1 auto;
flex-direction: column;
}

View File

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

View File

@@ -0,0 +1,75 @@
import React, { useCallback, useState } from 'react';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds, scrollDirections } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './SelectIndexerFlagsModalContent.css';
interface SelectIndexerFlagsModalContentProps {
indexerFlags: number;
modalTitle: string;
onIndexerFlagsSelect(indexerFlags: number): void;
onModalClose(): void;
}
function SelectIndexerFlagsModalContent(
props: SelectIndexerFlagsModalContentProps
) {
const { modalTitle, onIndexerFlagsSelect, onModalClose } = props;
const [indexerFlags, setIndexerFlags] = useState(props.indexerFlags);
const onIndexerFlagsChange = useCallback(
({ value }: { value: number }) => {
setIndexerFlags(value);
},
[setIndexerFlags]
);
const onIndexerFlagsSelectWrapper = useCallback(() => {
onIndexerFlagsSelect(indexerFlags);
}, [indexerFlags, onIndexerFlagsSelect]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('SetIndexerFlagsModalTitle', { modalTitle })}
</ModalHeader>
<ModalBody
className={styles.modalBody}
scrollDirection={scrollDirections.NONE}
>
<Form>
<FormGroup>
<FormLabel>{translate('IndexerFlags')}</FormLabel>
<FormInputGroup
type={inputTypes.INDEXER_FLAGS_SELECT}
name="indexerFlags"
indexerFlags={indexerFlags}
autoFocus={true}
onChange={onIndexerFlagsChange}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
<Button kind={kinds.SUCCESS} onPress={onIndexerFlagsSelectWrapper}>
{translate('SetIndexerFlags')}
</Button>
</ModalFooter>
</ModalContent>
);
}
export default SelectIndexerFlagsModalContent;

View File

@@ -26,6 +26,7 @@ import usePrevious from 'Helpers/Hooks/usePrevious';
import useSelectState from 'Helpers/Hooks/useSelectState';
import { align, icons, kinds, scrollDirections } from 'Helpers/Props';
import ImportMode from 'InteractiveImport/ImportMode';
import SelectIndexerFlagsModal from 'InteractiveImport/IndexerFlags/SelectIndexerFlagsModal';
import InteractiveImport, {
InteractiveImportCommandOptions,
} from 'InteractiveImport/InteractiveImport';
@@ -59,7 +60,13 @@ import getSelectedIds from 'Utilities/Table/getSelectedIds';
import InteractiveImportRow from './InteractiveImportRow';
import styles from './InteractiveImportModalContent.css';
type SelectType = 'select' | 'movie' | 'releaseGroup' | 'quality' | 'language';
type SelectType =
| 'select'
| 'movie'
| 'releaseGroup'
| 'quality'
| 'language'
| 'indexerFlags';
type FilterExistingFiles = 'all' | 'new';
@@ -113,6 +120,15 @@ const COLUMNS = [
isSortable: true,
isVisible: true,
},
{
name: 'indexerFlags',
label: React.createElement(Icon, {
name: icons.FLAG,
title: () => translate('IndexerFlags'),
}),
isSortable: true,
isVisible: true,
},
{
name: 'rejections',
label: React.createElement(Icon, {
@@ -257,8 +273,18 @@ function InteractiveImportModalContent(
}
}
const showIndexerFlags = items.some((item) => item.indexerFlags);
if (!showIndexerFlags) {
const indexerFlagsColumn = result.find((c) => c.name === 'indexerFlags');
if (indexerFlagsColumn) {
indexerFlagsColumn.isVisible = false;
}
}
return result;
}, [showMovie]);
}, [showMovie, items]);
const selectedIds: number[] = useMemo(() => {
return getSelectedIds(selectedState);
@@ -283,6 +309,10 @@ function InteractiveImportModalContent(
key: 'language',
value: translate('SelectLanguage'),
},
{
key: 'indexerFlags',
value: translate('SelectIndexerFlags'),
},
];
if (allowMovieChange) {
@@ -416,7 +446,14 @@ function InteractiveImportModalContent(
const isSelected = selectedIds.indexOf(item.id) > -1;
if (isSelected) {
const { movie, releaseGroup, quality, languages, movieFileId } = item;
const {
movie,
releaseGroup,
quality,
languages,
indexerFlags,
movieFileId,
} = item;
if (!movie) {
setInteractiveImportErrorMessage(
@@ -450,6 +487,7 @@ function InteractiveImportModalContent(
releaseGroup,
quality,
languages,
indexerFlags,
});
return;
@@ -463,6 +501,7 @@ function InteractiveImportModalContent(
releaseGroup,
quality,
languages,
indexerFlags,
downloadId,
movieFileId,
});
@@ -620,6 +659,22 @@ function InteractiveImportModalContent(
[selectedIds, dispatch]
);
const onIndexerFlagsSelect = useCallback(
(indexerFlags: number) => {
dispatch(
updateInteractiveImportItems({
ids: selectedIds,
indexerFlags,
})
);
dispatch(reprocessInteractiveImportItems({ ids: selectedIds }));
setSelectModalOpen(null);
},
[selectedIds, dispatch]
);
const errorMessage = getErrorMessage(
error,
translate('InteractiveImportLoadError')
@@ -794,6 +849,14 @@ function InteractiveImportModalContent(
onModalClose={onSelectModalClose}
/>
<SelectIndexerFlagsModal
isOpen={selectModalOpen === 'indexerFlags'}
indexerFlags={0}
modalTitle={modalTitle}
onIndexerFlagsSelect={onIndexerFlagsSelect}
onModalClose={onSelectModalClose}
/>
<ConfirmModal
isOpen={isConfirmDeleteModalOpen}
kind={kinds.DANGER}

View File

@@ -8,11 +8,13 @@ import Column from 'Components/Table/Column';
import TableRow from 'Components/Table/TableRow';
import Popover from 'Components/Tooltip/Popover';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import SelectIndexerFlagsModal from 'InteractiveImport/IndexerFlags/SelectIndexerFlagsModal';
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
import SelectMovieModal from 'InteractiveImport/Movie/SelectMovieModal';
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
import Language from 'Language/Language';
import IndexerFlags from 'Movie/IndexerFlags';
import Movie from 'Movie/Movie';
import MovieFormats from 'Movie/MovieFormats';
import MovieLanguage from 'Movie/MovieLanguage';
@@ -30,7 +32,12 @@ import translate from 'Utilities/String/translate';
import InteractiveImportRowCellPlaceholder from './InteractiveImportRowCellPlaceholder';
import styles from './InteractiveImportRow.css';
type SelectType = 'movie' | 'releaseGroup' | 'quality' | 'language';
type SelectType =
| 'movie'
| 'releaseGroup'
| 'quality'
| 'language'
| 'indexerFlags';
type SelectedChangeProps = SelectStateInputProps & {
hasMovieFileId: boolean;
@@ -47,6 +54,7 @@ interface InteractiveImportRowProps {
size: number;
customFormats?: object[];
customFormatScore?: number;
indexerFlags: number;
rejections: Rejection[];
columns: Column[];
movieFileId?: number;
@@ -69,6 +77,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
size,
customFormats,
customFormatScore,
indexerFlags,
rejections,
isSelected,
modalTitle,
@@ -84,6 +93,10 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
() => columns.find((c) => c.name === 'movie')?.isVisible ?? false,
[columns]
);
const isIndexerFlagsColumnVisible = useMemo(
() => columns.find((c) => c.name === 'indexerFlags')?.isVisible ?? false,
[columns]
);
const [selectModalOpen, setSelectModalOpen] = useState<SelectType | null>(
null
@@ -223,12 +236,34 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
[id, dispatch, setSelectModalOpen, selectRowAfterChange]
);
const onSelectIndexerFlagsPress = useCallback(() => {
setSelectModalOpen('indexerFlags');
}, [setSelectModalOpen]);
const onIndexerFlagsSelect = useCallback(
(indexerFlags: number) => {
dispatch(
updateInteractiveImportItem({
id,
indexerFlags,
})
);
dispatch(reprocessInteractiveImportItems({ ids: [id] }));
setSelectModalOpen(null);
selectRowAfterChange();
},
[id, dispatch, setSelectModalOpen, selectRowAfterChange]
);
const movieTitle = movie ? movie.title : '';
const showMoviePlaceholder = isSelected && !movie;
const showReleaseGroupPlaceholder = isSelected && !releaseGroup;
const showQualityPlaceholder = isSelected && !quality;
const showLanguagePlaceholder = isSelected && !languages;
const showIndexerFlagsPlaceholder = isSelected && !indexerFlags;
return (
<TableRow>
@@ -311,6 +346,28 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
) : null}
</TableRowCell>
{isIndexerFlagsColumnVisible ? (
<TableRowCellButton
title={translate('ClickToChangeIndexerFlags')}
onPress={onSelectIndexerFlagsPress}
>
{showIndexerFlagsPlaceholder ? (
<InteractiveImportRowCellPlaceholder isOptional={true} />
) : (
<>
{indexerFlags ? (
<Popover
anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />}
title={translate('IndexerFlags')}
body={<IndexerFlags indexerFlags={indexerFlags} />}
position={tooltipPositions.LEFT}
/>
) : null}
</>
)}
</TableRowCellButton>
) : null}
<TableRowCell>
{rejections.length ? (
<Popover
@@ -361,6 +418,14 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
onLanguagesSelect={onLanguagesSelect}
onModalClose={onSelectModalClose}
/>
<SelectIndexerFlagsModal
isOpen={selectModalOpen === 'indexerFlags'}
indexerFlags={indexerFlags ?? 0}
modalTitle={modalTitle}
onIndexerFlagsSelect={onIndexerFlagsSelect}
onModalClose={onSelectModalClose}
/>
</TableRow>
);
}

View File

@@ -11,6 +11,7 @@ export interface InteractiveImportCommandOptions {
releaseGroup?: string;
quality: QualityModel;
languages: Language[];
indexerFlags: number;
downloadId?: string;
movieFileId?: number;
}
@@ -27,6 +28,7 @@ interface InteractiveImport extends ModelBase {
movie?: Movie;
qualityWeight: number;
customFormats: object[];
indexerFlags: number;
rejections: Rejection[];
movieFileId?: number;
}

View File

@@ -39,8 +39,7 @@
}
.rejected,
.indexerFlags,
.download {
.indexerFlags {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 50px;

View File

@@ -90,8 +90,8 @@ interface InteractiveSearchRowProps {
customFormats: CustomFormat[];
customFormatScore: number;
mappedMovieId?: number;
rejections: string[];
indexerFlags: string[];
rejections: string[];
downloadAllowed: boolean;
isGrabbing: boolean;
isGrabbed: boolean;
@@ -125,8 +125,8 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
customFormatScore,
customFormats,
mappedMovieId,
rejections = [],
indexerFlags = [],
rejections = [],
downloadAllowed,
isGrabbing = false,
isGrabbed = false,
@@ -276,7 +276,7 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
customFormats.length
)}
tooltip={<MovieFormats formats={customFormats} />}
position={tooltipPositions.TOP}
position={tooltipPositions.LEFT}
/>
</TableRowCell>

View File

@@ -98,7 +98,7 @@ class DeleteMovieModalContent extends Component {
type={inputTypes.CHECK}
name="addImportExclusion"
value={addImportExclusion}
helpText={translate('AddImportExclusionHelpText')}
helpText={translate('AddListExclusionMovieHelpText')}
kind={kinds.DANGER}
onChange={onDeleteOptionChange}
/>

View File

@@ -1,3 +1,4 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Icon from 'Components/Icon';
@@ -142,10 +143,10 @@ class MovieCastPoster extends Component {
</div>
</div>
<div className={styles.title}>
<div className={classNames(styles.title, 'swiper-no-swiping')}>
{personName}
</div>
<div className={styles.title}>
<div className={classNames(styles.title, 'swiper-no-swiping')}>
{character}
</div>

View File

@@ -1,3 +1,4 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Icon from 'Components/Icon';
@@ -142,10 +143,10 @@ class MovieCrewPoster extends Component {
</div>
</div>
<div className={styles.title}>
<div className={classNames(styles.title, 'swiper-no-swiping')}>
{personName}
</div>
<div className={styles.title}>
<div className={classNames(styles.title, 'swiper-no-swiping')}>
{job}
</div>

View File

@@ -1,9 +1,10 @@
.links {
display: flex;
flex-wrap: wrap;
margin: 0;
}
.link {
display: inline-block;
white-space: nowrap;
}

View File

@@ -44,6 +44,7 @@ import MovieIndexViewMenu from './Menus/MovieIndexViewMenu';
import MovieIndexFooter from './MovieIndexFooter';
import MovieIndexRefreshMovieButton from './MovieIndexRefreshMovieButton';
import MovieIndexSearchButton from './MovieIndexSearchButton';
import MovieIndexSearchMenuItem from './MovieIndexSearchMenuItem';
import MovieIndexOverviews from './Overview/MovieIndexOverviews';
import MovieIndexOverviewOptionsModal from './Overview/Options/MovieIndexOverviewOptionsModal';
import MovieIndexPosters from './Posters/MovieIndexPosters';
@@ -247,6 +248,7 @@ const MovieIndex = withScrollPosition((props: MovieIndexProps) => {
<MovieIndexSearchButton
isSelectMode={isSelectMode}
selectedFilterKey={selectedFilterKey}
overflowComponent={MovieIndexSearchMenuItem}
/>
<PageToolbarButton

View File

@@ -16,6 +16,7 @@ import getSelectedIds from 'Utilities/Table/getSelectedIds';
interface MovieIndexSearchButtonProps {
isSelectMode: boolean;
selectedFilterKey: string;
overflowComponent: React.FunctionComponent<never>;
}
function MovieIndexSearchButton(props: MovieIndexSearchButtonProps) {

View File

@@ -0,0 +1,72 @@
import React, { useCallback, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useSelect } from 'App/SelectContext';
import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState';
import MoviesAppState, { MovieIndexAppState } from 'App/State/MoviesAppState';
import { MOVIE_SEARCH } from 'Commands/commandNames';
import PageToolbarOverflowMenuItem from 'Components/Page/Toolbar/PageToolbarOverflowMenuItem';
import { icons } from 'Helpers/Props';
import { executeCommand } from 'Store/Actions/commandActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import createMovieClientSideCollectionItemsSelector from 'Store/Selectors/createMovieClientSideCollectionItemsSelector';
import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
interface MovieIndexSearchMenuItemProps {
isSelectMode: boolean;
selectedFilterKey: string;
}
function MovieIndexSearchMenuItem(props: MovieIndexSearchMenuItemProps) {
const isSearching = useSelector(createCommandExecutingSelector(MOVIE_SEARCH));
const {
items,
}: MoviesAppState & MovieIndexAppState & ClientSideCollectionAppState =
useSelector(createMovieClientSideCollectionItemsSelector('movieIndex'));
const dispatch = useDispatch();
const { isSelectMode, selectedFilterKey } = props;
const [selectState] = useSelect();
const { selectedState } = selectState;
const selectedMovieIds = useMemo(() => {
return getSelectedIds(selectedState);
}, [selectedState]);
const moviesToSearch =
isSelectMode && selectedMovieIds.length > 0
? selectedMovieIds
: items.map((m) => m.id);
const searchIndexLabel =
selectedFilterKey === 'all'
? translate('SearchAll')
: translate('SearchFiltered');
const searchSelectLabel =
selectedMovieIds.length > 0
? translate('SearchSelected')
: translate('SearchAll');
const onPress = useCallback(() => {
dispatch(
executeCommand({
name: MOVIE_SEARCH,
movieIds: moviesToSearch,
})
);
}, [dispatch, moviesToSearch]);
return (
<PageToolbarOverflowMenuItem
label={isSelectMode ? searchSelectLabel : searchIndexLabel}
isSpinning={isSearching}
isDisabled={!items.length}
iconName={icons.SEARCH}
onPress={onPress}
/>
);
}
export default MovieIndexSearchMenuItem;

View File

@@ -98,7 +98,7 @@ function DeleteMovieModalContent(props: DeleteMovieModalContentProps) {
type={inputTypes.CHECK}
name="addImportExclusion"
value={addImportExclusion}
helpText={translate('AddImportExclusionHelpText')}
helpText={translate('AddListExclusionMovieHelpText')}
onChange={onDeleteOptionChange}
/>
</FormGroup>

View File

@@ -34,7 +34,7 @@ const monitoredOptions = [
get value() {
return translate('NoChange');
},
disabled: true,
isDisabled: true,
},
{
key: 'monitored',

View File

@@ -0,0 +1,26 @@
import React from 'react';
import { useSelector } from 'react-redux';
import createIndexerFlagsSelector from 'Store/Selectors/createIndexerFlagsSelector';
interface IndexerFlagsProps {
indexerFlags: number;
}
function IndexerFlags({ indexerFlags = 0 }: IndexerFlagsProps) {
const allIndexerFlags = useSelector(createIndexerFlagsSelector);
const flags = allIndexerFlags.items.filter(
// eslint-disable-next-line no-bitwise
(item) => (indexerFlags & item.id) === item.id
);
return flags.length ? (
<ul>
{flags.map((flag, index) => {
return <li key={index}>{flag.name}</li>;
})}
</ul>
) : null;
}
export default IndexerFlags;

View File

@@ -57,3 +57,9 @@
width: 55px;
}
.indexerFlags {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 50px;
}

View File

@@ -9,6 +9,7 @@ interface CssExports {
'dateAdded': string;
'download': string;
'formats': string;
'indexerFlags': string;
'language': string;
'languages': string;
'quality': string;

View File

@@ -1,12 +1,15 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Icon from 'Components/Icon';
import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import Popover from 'Components/Tooltip/Popover';
import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import IndexerFlags from 'Movie/IndexerFlags';
import MovieFormats from 'Movie/MovieFormats';
import MovieLanguage from 'Movie/MovieLanguage';
import MovieQuality from 'Movie/MovieQuality';
@@ -82,6 +85,7 @@ class MovieFileEditorRow extends Component {
qualityCutoffNotMet,
customFormats,
customFormatScore,
indexerFlags,
languages,
dateAdded,
columns
@@ -143,12 +147,30 @@ class MovieFileEditorRow extends Component {
customFormats.length
)}
tooltip={<MovieFormats formats={customFormats} />}
position={tooltipPositions.TOP}
position={tooltipPositions.LEFT}
/>
</TableRowCell>
);
}
if (name === 'indexerFlags') {
return (
<TableRowCell
key={name}
className={styles.indexerFlags}
>
{indexerFlags ? (
<Popover
anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />}
title={translate('IndexerFlags')}
body={<IndexerFlags indexerFlags={indexerFlags} />}
position={tooltipPositions.LEFT}
/>
) : null}
</TableRowCell>
);
}
if (name === 'languages') {
return (
<TableRowCell
@@ -363,6 +385,7 @@ MovieFileEditorRow.propTypes = {
releaseGroup: PropTypes.string,
customFormats: PropTypes.arrayOf(PropTypes.object).isRequired,
customFormatScore: PropTypes.number.isRequired,
indexerFlags: PropTypes.number.isRequired,
qualityCutoffNotMet: PropTypes.bool.isRequired,
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
mediaInfo: PropTypes.object,
@@ -372,7 +395,8 @@ MovieFileEditorRow.propTypes = {
};
MovieFileEditorRow.defaultProps = {
customFormats: []
customFormats: [],
indexerFlags: 0
};
export default MovieFileEditorRow;

View File

@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { sortDirections } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import MovieFileEditorRow from './MovieFileEditorRow';
import styles from './MovieFileEditorTableContent.css';
@@ -15,6 +16,9 @@ class MovieFileEditorTableContent extends Component {
const {
items,
columns,
sortKey,
sortDirection,
onSortPress,
onTableOptionChange
} = this.props;
@@ -31,6 +35,9 @@ class MovieFileEditorTableContent extends Component {
!!items.length &&
<Table
columns={columns}
sortKey={sortKey}
sortDirection={sortDirection}
onSortPress={onSortPress}
onTableOptionChange={onTableOptionChange}
>
<TableBody>
@@ -60,7 +67,10 @@ MovieFileEditorTableContent.propTypes = {
isDeleting: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
sortKey: PropTypes.string.isRequired,
sortDirection: PropTypes.oneOf(sortDirections.all),
onTableOptionChange: PropTypes.func.isRequired,
onSortPress: PropTypes.func.isRequired,
onDeletePress: PropTypes.func.isRequired
};

View File

@@ -2,8 +2,9 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { deleteMovieFile, setMovieFilesTableOption, updateMovieFiles } from 'Store/Actions/movieFileActions';
import { deleteMovieFile, setMovieFilesSort, setMovieFilesTableOption } from 'Store/Actions/movieFileActions';
import { fetchLanguages, fetchQualityProfileSchema } from 'Store/Actions/settingsActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import createMovieSelector from 'Store/Selectors/createMovieSelector';
import getQualities from 'Utilities/Quality/getQualities';
import MovieFileEditorTableContent from './MovieFileEditorTableContent';
@@ -11,7 +12,7 @@ import MovieFileEditorTableContent from './MovieFileEditorTableContent';
function createMapStateToProps() {
return createSelector(
(state, { movieId }) => movieId,
(state) => state.movieFiles,
createClientSideCollectionSelector('movieFiles'),
(state) => state.settings.languages,
(state) => state.settings.qualityProfiles,
createMovieSelector(),
@@ -28,6 +29,8 @@ function createMapStateToProps() {
return {
items: filesForMovie,
columns: movieFiles.columns,
sortKey: movieFiles.sortKey,
sortDirection: movieFiles.sortDirection,
isDeleting: movieFiles.isDeleting,
isSaving: movieFiles.isSaving,
error: null,
@@ -38,31 +41,13 @@ function createMapStateToProps() {
);
}
function createMapDispatchToProps(dispatch, props) {
return {
dispatchFetchQualityProfileSchema() {
dispatch(fetchQualityProfileSchema());
},
dispatchFetchLanguages() {
dispatch(fetchLanguages());
},
dispatchUpdateMovieFiles(updateProps) {
dispatch(updateMovieFiles(updateProps));
},
onTableOptionChange(payload) {
dispatch(setMovieFilesTableOption(payload));
},
onDeletePress(movieFileId) {
dispatch(deleteMovieFile({
id: movieFileId
}));
}
};
}
const mapDispatchToProps = {
fetchQualityProfileSchema,
fetchLanguages,
deleteMovieFile,
setMovieFilesTableOption,
setMovieFilesSort
};
class MovieFileEditorTableContentConnector extends Component {
@@ -70,24 +55,40 @@ class MovieFileEditorTableContentConnector extends Component {
// Lifecycle
componentDidMount() {
this.props.dispatchFetchLanguages();
this.props.dispatchFetchQualityProfileSchema();
this.props.fetchLanguages();
this.props.fetchQualityProfileSchema();
}
//
// Listeners
onDeletePress = (movieFileId) => {
this.props.deleteMovieFile({
id: movieFileId
});
};
onTableOptionChange = (payload) => {
this.props.setMovieFilesTableOption(payload);
};
onSortPress = (sortKey, sortDirection) => {
this.props.setMovieFilesSort({
sortKey,
sortDirection
});
};
//
// Render
render() {
const {
dispatchFetchLanguages,
dispatchFetchQualityProfileSchema,
dispatchUpdateMovieFiles,
...otherProps
} = this.props;
return (
<MovieFileEditorTableContent
{...otherProps}
{...this.props}
onDeletePress={this.onDeletePress}
onTableOptionChange={this.onTableOptionChange}
onSortPress={this.onSortPress}
/>
);
}
@@ -97,9 +98,11 @@ MovieFileEditorTableContentConnector.propTypes = {
movieId: PropTypes.number.isRequired,
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
qualities: PropTypes.arrayOf(PropTypes.object).isRequired,
dispatchFetchLanguages: PropTypes.func.isRequired,
dispatchFetchQualityProfileSchema: PropTypes.func.isRequired,
dispatchUpdateMovieFiles: PropTypes.func.isRequired
fetchLanguages: PropTypes.func.isRequired,
fetchQualityProfileSchema: PropTypes.func.isRequired,
deleteMovieFile: PropTypes.func.isRequired,
setMovieFilesTableOption: PropTypes.func.isRequired,
setMovieFilesSort: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, createMapDispatchToProps)(MovieFileEditorTableContentConnector);
export default connect(createMapStateToProps, mapDispatchToProps)(MovieFileEditorTableContentConnector);

View File

@@ -15,6 +15,7 @@ export interface MovieFile extends ModelBase {
languages: Language[];
quality: QualityModel;
customFormats: CustomFormat[];
indexerFlags: number;
mediaInfo: MediaInfo;
qualityCutoffNotMet: boolean;
}

View File

@@ -104,7 +104,7 @@ class OrganizePreviewModalContent extends Component {
{
!isFetching && error &&
<div>{translate('OrganizeLoadError')}</div>
<Alert kind={kinds.DANGER}>{translate('OrganizeLoadError')}</Alert>
}
{

View File

@@ -152,7 +152,7 @@ class CustomFormat extends Component {
isOpen={this.state.isDeleteCustomFormatModalOpen}
kind={kinds.DANGER}
title={translate('DeleteCustomFormat')}
message={translate('DeleteCustomFormatMessageText', { customFormatName: name })}
message={translate('DeleteCustomFormatMessageText', { name })}
confirmLabel={translate('Delete')}
isSpinning={isDeleting}
onConfirm={this.onConfirmDeleteCustomFormat}

View File

@@ -62,7 +62,7 @@ class CustomFormats extends Component {
<FieldSet legend={translate('CustomFormats')}>
<PageSectionContent
errorMessage={translate('CustomFormatsLoadError')}
{...otherProps}c={true}
{...otherProps}
>
<div className={styles.customFormats}>
{

View File

@@ -151,6 +151,11 @@ class EditCustomFormatModalContent extends Component {
</Form>
<FieldSet legend={translate('Conditions')}>
<Alert kind={kinds.INFO}>
<div>
{translate('CustomFormatsSettingsTriggerInfo')}
</div>
</Alert>
<div className={styles.customFormats}>
{
specifications.map((tag) => {

View File

@@ -42,9 +42,9 @@ class AddSpecificationModalContent extends Component {
{
!isSchemaFetching && !!schemaError &&
<div>
<Alert kind={kinds.DANGER}>
{translate('AddConditionError')}
</div>
</Alert>
}
{

View File

@@ -41,7 +41,7 @@ class DownloadClient extends Component {
});
};
onDeleteDownloadClientModalClose= () => {
onDeleteDownloadClientModalClose = () => {
this.setState({ isDeleteDownloadClientModalOpen: false });
};

View File

@@ -15,6 +15,7 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
import translate from 'Utilities/String/translate';
import styles from './EditDownloadClientModalContent.css';
@@ -37,6 +38,7 @@ class EditDownloadClientModalContent extends Component {
onModalClose,
onSavePress,
onTestPress,
onAdvancedSettingsPress,
onDeleteDownloadClientPress,
...otherProps
} = this.props;
@@ -199,6 +201,12 @@ class EditDownloadClientModalContent extends Component {
</Button>
}
<AdvancedSettingsButton
advancedSettings={advancedSettings}
onAdvancedSettingsPress={onAdvancedSettingsPress}
showLabel={false}
/>
<SpinnerErrorButton
isSpinning={isTesting}
error={saveError}
@@ -239,6 +247,7 @@ EditDownloadClientModalContent.propTypes = {
onModalClose: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onTestPress: PropTypes.func.isRequired,
onAdvancedSettingsPress: PropTypes.func.isRequired,
onDeleteDownloadClientPress: PropTypes.func
};

View File

@@ -2,7 +2,13 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { saveDownloadClient, setDownloadClientFieldValue, setDownloadClientValue, testDownloadClient } from 'Store/Actions/settingsActions';
import {
saveDownloadClient,
setDownloadClientFieldValue,
setDownloadClientValue,
testDownloadClient,
toggleAdvancedSettings
} from 'Store/Actions/settingsActions';
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
import EditDownloadClientModalContent from './EditDownloadClientModalContent';
@@ -23,7 +29,8 @@ const mapDispatchToProps = {
setDownloadClientValue,
setDownloadClientFieldValue,
saveDownloadClient,
testDownloadClient
testDownloadClient,
toggleAdvancedSettings
};
class EditDownloadClientModalContentConnector extends Component {
@@ -56,6 +63,10 @@ class EditDownloadClientModalContentConnector extends Component {
this.props.testDownloadClient({ id: this.props.id });
};
onAdvancedSettingsPress = () => {
this.props.toggleAdvancedSettings();
};
//
// Render
@@ -65,6 +76,7 @@ class EditDownloadClientModalContentConnector extends Component {
{...this.props}
onSavePress={this.onSavePress}
onTestPress={this.onTestPress}
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
onInputChange={this.onInputChange}
onFieldChange={this.onFieldChange}
/>
@@ -82,6 +94,7 @@ EditDownloadClientModalContentConnector.propTypes = {
setDownloadClientFieldValue: PropTypes.func.isRequired,
saveDownloadClient: PropTypes.func.isRequired,
testDownloadClient: PropTypes.func.isRequired,
toggleAdvancedSettings: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

View File

@@ -32,7 +32,7 @@ const enableOptions = [
get value() {
return translate('NoChange');
},
disabled: true,
isDisabled: true,
},
{
key: 'enabled',

View File

@@ -1,4 +1,3 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
@@ -55,7 +54,7 @@ function createRemotePathMappingSelector() {
items
} = remotePathMappings;
const mapping = id ? _.find(items, { id }) : newRemotePathMapping;
const mapping = id ? items.find((i) => i.id === id) : newRemotePathMapping;
const settings = selectSettings(mapping, pendingChanges, saveError);
return {

View File

@@ -1,4 +1,3 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
@@ -7,7 +6,7 @@ import { saveImportExclusion, setImportExclusionValue } from 'Store/Actions/sett
import selectSettings from 'Store/Selectors/selectSettings';
import EditImportListExclusionModalContent from './EditImportListExclusionModalContent';
const newImportExclusion = {
const newImportListExclusion = {
movieTitle: '',
tmdbId: 0,
movieYear: 0
@@ -27,7 +26,7 @@ function createImportExclusionSelector() {
items
} = importExclusions;
const mapping = id ? _.find(items, { id }) : newImportExclusion;
const mapping = id ? items.find((i) => i.id === id) : newImportListExclusion;
const settings = selectSettings(mapping, pendingChanges, saveError);
return {
@@ -66,10 +65,10 @@ class EditImportExclusionModalContentConnector extends Component {
componentDidMount() {
if (!this.props.id) {
Object.keys(newImportExclusion).forEach((name) => {
Object.keys(newImportListExclusion).forEach((name) => {
this.props.setImportExclusionValue({
name,
value: newImportExclusion[name]
value: newImportListExclusion[name]
});
});
}

View File

@@ -10,7 +10,7 @@
.movieTitle {
@add-mixin truncate;
flex: 0 0 600px;
flex: 0 1 600px;
}
.tmdbId,

View File

@@ -67,7 +67,7 @@ class ImportListExclusion extends Component {
)}
>
<div className={styles.tmdbId}>{tmdbId}</div>
<div className={styles.movieTitle}>{movieTitle}</div>
<div className={styles.movieTitle} title={movieTitle}>{movieTitle}</div>
<div className={styles.movieYear}>{movieYear}</div>
<div className={styles.actions}>

View File

@@ -5,7 +5,7 @@
}
.title {
flex: 0 0 600px;
flex: 0 1 600px;
}
.tmdbId,

View File

@@ -14,6 +14,7 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props';
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan';
import translate from 'Utilities/String/translate';
import styles from './EditImportListModalContent.css';
@@ -33,6 +34,7 @@ function EditImportListModalContent(props) {
onModalClose,
onSavePress,
onTestPress,
onAdvancedSettingsPress,
onDeleteImportListPress,
...otherProps
} = props;
@@ -234,6 +236,12 @@ function EditImportListModalContent(props) {
</Button>
}
<AdvancedSettingsButton
advancedSettings={advancedSettings}
onAdvancedSettingsPress={onAdvancedSettingsPress}
showLabel={false}
/>
<SpinnerErrorButton
isSpinning={isTesting}
error={saveError}
@@ -274,6 +282,7 @@ EditImportListModalContent.propTypes = {
onModalClose: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onTestPress: PropTypes.func.isRequired,
onAdvancedSettingsPress: PropTypes.func.isRequired,
onDeleteImportListPress: PropTypes.func
};

View File

@@ -2,7 +2,13 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { saveImportList, setImportListFieldValue, setImportListValue, testImportList } from 'Store/Actions/settingsActions';
import {
saveImportList,
setImportListFieldValue,
setImportListValue,
testImportList,
toggleAdvancedSettings
} from 'Store/Actions/settingsActions';
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
import EditImportListModalContent from './EditImportListModalContent';
@@ -33,7 +39,8 @@ const mapDispatchToProps = {
setImportListValue,
setImportListFieldValue,
saveImportList,
testImportList
testImportList,
toggleAdvancedSettings
};
class EditImportListModalContentConnector extends Component {
@@ -66,6 +73,10 @@ class EditImportListModalContentConnector extends Component {
this.props.testImportList({ id: this.props.id });
};
onAdvancedSettingsPress = () => {
this.props.toggleAdvancedSettings();
};
//
// Render
@@ -75,6 +86,7 @@ class EditImportListModalContentConnector extends Component {
{...this.props}
onSavePress={this.onSavePress}
onTestPress={this.onTestPress}
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
onInputChange={this.onInputChange}
onFieldChange={this.onFieldChange}
/>
@@ -92,6 +104,7 @@ EditImportListModalContentConnector.propTypes = {
setImportListFieldValue: PropTypes.func.isRequired,
saveImportList: PropTypes.func.isRequired,
testImportList: PropTypes.func.isRequired,
toggleAdvancedSettings: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

View File

@@ -41,7 +41,7 @@ class ImportList extends Component {
});
};
onDeleteImportListModalClose= () => {
onDeleteImportListModalClose = () => {
this.setState({ isDeleteImportListModalOpen: false });
};

View File

@@ -33,7 +33,7 @@ const enableOptions = [
get value() {
return translate('NoChange');
},
disabled: true,
isDisabled: true,
},
{
key: 'enabled',

View File

@@ -39,7 +39,7 @@ function ImportListOptions(props) {
{
!isFetching && error &&
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadListOptions')}
{translate('ListOptionsLoadError')}
</Alert>
}

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React from 'react';
import Alert from 'Components/Alert';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
@@ -66,9 +67,9 @@ function EditIndexerModalContent(props) {
{
!isFetching && !!error &&
<div>
<Alert kind={kinds.DANGER}>
{translate('AddIndexerError')}
</div>
</Alert>
}
{

View File

@@ -42,7 +42,7 @@ class Indexer extends Component {
});
};
onDeleteIndexerModalClose= () => {
onDeleteIndexerModalClose = () => {
this.setState({ isDeleteIndexerModalOpen: false });
};

View File

@@ -32,7 +32,7 @@ const enableOptions = [
get value() {
return translate('NoChange');
},
disabled: true,
isDisabled: true,
},
{
key: 'enabled',

View File

@@ -17,7 +17,7 @@
}
.small {
width: 480px;
width: 490px;
}
.large {
@@ -26,8 +26,8 @@
.token {
flex: 0 0 50%;
padding: 6px 16px;
background-color: var(--popoverTitleBorderColor);
padding: 6px;
background-color: var(--popoverTitleBackgroundColor);
font-family: $monoSpaceFontFamily;
}
@@ -37,8 +37,8 @@
align-self: stretch;
justify-content: space-between;
flex: 0 0 50%;
padding: 6px 16px;
background-color: var(--popoverTitleBackgroundColor);
padding: 6px;
background-color: var(--popoverBodyBackgroundColor);
.footNote {
padding: 2px;

View File

@@ -14,6 +14,7 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props';
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
import translate from 'Utilities/String/translate';
import NotificationEventItems from './NotificationEventItems';
import styles from './EditNotificationModalContent.css';
@@ -32,6 +33,7 @@ function EditNotificationModalContent(props) {
onModalClose,
onSavePress,
onTestPress,
onAdvancedSettingsPress,
onDeleteNotificationPress,
...otherProps
} = props;
@@ -136,6 +138,12 @@ function EditNotificationModalContent(props) {
</Button>
}
<AdvancedSettingsButton
advancedSettings={advancedSettings}
onAdvancedSettingsPress={onAdvancedSettingsPress}
showLabel={false}
/>
<SpinnerErrorButton
isSpinning={isTesting}
error={saveError}
@@ -175,6 +183,7 @@ EditNotificationModalContent.propTypes = {
onModalClose: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onTestPress: PropTypes.func.isRequired,
onAdvancedSettingsPress: PropTypes.func.isRequired,
onDeleteNotificationPress: PropTypes.func
};

View File

@@ -2,7 +2,13 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { saveNotification, setNotificationFieldValue, setNotificationValue, testNotification } from 'Store/Actions/settingsActions';
import {
saveNotification,
setNotificationFieldValue,
setNotificationValue,
testNotification,
toggleAdvancedSettings
} from 'Store/Actions/settingsActions';
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
import EditNotificationModalContent from './EditNotificationModalContent';
@@ -23,7 +29,8 @@ const mapDispatchToProps = {
setNotificationValue,
setNotificationFieldValue,
saveNotification,
testNotification
testNotification,
toggleAdvancedSettings
};
class EditNotificationModalContentConnector extends Component {
@@ -56,6 +63,10 @@ class EditNotificationModalContentConnector extends Component {
this.props.testNotification({ id: this.props.id });
};
onAdvancedSettingsPress = () => {
this.props.toggleAdvancedSettings();
};
//
// Render
@@ -65,6 +76,7 @@ class EditNotificationModalContentConnector extends Component {
{...this.props}
onSavePress={this.onSavePress}
onTestPress={this.onTestPress}
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
onInputChange={this.onInputChange}
onFieldChange={this.onFieldChange}
/>
@@ -82,6 +94,7 @@ EditNotificationModalContentConnector.propTypes = {
setNotificationFieldValue: PropTypes.func.isRequired,
saveNotification: PropTypes.func.isRequired,
testNotification: PropTypes.func.isRequired,
toggleAdvancedSettings: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

View File

@@ -191,7 +191,7 @@ class Notification extends Component {
}
{
!onGrab && !onDownload && !onRename && !onHealthIssue && !onHealthRestored && !onApplicationUpdate && !onMovieDelete && !onMovieFileDelete && !onManualInteractionRequired ?
!onGrab && !onDownload && !onRename && !onHealthIssue && !onHealthRestored && !onApplicationUpdate && !onMovieAdded && !onMovieDelete && !onMovieFileDelete && !onManualInteractionRequired ?
<Label
kind={kinds.DISABLED}
outline={true}

View File

@@ -1,4 +1,3 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
@@ -33,7 +32,7 @@ function createDelayProfileSelector() {
items
} = delayProfiles;
const profile = id ? _.find(items, { id }) : newDelayProfile;
const profile = id ? items.find((i) => i.id === id) : newDelayProfile;
const settings = selectSettings(profile, pendingChanges, saveError);
return {

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
@@ -157,9 +158,9 @@ class EditQualityProfileModalContent extends Component {
{
!isFetching && !!error &&
<div>
<Alert kind={kinds.DANGER}>
{translate('AddQualityProfileError')}
</div>
</Alert>
}
{

View File

@@ -55,7 +55,7 @@ class QualityProfiles extends Component {
<FieldSet legend={translate('QualityProfiles')}>
<PageSectionContent
errorMessage={translate('QualityProfilesLoadError')}
{...otherProps}c={true}
{...otherProps}
>
<div className={styles.qualityProfiles}>
{

View File

@@ -41,7 +41,7 @@ class ReleaseProfile extends Component {
});
};
onDeleteReleaseProfileModalClose= () => {
onDeleteReleaseProfileModalClose = () => {
this.setState({ isDeleteReleaseProfileModalOpen: false });
};
@@ -70,7 +70,7 @@ class ReleaseProfile extends Component {
isDeleteReleaseProfileModalOpen
} = this.state;
const indexer = indexerList.find((i) => i.id === indexerId);
const indexer = indexerId !== 0 && indexerList.find((i) => i.id === indexerId);
return (
<Card

View File

@@ -3,7 +3,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchDelayProfiles, fetchDownloadClients, fetchImportLists, fetchIndexers, fetchNotifications, fetchReleaseProfiles } from 'Store/Actions/settingsActions';
import { fetchTagDetails } from 'Store/Actions/tagActions';
import { fetchTagDetails, fetchTags } from 'Store/Actions/tagActions';
import Tags from './Tags';
function createMapStateToProps() {
@@ -25,6 +25,7 @@ function createMapStateToProps() {
}
const mapDispatchToProps = {
dispatchFetchTags: fetchTags,
dispatchFetchTagDetails: fetchTagDetails,
dispatchFetchDelayProfiles: fetchDelayProfiles,
dispatchFetchNotifications: fetchNotifications,
@@ -41,6 +42,7 @@ class MetadatasConnector extends Component {
componentDidMount() {
const {
dispatchFetchTags,
dispatchFetchTagDetails,
dispatchFetchDelayProfiles,
dispatchFetchNotifications,
@@ -50,6 +52,7 @@ class MetadatasConnector extends Component {
dispatchFetchDownloadClients
} = this.props;
dispatchFetchTags();
dispatchFetchTagDetails();
dispatchFetchDelayProfiles();
dispatchFetchNotifications();
@@ -72,6 +75,7 @@ class MetadatasConnector extends Component {
}
MetadatasConnector.propTypes = {
dispatchFetchTags: PropTypes.func.isRequired,
dispatchFetchTagDetails: PropTypes.func.isRequired,
dispatchFetchDelayProfiles: PropTypes.func.isRequired,
dispatchFetchNotifications: PropTypes.func.isRequired,

View File

@@ -31,19 +31,19 @@ export const firstDayOfWeekOptions = [
];
export const weekColumnOptions = [
{ key: 'ddd M/D', value: 'Tue 3/25' },
{ key: 'ddd MM/DD', value: 'Tue 03/25' },
{ key: 'ddd D/M', value: 'Tue 25/3' },
{ key: 'ddd DD/MM', value: 'Tue 25/03' }
{ key: 'ddd M/D', value: 'Tue 3/25', hint: 'ddd M/D' },
{ key: 'ddd MM/DD', value: 'Tue 03/25', hint: 'ddd MM/DD' },
{ key: 'ddd D/M', value: 'Tue 25/3', hint: 'ddd D/M' },
{ key: 'ddd DD/MM', value: 'Tue 25/03', hint: 'ddd DD/MM' }
];
const shortDateFormatOptions = [
{ key: 'MMM D YYYY', value: 'Mar 25 2014' },
{ key: 'DD MMM YYYY', value: '25 Mar 2014' },
{ key: 'MM/D/YYYY', value: '03/25/2014' },
{ key: 'MM/DD/YYYY', value: '03/25/2014' },
{ key: 'DD/MM/YYYY', value: '25/03/2014' },
{ key: 'YYYY-MM-DD', value: '2014-03-25' }
{ key: 'MMM D YYYY', value: 'Mar 25 2014', hint: 'MMM D YYYY' },
{ key: 'DD MMM YYYY', value: '25 Mar 2014', hint: 'DD MMM YYYY' },
{ key: 'MM/D/YYYY', value: '03/25/2014', hint: 'MM/D/YYYY' },
{ key: 'MM/DD/YYYY', value: '03/25/2014', hint: 'MM/DD/YYYY' },
{ key: 'DD/MM/YYYY', value: '25/03/2014', hint: 'DD/MM/YYYY' },
{ key: 'YYYY-MM-DD', value: '2014-03-25', hint: 'YYYY-MM-DD' }
];
const longDateFormatOptions = [

View File

@@ -83,21 +83,21 @@ export const defaultState = {
},
{
name: 'isRecommendation',
columnLabel: 'Recommedation',
columnLabel: () => translate('Recommendation'),
isSortable: true,
isVisible: true,
isModifiable: false
},
{
name: 'isTrending',
columnLabel: 'Trending',
columnLabel: () => translate('Trending'),
isSortable: true,
isVisible: true,
isModifiable: false
},
{
name: 'isPopular',
columnLabel: 'Popular',
columnLabel: () => translate('Popular'),
isSortable: true,
isVisible: true,
isModifiable: false
@@ -177,7 +177,7 @@ export const defaultState = {
},
{
name: 'lists',
label: 'Lists',
label: () => translate('Lists'),
isSortable: false,
isVisible: false
},
@@ -283,7 +283,7 @@ export const defaultState = {
},
{
key: 'popular',
label: 'Popular',
label: () => translate('Popular'),
filters: [
{
key: 'isPopular',
@@ -294,7 +294,7 @@ export const defaultState = {
},
{
key: 'trending',
label: 'Trending',
label: () => translate('Trending'),
filters: [
{
key: 'isTrending',
@@ -305,7 +305,7 @@ export const defaultState = {
},
{
key: 'newNotExcluded',
label: 'New Non-Excluded',
label: () => translate('NewNonExcluded'),
filters: [
{
key: 'isExisting',
@@ -332,7 +332,7 @@ export const defaultState = {
},
{
name: 'studio',
label: 'Studio',
label: () => translate('Studio'),
type: filterBuilderTypes.ARRAY,
optionsSelector: function(items) {
const tagList = items.reduce((acc, movie) => {
@@ -387,19 +387,19 @@ export const defaultState = {
},
{
name: 'inCinemas',
label: 'In Cinemas',
label: () => translate('InCinemas'),
type: filterBuilderTypes.DATE,
valueType: filterBuilderValueTypes.DATE
},
{
name: 'physicalRelease',
label: 'Physical Release',
label: () => translate('PhysicalRelease'),
type: filterBuilderTypes.DATE,
valueType: filterBuilderValueTypes.DATE
},
{
name: 'digitalRelease',
label: 'Digital Release',
label: () => translate('DigitalRelease'),
type: filterBuilderTypes.DATE,
valueType: filterBuilderValueTypes.DATE
},
@@ -410,7 +410,7 @@ export const defaultState = {
},
{
name: 'genres',
label: 'Genres',
label: () => translate('Genres'),
type: filterBuilderTypes.ARRAY,
optionsSelector: function(items) {
const tagList = items.reduce((acc, movie) => {
@@ -466,42 +466,42 @@ export const defaultState = {
},
{
name: 'certification',
label: 'Certification',
label: () => translate('Certification'),
type: filterBuilderTypes.EXACT
},
{
name: 'lists',
label: 'Lists',
label: () => translate('Lists'),
type: filterBuilderTypes.ARRAY,
valueType: filterBuilderValueTypes.IMPORTLIST
},
{
name: 'isExcluded',
label: 'On Excluded List',
label: () => translate('OnExcludedList'),
type: filterBuilderTypes.EXACT,
valueType: filterBuilderValueTypes.BOOL
},
{
name: 'isExisting',
label: 'Exists in Library',
label: () => translate('ExistsInLibrary'),
type: filterBuilderTypes.EXACT,
valueType: filterBuilderValueTypes.BOOL
},
{
name: 'isRecommendation',
label: 'Recommended',
label: () => translate('Recommended'),
type: filterBuilderTypes.EXACT,
valueType: filterBuilderValueTypes.BOOL
},
{
name: 'isTrending',
label: 'Trending',
label: () => translate('Trending'),
type: filterBuilderTypes.EXACT,
valueType: filterBuilderValueTypes.BOOL
},
{
name: 'isPopular',
label: 'Popular',
label: () => translate('Popular'),
type: filterBuilderTypes.EXACT,
valueType: filterBuilderValueTypes.BOOL
}

View File

@@ -159,6 +159,7 @@ export const actionHandlers = handleThunks({
quality: item.quality,
languages: item.languages,
releaseGroup: item.releaseGroup,
indexerFlags: item.indexerFlags,
downloadId: item.downloadId
};
});

View File

@@ -4,8 +4,9 @@ import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions';
import Icon from 'Components/Icon';
import IconButton from 'Components/Link/IconButton';
import { icons } from 'Helpers/Props';
import { icons, sortDirections } from 'Helpers/Props';
import movieEntities from 'Movie/movieEntities';
import createSetClientSideCollectionSortReducer from 'Store/Actions/Creators/Reducers/createSetClientSideCollectionSortReducer';
import createSetTableOptionReducer from 'Store/Actions/Creators/Reducers/createSetTableOptionReducer';
import { createThunk, handleThunks } from 'Store/thunks';
import createAjaxRequest from 'Utilities/createAjaxRequest';
@@ -31,13 +32,16 @@ export const defaultState = {
deleteError: null,
isSaving: false,
saveError: null,
sortKey: 'relativePath',
sortDirection: sortDirections.ASCENDING,
items: [],
columns: [
{
name: 'relativePath',
label: () => translate('RelativePath'),
isVisible: true
isVisible: true,
isSortable: true
},
{
name: 'videoCodec',
@@ -67,7 +71,8 @@ export const defaultState = {
{
name: 'size',
label: () => translate('Size'),
isVisible: true
isVisible: true,
isSortable: true
},
{
name: 'languages',
@@ -96,12 +101,23 @@ export const defaultState = {
name: icons.SCORE,
title: () => translate('CustomFormatScore')
}),
isVisible: true
isVisible: true,
isSortable: true
},
{
name: 'indexerFlags',
columnLabel: () => translate('IndexerFlags'),
label: React.createElement(Icon, {
name: icons.FLAG,
title: () => translate('IndexerFlags')
}),
isVisible: false
},
{
name: 'dateAdded',
label: () => translate('Added'),
isVisible: false
isVisible: false,
isSortable: true
},
{
name: 'actions',
@@ -114,7 +130,9 @@ export const defaultState = {
};
export const persistState = [
'movieFiles.columns'
'movieFiles.columns',
'movieFiles.sortDirection',
'movieFiles.sortKey'
];
//
@@ -125,6 +143,7 @@ export const DELETE_MOVIE_FILE = 'movieFiles/deleteMovieFile';
export const DELETE_MOVIE_FILES = 'movieFiles/deleteMovieFiles';
export const UPDATE_MOVIE_FILES = 'movieFiles/updateMovieFiles';
export const CLEAR_MOVIE_FILES = 'movieFiles/clearMovieFiles';
export const SET_MOVIE_FILES_SORT = 'movieFiles/setMovieFilesSort';
export const SET_MOVIE_FILES_TABLE_OPTION = 'movieFiles/setMovieFilesTableOption';
//
@@ -135,6 +154,7 @@ export const deleteMovieFile = createThunk(DELETE_MOVIE_FILE);
export const deleteMovieFiles = createThunk(DELETE_MOVIE_FILES);
export const updateMovieFiles = createThunk(UPDATE_MOVIE_FILES);
export const clearMovieFiles = createAction(CLEAR_MOVIE_FILES);
export const setMovieFilesSort = createAction(SET_MOVIE_FILES_SORT);
export const setMovieFilesTableOption = createAction(SET_MOVIE_FILES_TABLE_OPTION);
//
@@ -327,6 +347,7 @@ export const actionHandlers = handleThunks({
// Reducers
export const reducers = createHandleActions({
[SET_MOVIE_FILES_TABLE_OPTION]: createSetTableOptionReducer(section),
[CLEAR_MOVIE_FILES]: (state) => {
@@ -340,6 +361,8 @@ export const reducers = createHandleActions({
saveError: null,
items: []
});
}
},
[SET_MOVIE_FILES_SORT]: createSetClientSideCollectionSortReducer(section)
}, defaultState, section);

View File

@@ -0,0 +1,23 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import Movie from 'Movie/Movie';
function createMultiMoviesSelector(movieIds: number[]) {
return createSelector(
(state: AppState) => state.movies.itemMap,
(state: AppState) => state.movies.items,
(itemMap, allMovies) => {
return movieIds.reduce((acc: Movie[], movieId) => {
const movie = allMovies[itemMap[movieId]];
if (movie) {
acc.push(movie);
}
return acc;
}, []);
}
);
}
export default createMultiMoviesSelector;

View File

@@ -10,15 +10,6 @@
width: 100%;
}
.commandName {
display: inline-block;
min-width: 220px;
}
.userAgent {
color: #b0b0b0;
}
.queued,
.started,
.ended {

View File

@@ -2,14 +2,12 @@
// Please do not change this file!
interface CssExports {
'actions': string;
'commandName': string;
'duration': string;
'ended': string;
'queued': string;
'started': string;
'trigger': string;
'triggerContent': string;
'userAgent': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -1,279 +0,0 @@
import moment from 'moment';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Icon from 'Components/Icon';
import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import { icons, kinds } from 'Helpers/Props';
import formatDate from 'Utilities/Date/formatDate';
import formatDateTime from 'Utilities/Date/formatDateTime';
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
import titleCase from 'Utilities/String/titleCase';
import translate from 'Utilities/String/translate';
import styles from './QueuedTaskRow.css';
function getStatusIconProps(status, message) {
const title = titleCase(status);
switch (status) {
case 'queued':
return {
name: icons.PENDING,
title
};
case 'started':
return {
name: icons.REFRESH,
isSpinning: true,
title
};
case 'completed':
return {
name: icons.CHECK,
kind: kinds.SUCCESS,
title: message === 'Completed' ? title : `${title}: ${message}`
};
case 'failed':
return {
name: icons.FATAL,
kind: kinds.DANGER,
title: `${title}: ${message}`
};
default:
return {
name: icons.UNKNOWN,
title
};
}
}
function getFormattedDates(props) {
const {
queued,
started,
ended,
showRelativeDates,
shortDateFormat
} = props;
if (showRelativeDates) {
return {
queuedAt: moment(queued).fromNow(),
startedAt: started ? moment(started).fromNow() : '-',
endedAt: ended ? moment(ended).fromNow() : '-'
};
}
return {
queuedAt: formatDate(queued, shortDateFormat),
startedAt: started ? formatDate(started, shortDateFormat) : '-',
endedAt: ended ? formatDate(ended, shortDateFormat) : '-'
};
}
class QueuedTaskRow extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
...getFormattedDates(props),
isCancelConfirmModalOpen: false
};
this._updateTimeoutId = null;
}
componentDidMount() {
this.setUpdateTimer();
}
componentDidUpdate(prevProps) {
const {
queued,
started,
ended
} = this.props;
if (
queued !== prevProps.queued ||
started !== prevProps.started ||
ended !== prevProps.ended
) {
this.setState(getFormattedDates(this.props));
}
}
componentWillUnmount() {
if (this._updateTimeoutId) {
this._updateTimeoutId = clearTimeout(this._updateTimeoutId);
}
}
//
// Control
setUpdateTimer() {
this._updateTimeoutId = setTimeout(() => {
this.setState(getFormattedDates(this.props));
this.setUpdateTimer();
}, 30000);
}
//
// Listeners
onCancelPress = () => {
this.setState({
isCancelConfirmModalOpen: true
});
};
onAbortCancel = () => {
this.setState({
isCancelConfirmModalOpen: false
});
};
//
// Render
render() {
const {
trigger,
commandName,
queued,
started,
ended,
status,
duration,
message,
clientUserAgent,
longDateFormat,
timeFormat,
onCancelPress
} = this.props;
const {
queuedAt,
startedAt,
endedAt,
isCancelConfirmModalOpen
} = this.state;
let triggerIcon = icons.QUICK;
if (trigger === 'manual') {
triggerIcon = icons.INTERACTIVE;
} else if (trigger === 'scheduled') {
triggerIcon = icons.SCHEDULED;
}
return (
<TableRow>
<TableRowCell className={styles.trigger}>
<span className={styles.triggerContent}>
<Icon
name={triggerIcon}
title={titleCase(trigger)}
/>
<Icon
{...getStatusIconProps(status, message)}
/>
</span>
</TableRowCell>
<TableRowCell>
<span className={styles.commandName}>
{commandName}
</span>
{
clientUserAgent ?
<span className={styles.userAgent} title={translate('TaskUserAgentTooltip')}>
{translate('From')}: {clientUserAgent}
</span> :
null
}
</TableRowCell>
<TableRowCell
className={styles.queued}
title={formatDateTime(queued, longDateFormat, timeFormat)}
>
{queuedAt}
</TableRowCell>
<TableRowCell
className={styles.started}
title={formatDateTime(started, longDateFormat, timeFormat)}
>
{startedAt}
</TableRowCell>
<TableRowCell
className={styles.ended}
title={formatDateTime(ended, longDateFormat, timeFormat)}
>
{endedAt}
</TableRowCell>
<TableRowCell className={styles.duration}>
{formatTimeSpan(duration)}
</TableRowCell>
<TableRowCell
className={styles.actions}
>
{
status === 'queued' &&
<IconButton
title={translate('RemovedFromTaskQueue')}
name={icons.REMOVE}
onPress={this.onCancelPress}
/>
}
</TableRowCell>
<ConfirmModal
isOpen={isCancelConfirmModalOpen}
kind={kinds.DANGER}
title={translate('Cancel')}
message={translate('CancelPendingTask')}
confirmLabel={translate('YesCancel')}
cancelLabel={translate('NoLeaveIt')}
onConfirm={onCancelPress}
onCancel={this.onAbortCancel}
/>
</TableRow>
);
}
}
QueuedTaskRow.propTypes = {
trigger: PropTypes.string.isRequired,
commandName: PropTypes.string.isRequired,
queued: PropTypes.string.isRequired,
started: PropTypes.string,
ended: PropTypes.string,
status: PropTypes.string.isRequired,
duration: PropTypes.string,
message: PropTypes.string,
clientUserAgent: PropTypes.string,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
onCancelPress: PropTypes.func.isRequired
};
export default QueuedTaskRow;

View File

@@ -0,0 +1,238 @@
import moment from 'moment';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { CommandBody } from 'Commands/Command';
import Icon from 'Components/Icon';
import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
import { icons, kinds } from 'Helpers/Props';
import { cancelCommand } from 'Store/Actions/commandActions';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import formatDate from 'Utilities/Date/formatDate';
import formatDateTime from 'Utilities/Date/formatDateTime';
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
import titleCase from 'Utilities/String/titleCase';
import translate from 'Utilities/String/translate';
import QueuedTaskRowNameCell from './QueuedTaskRowNameCell';
import styles from './QueuedTaskRow.css';
function getStatusIconProps(status: string, message: string | undefined) {
const title = titleCase(status);
switch (status) {
case 'queued':
return {
name: icons.PENDING,
title,
};
case 'started':
return {
name: icons.REFRESH,
isSpinning: true,
title,
};
case 'completed':
return {
name: icons.CHECK,
kind: kinds.SUCCESS,
title: message === 'Completed' ? title : `${title}: ${message}`,
};
case 'failed':
return {
name: icons.FATAL,
kind: kinds.DANGER,
title: `${title}: ${message}`,
};
default:
return {
name: icons.UNKNOWN,
title,
};
}
}
function getFormattedDates(
queued: string,
started: string | undefined,
ended: string | undefined,
showRelativeDates: boolean,
shortDateFormat: string
) {
if (showRelativeDates) {
return {
queuedAt: moment(queued).fromNow(),
startedAt: started ? moment(started).fromNow() : '-',
endedAt: ended ? moment(ended).fromNow() : '-',
};
}
return {
queuedAt: formatDate(queued, shortDateFormat),
startedAt: started ? formatDate(started, shortDateFormat) : '-',
endedAt: ended ? formatDate(ended, shortDateFormat) : '-',
};
}
interface QueuedTimes {
queuedAt: string;
startedAt: string;
endedAt: string;
}
export interface QueuedTaskRowProps {
id: number;
trigger: string;
commandName: string;
queued: string;
started?: string;
ended?: string;
status: string;
duration?: string;
message?: string;
body: CommandBody;
clientUserAgent?: string;
}
export default function QueuedTaskRow(props: QueuedTaskRowProps) {
const {
id,
trigger,
commandName,
queued,
started,
ended,
status,
duration,
message,
body,
clientUserAgent,
} = props;
const dispatch = useDispatch();
const { longDateFormat, shortDateFormat, showRelativeDates, timeFormat } =
useSelector(createUISettingsSelector());
const updateTimeTimeoutId = useRef<ReturnType<typeof setTimeout> | null>(
null
);
const [times, setTimes] = useState<QueuedTimes>(
getFormattedDates(
queued,
started,
ended,
showRelativeDates,
shortDateFormat
)
);
const [
isCancelConfirmModalOpen,
openCancelConfirmModal,
closeCancelConfirmModal,
] = useModalOpenState(false);
const handleCancelPress = useCallback(() => {
dispatch(cancelCommand({ id }));
}, [id, dispatch]);
useEffect(() => {
updateTimeTimeoutId.current = setTimeout(() => {
setTimes(
getFormattedDates(
queued,
started,
ended,
showRelativeDates,
shortDateFormat
)
);
}, 30000);
return () => {
if (updateTimeTimeoutId.current) {
clearTimeout(updateTimeTimeoutId.current);
}
};
}, [queued, started, ended, showRelativeDates, shortDateFormat, setTimes]);
const { queuedAt, startedAt, endedAt } = times;
let triggerIcon = icons.QUICK;
if (trigger === 'manual') {
triggerIcon = icons.INTERACTIVE;
} else if (trigger === 'scheduled') {
triggerIcon = icons.SCHEDULED;
}
return (
<TableRow>
<TableRowCell className={styles.trigger}>
<span className={styles.triggerContent}>
<Icon name={triggerIcon} title={titleCase(trigger)} />
<Icon {...getStatusIconProps(status, message)} />
</span>
</TableRowCell>
<QueuedTaskRowNameCell
commandName={commandName}
body={body}
clientUserAgent={clientUserAgent}
/>
<TableRowCell
className={styles.queued}
title={formatDateTime(queued, longDateFormat, timeFormat)}
>
{queuedAt}
</TableRowCell>
<TableRowCell
className={styles.started}
title={formatDateTime(started, longDateFormat, timeFormat)}
>
{startedAt}
</TableRowCell>
<TableRowCell
className={styles.ended}
title={formatDateTime(ended, longDateFormat, timeFormat)}
>
{endedAt}
</TableRowCell>
<TableRowCell className={styles.duration}>
{formatTimeSpan(duration)}
</TableRowCell>
<TableRowCell className={styles.actions}>
{status === 'queued' && (
<IconButton
title={translate('RemovedFromTaskQueue')}
name={icons.REMOVE}
onPress={openCancelConfirmModal}
/>
)}
</TableRowCell>
<ConfirmModal
isOpen={isCancelConfirmModalOpen}
kind={kinds.DANGER}
title={translate('Cancel')}
message={translate('CancelPendingTask')}
confirmLabel={translate('YesCancel')}
cancelLabel={translate('NoLeaveIt')}
onConfirm={handleCancelPress}
onCancel={closeCancelConfirmModal}
/>
</TableRow>
);
}

View File

@@ -1,31 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { cancelCommand } from 'Store/Actions/commandActions';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import QueuedTaskRow from './QueuedTaskRow';
function createMapStateToProps() {
return createSelector(
createUISettingsSelector(),
(uiSettings) => {
return {
showRelativeDates: uiSettings.showRelativeDates,
shortDateFormat: uiSettings.shortDateFormat,
longDateFormat: uiSettings.longDateFormat,
timeFormat: uiSettings.timeFormat
};
}
);
}
function createMapDispatchToProps(dispatch, props) {
return {
onCancelPress() {
dispatch(cancelCommand({
id: props.id
}));
}
};
}
export default connect(createMapStateToProps, createMapDispatchToProps)(QueuedTaskRow);

View File

@@ -0,0 +1,8 @@
.commandName {
display: inline-block;
min-width: 220px;
}
.userAgent {
color: #b0b0b0;
}

View File

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

View File

@@ -0,0 +1,49 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { CommandBody } from 'Commands/Command';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import createMultiMoviesSelector from 'Store/Selectors/createMultiMoviesSelector';
import translate from 'Utilities/String/translate';
import styles from './QueuedTaskRowNameCell.css';
export interface QueuedTaskRowNameCellProps {
commandName: string;
body: CommandBody;
clientUserAgent?: string;
}
export default function QueuedTaskRowNameCell(
props: QueuedTaskRowNameCellProps
) {
const { commandName, body, clientUserAgent } = props;
const movieIds = [...(body.movieIds ?? [])];
if (body.movieId) {
movieIds.push(body.movieId);
}
const movies = useSelector(createMultiMoviesSelector(movieIds));
const sortedMovies = movies.sort((a, b) =>
a.sortTitle.localeCompare(b.sortTitle)
);
return (
<TableRowCell>
<span className={styles.commandName}>
{commandName}
{sortedMovies.length ? (
<span> - {sortedMovies.map((m) => m.title).join(', ')}</span>
) : null}
</span>
{clientUserAgent ? (
<span
className={styles.userAgent}
title={translate('TaskUserAgentTooltip')}
>
{translate('From')}: {clientUserAgent}
</span>
) : null}
</TableRowCell>
);
}

View File

@@ -1,90 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import FieldSet from 'Components/FieldSet';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import translate from 'Utilities/String/translate';
import QueuedTaskRowConnector from './QueuedTaskRowConnector';
const columns = [
{
name: 'trigger',
label: () => translate('Trigger'),
isVisible: true
},
{
name: 'commandName',
label: () => translate('Name'),
isVisible: true
},
{
name: 'queued',
label: () => translate('Queued'),
isVisible: true
},
{
name: 'started',
label: () => translate('Started'),
isVisible: true
},
{
name: 'ended',
label: () => translate('Ended'),
isVisible: true
},
{
name: 'duration',
label: () => translate('Duration'),
isVisible: true
},
{
name: 'actions',
isVisible: true
}
];
function QueuedTasks(props) {
const {
isFetching,
isPopulated,
items
} = props;
return (
<FieldSet legend={translate('Queue')}>
{
isFetching && !isPopulated &&
<LoadingIndicator />
}
{
isPopulated &&
<Table
columns={columns}
>
<TableBody>
{
items.map((item) => {
return (
<QueuedTaskRowConnector
key={item.id}
{...item}
/>
);
})
}
</TableBody>
</Table>
}
</FieldSet>
);
}
QueuedTasks.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
items: PropTypes.array.isRequired
};
export default QueuedTasks;

View File

@@ -0,0 +1,74 @@
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import FieldSet from 'Components/FieldSet';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { fetchCommands } from 'Store/Actions/commandActions';
import translate from 'Utilities/String/translate';
import QueuedTaskRow from './QueuedTaskRow';
const columns = [
{
name: 'trigger',
label: '',
isVisible: true,
},
{
name: 'commandName',
label: () => translate('Name'),
isVisible: true,
},
{
name: 'queued',
label: () => translate('Queued'),
isVisible: true,
},
{
name: 'started',
label: () => translate('Started'),
isVisible: true,
},
{
name: 'ended',
label: () => translate('Ended'),
isVisible: true,
},
{
name: 'duration',
label: () => translate('Duration'),
isVisible: true,
},
{
name: 'actions',
isVisible: true,
},
];
export default function QueuedTasks() {
const dispatch = useDispatch();
const { isFetching, isPopulated, items } = useSelector(
(state: AppState) => state.commands
);
useEffect(() => {
dispatch(fetchCommands());
}, [dispatch]);
return (
<FieldSet legend={translate('Queue')}>
{isFetching && !isPopulated && <LoadingIndicator />}
{isPopulated && (
<Table columns={columns}>
<TableBody>
{items.map((item) => {
return <QueuedTaskRow key={item.id} {...item} />;
})}
</TableBody>
</Table>
)}
</FieldSet>
);
}

View File

@@ -1,46 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchCommands } from 'Store/Actions/commandActions';
import QueuedTasks from './QueuedTasks';
function createMapStateToProps() {
return createSelector(
(state) => state.commands,
(commands) => {
return commands;
}
);
}
const mapDispatchToProps = {
dispatchFetchCommands: fetchCommands
};
class QueuedTasksConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.dispatchFetchCommands();
}
//
// Render
render() {
return (
<QueuedTasks
{...this.props}
/>
);
}
}
QueuedTasksConnector.propTypes = {
dispatchFetchCommands: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(QueuedTasksConnector);

View File

@@ -2,7 +2,7 @@ import React from 'react';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import translate from 'Utilities/String/translate';
import QueuedTasksConnector from './Queued/QueuedTasksConnector';
import QueuedTasks from './Queued/QueuedTasks';
import ScheduledTasksConnector from './Scheduled/ScheduledTasksConnector';
function Tasks() {
@@ -10,7 +10,7 @@ function Tasks() {
<PageContent title={translate('Tasks')}>
<PageContentBody>
<ScheduledTasksConnector />
<QueuedTasksConnector />
<QueuedTasks />
</PageContentBody>
</PageContent>
);

View File

@@ -18,9 +18,9 @@ export default function getQueueStatusText(queueStatus, queueState) {
status = titleCase(queueStatus);
break;
case 'delay':
case 'downloadClientUnavailable':
status = translate('Pending');
break;
case 'DownloadClientUnavailable':
case 'warning':
status = translate('Error');
break;

View File

@@ -3,13 +3,16 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<!-- Chrome, Safari, Opera, and Firefox OS -->
<meta name="theme-color" content="#595959" />
<!-- Windows Phone -->
<meta name="msapplication-navbutton-color" content="#3a3f51" />
<meta name="msapplication-navbutton-color" content="#464b51" />
<!-- Android/Apple Phone -->
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="format-detection" content="telephone=no">
<meta name="description" content="Radarr" />

View File

@@ -3,13 +3,16 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<!-- Chrome, Safari, Opera, and Firefox OS -->
<meta name="theme-color" content="#595959" />
<!-- Windows Phone -->
<meta name="msapplication-navbutton-color" content="#464b51" />
<!-- Android/Apple Phone -->
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="format-detection" content="telephone=no">
<meta name="description" content="Radarr (Preview)" />

View File

@@ -11,7 +11,7 @@
"lint": "eslint --config frontend/.eslintrc.js --ignore-path frontend/.eslintignore frontend/",
"lint-fix": "yarn lint --fix",
"stylelint-linux": "stylelint $(find frontend -name '*.css') --config frontend/.stylelintrc",
"stylelint-windows": "stylelint frontend/**/*.css --config frontend/.stylelintrc"
"stylelint-windows": "stylelint \"frontend/**/*.css\" --config frontend/.stylelintrc"
},
"repository": "https://github.com/Radarr/Radarr",
"author": "Team Radarr",
@@ -150,7 +150,7 @@
"worker-loader": "3.0.8"
},
"volta": {
"node": "16.17.0",
"node": "20.11.1",
"yarn": "1.22.19"
}
}

View File

@@ -19,6 +19,8 @@ namespace NzbDrone.Common.Test.InstrumentationTests
[TestCase(@"https://baconbits.org/feeds.php?feed=torrents_tv&user=12345&auth=2b51db35e1910123321025a12b9933d2&passkey=mySecret&authkey=2b51db35e1910123321025a12b9933d2")]
[TestCase(@"http://127.0.0.1:9117/dl/indexername?jackett_apikey=flwjiefewklfjacketmySecretsdfldskjfsdlk&path=we0re9f0sdfbase64sfdkfjsdlfjk&file=The+Torrent+File+Name.torrent")]
[TestCase(@"http://nzb.su/getnzb/2b51db35e1912ffc138825a12b9933d2.nzb&i=37292&r=2b51db35e1910123321025a12b9933d2")]
[TestCase(@"https://b-hd.me/torrent/download/auto.343756.is1t1pl127p1sfwur8h4kgyhg1wcsn05")]
[TestCase(@"https://b-hd.me/torrent/download/a-slug-in-the-url.343756.is1t1pl127p1sfwur8h4kgyhg1wcsn05")]
// NzbGet
[TestCase(@"{ ""Name"" : ""ControlUsername"", ""Value"" : ""mySecret"" }, { ""Name"" : ""ControlPassword"", ""Value"" : ""mySecret"" }, ")]

View File

@@ -1,12 +1,15 @@
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.NetworkInformation;
using System.Net.Security;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http.Proxy;
@@ -28,11 +31,14 @@ namespace NzbDrone.Common.Http.Dispatchers
private readonly ICached<System.Net.Http.HttpClient> _httpClientCache;
private readonly ICached<CredentialCache> _credentialCache;
private readonly Logger _logger;
public ManagedHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider,
ICreateManagedWebProxy createManagedWebProxy,
ICertificateValidationService certificateValidationService,
IUserAgentBuilder userAgentBuilder,
ICacheManager cacheManager)
ICacheManager cacheManager,
Logger logger)
{
_proxySettingsProvider = proxySettingsProvider;
_createManagedWebProxy = createManagedWebProxy;
@@ -41,6 +47,8 @@ namespace NzbDrone.Common.Http.Dispatchers
_httpClientCache = cacheManager.GetCache<System.Net.Http.HttpClient>(typeof(ManagedHttpDispatcher));
_credentialCache = cacheManager.GetCache<CredentialCache>(typeof(ManagedHttpDispatcher), "credentialcache");
_logger = logger;
}
public async Task<HttpResponse> GetResponseAsync(HttpRequest request, CookieContainer cookies)
@@ -246,7 +254,27 @@ namespace NzbDrone.Common.Http.Dispatchers
return _credentialCache.Get("credentialCache", () => new CredentialCache());
}
private static async ValueTask<Stream> onConnect(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
private bool HasRoutableIPv4Address()
{
// Get all IPv4 addresses from all interfaces and return true if there are any with non-loopback addresses
try
{
var networkInterfaces = NetworkInterface.GetAllNetworkInterfaces();
return networkInterfaces.Any(ni =>
ni.OperationalStatus == OperationalStatus.Up &&
ni.GetIPProperties().UnicastAddresses.Any(ip =>
ip.Address.AddressFamily == AddressFamily.InterNetwork &&
!IPAddress.IsLoopback(ip.Address)));
}
catch (Exception e)
{
_logger.Debug(e, "Caught exception while GetAllNetworkInterfaces assuming IPv4 connectivity: {0}", e.Message);
return true;
}
}
private async ValueTask<Stream> onConnect(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
{
// Until .NET supports an implementation of Happy Eyeballs (https://tools.ietf.org/html/rfc8305#section-2), let's make IPv4 fallback work in a simple way.
// This issue is being tracked at https://github.com/dotnet/runtime/issues/26177 and expected to be fixed in .NET 6.
@@ -269,10 +297,10 @@ namespace NzbDrone.Common.Http.Dispatchers
}
catch
{
// very naively fallback to ipv4 permanently for this execution based on the response of the first connection attempt.
// note that this may cause users to eventually get switched to ipv4 (on a random failure when they are switching networks, for instance)
// but in the interest of keeping this implementation simple, this is acceptable.
useIPv6 = false;
// Do not retry IPv6 if a routable IPv4 address is available, otherwise continue to attempt IPv6 connections.
var routableIPv4 = HasRoutableIPv4Address();
_logger.Info("IPv4 is available: {0}, IPv6 will be {1}", routableIPv4, routableIPv4 ? "disabled" : "left enabled");
useIPv6 = !routableIPv4;
}
finally
{

View File

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

View File

@@ -25,7 +25,7 @@ namespace NzbDrone.Core.Test.Framework
Mocker.SetConstant<IHttpProxySettingsProvider>(new HttpProxySettingsProvider(Mocker.Resolve<ConfigService>()));
Mocker.SetConstant<ICreateManagedWebProxy>(new ManagedWebProxyFactory(Mocker.Resolve<CacheManager>()));
Mocker.SetConstant<ICertificateValidationService>(new X509CertificateValidationService(Mocker.Resolve<ConfigService>(), TestLogger));
Mocker.SetConstant<IHttpDispatcher>(new ManagedHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<ICreateManagedWebProxy>(), Mocker.Resolve<ICertificateValidationService>(), Mocker.Resolve<UserAgentBuilder>(), Mocker.Resolve<CacheManager>()));
Mocker.SetConstant<IHttpDispatcher>(new ManagedHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<ICreateManagedWebProxy>(), Mocker.Resolve<ICertificateValidationService>(), Mocker.Resolve<UserAgentBuilder>(), Mocker.Resolve<CacheManager>(), TestLogger));
Mocker.SetConstant<IHttpClient>(new HttpClient(Array.Empty<IHttpRequestInterceptor>(), Mocker.Resolve<CacheManager>(), Mocker.Resolve<RateLimitService>(), Mocker.Resolve<IHttpDispatcher>(), TestLogger));
Mocker.SetConstant<IRadarrCloudRequestBuilder>(new RadarrCloudRequestBuilder());
}

View File

@@ -178,6 +178,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
Path.Combine(_movie.Path, "Scenes", "file7.mkv").AsOsAgnostic(),
Path.Combine(_movie.Path, "Shorts", "file8.mkv").AsOsAgnostic(),
Path.Combine(_movie.Path, "Trailers", "file9.mkv").AsOsAgnostic(),
Path.Combine(_movie.Path, "Other", "file9.mkv").AsOsAgnostic(),
Path.Combine(_movie.Path, "The Count of Monte Cristo (2002) (1080p BluRay x265 10bit Tigole).mkv").AsOsAgnostic(),
});

View File

@@ -55,7 +55,8 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Movie.Title.2019.1080p.AMZN.WEB-Rip.DDP.5.1.HEVC", null)]
[TestCase("Movie Name (2017) [2160p REMUX] [HEVC DV HYBRID HDR10+ Dolby TrueHD Atmos 7 1 24-bit Audio English] [Data Lass]", null)]
[TestCase("Movie Name (2017) [2160p REMUX] [HEVC DV HYBRID HDR10+ Dolby TrueHD Atmos 7 1 24-bit Audio English]-DataLass", "DataLass")]
public void should_parse_expected_release_group(string title, string expected)
[TestCase("Movie Name (2017) (Showtime) (1080p.BD.DD5.1.x265-TheSickle[TAoE])", "TheSickle")]
public void should_parse_release_group(string title, string expected)
{
Parser.Parser.ParseReleaseGroup(title).Should().Be(expected);
}
@@ -116,6 +117,8 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Movie Title (2011) [BluRay] [1080p] [YTS.MX] [YIFY]", "YIFY")]
[TestCase("Movie Title (2014) [BluRay] [1080p] [YIFY] [YTS]", "YTS")]
[TestCase("Movie Title (2018) [BluRay] [1080p] [YIFY] [YTS.LT]", "YTS.LT")]
[TestCase("Movie Title (2016) (1080p AMZN WEB-DL x265 HEVC 10bit EAC3 5 1 RZeroX) QxR", "RZeroX")]
[TestCase("Movie Title (2016) (1080p AMZN WEB-DL x265 HEVC 10bit EAC3 5 1 Garshasp) QxR", "Garshasp")]
public void should_parse_exception_release_group(string title, string expected)
{
Parser.Parser.ParseReleaseGroup(title).Should().Be(expected);

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