1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-04-16 21:15:33 -04:00

Compare commits

..

198 Commits

Author SHA1 Message Date
Bogdan
653b358fd3 Convert Delete Movie Modal to TypeScript
Co-authored-by: Mark McDowall <mark@mcdowall.ca>
2025-03-08 16:36:12 +02:00
Bogdan
6a7ed22b44 Convert Movie History to TypeScript
Closes #10755

Co-authored-by: Mark McDowall <mark@mcdowall.ca>
2025-03-08 16:04:08 +02:00
Mark McDowall
779292490a Convert SelectMovieRow to TypeScript
(cherry picked from commit 32ce09648cb9eb13c46b060f2665f3ce837261f2)
2025-03-08 15:16:12 +02:00
Mark McDowall
e4e96fc7f9 Convert Preview Rename to TypeScript
(cherry picked from commit a2fd23c84d0a9d01864119d2e643970845c9e49e)
2025-03-08 15:16:12 +02:00
Weblate
049bf7715e Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
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/it/
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
2025-03-08 15:16:04 +02:00
Bogdan
df4dfaac0b Bump SixLabors.ImageSharp to 3.1.7 2025-03-07 19:17:58 +02:00
Bogdan
89c96b0a80 Increase input sizes in edit movie modal
Closes #10749
2025-03-07 19:03:28 +02:00
Bogdan
7db12b6e58 Convert EditMovieModal to TypeScript
Towards #10700

Co-authored-by: Mark McDowall <mark@mcdowall.ca>
2025-03-07 19:03:28 +02:00
Bogdan
28dee7bc01 Convert MoveMovieModal to TypeScript
Co-authored-by: Mark McDowall <mark@mcdowall.ca>
2025-03-07 19:03:19 +02:00
Bogdan
8ec60eb0a6 Convert Movie Formats/Status/CollectionLabel to TypeScript 2025-03-07 16:43:37 +02:00
Weblate
102849a697 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Chong Yao Jun <yaojun12345678910@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Stan Ulbrych <stanulbrych@gmail.com>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: pbarone <pbarone@live.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
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/it/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_Hans/
Translation: Servarr/Radarr
2025-03-07 14:03:05 +02:00
Bogdan
95da7d7b47 Convert Interactive Search to TypeScript 2025-03-04 15:15:04 +02:00
Weblate
22b5739967 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Eduardo045 <eduardomeirelles045@gmail.com>
Co-authored-by: Gallyam Biktashev <gallyamb@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Volodymyr <minecrafter7893@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: corwin007x <skopal.ondrej@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
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/uk/
Translation: Servarr/Radarr
2025-03-04 14:57:54 +02:00
Bogdan
cfba047d80 Fixed: Parsing some titles with FRA as French 2025-03-04 14:46:11 +02:00
Bogdan
576d404e70 Fixed: Replace diacritics in Clean Title naming tokens 2025-03-02 07:19:13 +02:00
Bogdan
5959d4e51a Fixed: Instance name must contain application name 2025-02-26 03:59:22 +02:00
Chaz Harris
2aca6c6e1d Bump devcontainer nodejs version to 20
(cherry picked from commit d8222c066c04d5219a21a6e7f9f3571a67e8dcca)
2025-02-25 20:06:02 +02:00
Bogdan
e8bbe0ee9f Bump Polly to 8.5.2 2025-02-25 19:51:19 +02:00
Bogdan
66332a110a Bump version to 5.20.0 2025-02-25 19:50:48 +02:00
Bogdan
36c66deb4b Recommend against using uTorrent 2025-02-25 12:50:11 +02:00
Weblate
edec432244 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
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/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/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translation: Servarr/Radarr
2025-02-24 19:28:00 +02:00
Bogdan
554e15d438 New: Watch list sorting and rate limit for Trakt Import Lists 2025-02-23 14:59:38 +02:00
Bogdan
553645a07c Bump version to 5.19.2 2025-02-23 12:14:28 +02:00
Bogdan
7de7e83c5b New: Add Blu-ray link to movie details 2025-02-23 00:03:48 +02:00
Bogdan
b7a46bedb0 Fixed: Avoid checking for free space if other specifications fail first 2025-02-22 21:33:38 +02:00
Weblate
0925769377 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Al3xPdx007 <constantin.pdx@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Pablo <pablo@pabloarraiz.com>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: pelnoph <pierre.regnier.1984@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
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/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/he/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pl/
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/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_TW/
Translation: Servarr/Radarr
2025-02-22 18:34:51 +02:00
Servarr
72244362fe Automated API Docs update 2025-02-22 18:33:37 +02:00
Mark McDowall
c6526c34e9 Cleanse console log messages
(cherry picked from commit 609e964794e17343f63e1ecff3fef323e3d284ff)
2025-02-19 15:44:15 +02:00
Bogdan
efa2913dbc Translate Trakt popular list types 2025-02-19 15:28:05 +02:00
Mark McDowall
35c22a4ffa Fixed: Only show Additional Parameters on Trakt Popular list
(cherry picked from commit b122ee967009d53432f3d1dd196132487f3999e1)
2025-02-19 15:15:17 +02:00
Stevie Robinson
66d96e21da Fixed: Fallback to Instance Name for Discord notifications
(cherry picked from commit b99e06acc0a3ecae2857d9225b35424c82c67a2b)
2025-02-19 14:52:30 +02:00
Bogdan
36d4e9e6cd New: Movie Requested filter for interactive search 2025-02-18 04:42:10 +02:00
Weblate
7189d7b15c Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: haru4a <haru4as95@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translation: Servarr/Radarr
2025-02-16 23:04:54 +02:00
Servarr
6e80113987 Automated API Docs update 2025-02-16 23:03:09 +02:00
Bogdan
bb8a0dda63 Fixed: Processing existing movie files via Manage Files 2025-02-16 21:36:10 +02:00
Bogdan
525ed65687 Fix download links for FileList when passkey contains spaces 2025-02-16 12:19:17 +02:00
Bogdan
3fbccc6af3 Bump version to 5.19.2 2025-02-16 12:19:04 +02:00
Bogdan
8e10eecfac Fixed: Close Metadata settings modal on saving 2025-02-15 13:34:21 +02:00
Bogdan
a3b1512552 Fixed: Parsing some titles with FRE as French and ITA as Italian 2025-02-13 17:31:39 +02:00
epmt7w3ugk
d375b5ffbe Fixed: Parse GER/DE releases as German language
Fix parsing for German language to correctly detect "GER" and "DE"
Update test for GER/DE language parsing.
2025-02-10 17:17:43 +02:00
Bogdan
884abc0368 Bump version to 5.19.1 2025-02-09 17:50:50 +02:00
Weblate
f8da7aae03 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Gallyam Biktashev <gallyamb@gmail.com>
Co-authored-by: Gionatan Spedicato <natanoig444@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ca/
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/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/it/
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/
Translation: Servarr/Radarr
2025-02-07 19:14:05 -06:00
Connor Gallopo
c165118d4d Update README.md
Update Copyright Date
2025-02-07 14:01:42 -06:00
Robin Dadswell
b3dd571a92 New: Migrated StevenLu URL to new URL 2025-02-06 03:41:33 +02:00
Bogdan
dd900eb739 Building docs on ARM
Co-authored-by: Mark McDowall <mark@mcdowall.ca>
(cherry picked from commit 147e732c9ca7a4c289d4f6386f1277650e11f15b)
2025-02-06 00:43:53 +02:00
Bogdan
66aae0c91c Fixed: Reject multi-part files with P1, P2, etc. 2025-02-04 03:40:06 +02:00
Servarr
d888a0a2b3 Automated API Docs update 2025-02-03 21:31:13 +02:00
Bogdan
cb5416a18c Improve message for unknown movie rejection in release searching 2025-02-03 18:06:48 +02:00
Bogdan
7977e0be05 Add reason enum to decision engine rejections
Co-authored-by: Mark McDowall <mark@mcdowall.ca>
2025-02-03 17:46:13 +02:00
Bogdan
cd836fef38 Bump version to 5.19.0 2025-02-03 14:12:21 +02:00
Mark McDowall
b0bfbe767c Add MediaInfo AudioLanguagesAll and update styling
(cherry picked from commit 572b8620c9693f6824ae0919d6a37ba69c7590b1)
2025-02-02 15:16:41 +02:00
Bogdan
528b93dabe Fixed: Format bitrate for primary streams in media info
Co-authored-by: Mark McDowall <markus.mcd5@gmail.com>
2025-02-02 13:58:44 +02:00
Bogdan
1edcbee5e1 Bump version to 5.18.4 2025-02-02 12:49:50 +02:00
Bogdan
8853dced9f Fixed: Health warning for downloading inside root folders
(cherry picked from commit 1e9fd02e9d2bf57247adcac5728e2a0d2b084b86)
2025-02-01 23:36:10 +02:00
Mark McDowall
c7aa1bae5e Fixed: Ignore special folders inside Blackhole watch folders
(cherry picked from commit e79dd6f8e689617b1fd9f96c639ac300669112c5)
2025-02-01 23:35:45 +02:00
Bogdan
405ae77070 New: Prefer newer Usenet releases
(cherry picked from commit 6a439f03273b376feda713ef04a6912fc3af9d0a)
2025-02-01 23:35:31 +02:00
Weblate
6236bc9b4f Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dani Talens <databio@gmail.com>
Co-authored-by: Mailme Dashite <mailmedashite@protonmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translation: Servarr/Radarr
2025-01-31 23:30:40 +02:00
Bogdan
743c977e5b New: Refresh cache for tracked queue on movies update 2025-01-31 23:29:41 +02:00
Bogdan
c0e5646f07 Bump Polly and NLog.Layouts.ClefJsonLayout 2025-01-26 15:42:43 +02:00
Weblate
10094b4e66 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Craze <christian.strey@gmail.com>
Co-authored-by: Dimitar \"Topper\" Maznekov <d.maznekov@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: ηg <jonas.konrath@icloud.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
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/el/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fa/
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/he/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ja/
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/pl/
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/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2025-01-26 15:00:33 +02:00
Bogdan
d923406f08 Bump version to 5.18.3 2025-01-26 14:48:34 +02:00
Bogdan
69a9c72286 Fixed: Loading movies with duplicated translations 2025-01-26 14:47:13 +02:00
Bogdan
55b9477a01 Fixed: Cleanup duplicated movie translations 2025-01-26 14:47:13 +02:00
Bogdan
6b81f92137 Fixed: Import Movies page crashing on console.error with non-string values 2025-01-21 16:11:22 +02:00
Bogdan
3ceda1bcda New: Parse releases with JPN as Japanese and KOR as Korean 2025-01-20 03:59:38 +02:00
Luke Anderson
f1f1921517 Update Trakt ratings logo (#10822)
Co-authored-by: Bogdan <mynameisbogdan@users.noreply.github.com>
2025-01-19 17:05:55 +02:00
Weblate
af0c96538a Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dimitar \"Topper\" Maznekov <d.maznekov@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: ahgharaghani <ah.gharaghani@gmail.com>
Co-authored-by: keysuck <joshkkim@gmail.com>
Co-authored-by: warkurre86 <tom.novo.86@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fa/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ko/
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/tr/
Translation: Servarr/Radarr
2025-01-19 16:56:42 +02:00
jcassette
3d52f45b6a New: reflink support for ZFS
(cherry picked from commit a840bb542362d58006b6cc27affd58ee6b965b80)
2025-01-19 16:55:06 +02:00
Bogdan
d4715f119d Bump version to 5.18.2 2025-01-19 16:54:39 +02:00
kephasdev
d58135bf17 Fixed: Augmenting languages for releases with MULTI and other languages (#10842) 2025-01-17 20:32:09 +02:00
Bogdan
b452c10da3 Bump SonarCloud azure extension for UI analysis to 3.X
(cherry picked from commit 396b2ae7c10c7df749ea23ea93608b56482175a1)
2025-01-14 11:43:51 +02:00
Stevie Robinson
f6b364725d Additional logging for delay profile decisions
(cherry picked from commit fa0f77659cbd3e9efdae55bbedb30fd8288622a6)

Closes #10831
2025-01-12 20:40:59 +02:00
Stevie Robinson
99f6be3f3d New: Show release source in history grab details
(cherry picked from commit 1609f0c9647b89bf55b8c043eeffc8a61653a1e5)

Closes #10830
2025-01-12 20:40:59 +02:00
Stevie Robinson
c2ac49a873 Additional logging for custom format score
(cherry picked from commit 3c8268c428688cc703af76b648c9b3385858274f)

Closes #10828
2025-01-12 20:40:58 +02:00
Weblate
0e24a3e8bc Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Ano10 <Ano10@users.noreply.translate.servarr.com>
Co-authored-by: Dimitar \"Topper\" Maznekov <d.maznekov@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Mickaël O <mickael.ouillon@ac-bordeaux.fr>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: xumei51201314 <xumei51201314@163.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
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/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/he/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/id/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pl/
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/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_Hans/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_TW/
Translation: Servarr/Radarr
2025-01-12 15:19:47 +02:00
Weblate
18032cc83b Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Ano10 <Ano10@users.noreply.translate.servarr.com>
Co-authored-by: Dimitar \"Topper\" Maznekov <d.maznekov@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Mickaël O <mickael.ouillon@ac-bordeaux.fr>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
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/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/he/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/id/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pl/
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/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_TW/
Translation: Servarr/Radarr
2025-01-12 15:19:09 +02:00
Bogdan
927eb38945 Bump version to 5.18.1 2025-01-12 15:16:00 +02:00
Qstick
5fac348613 Bump SonarCloud azure extension to 3.X
(cherry picked from commit 7b8e352d876cd8f8e5b6296f0c3938bed4db8bb8)
2025-01-11 17:44:00 -06:00
Bogdan
7ba9603449 Fixed: Sending Discord notifications with images without absolute links 2025-01-06 13:27:14 +02:00
Bogdan
e36de8ab8d New: Auto tag based on movie status 2025-01-06 04:24:29 +02:00
Stevie Robinson
f8704a1655 Translate backend: Autotagging + CF specs
Signed-off-by: Stevie Robinson <stevie.robinson@gmail.com>
(cherry picked from commit de1cc25c903924fecbca79fedb458d729ae584fd)

Towards #9647
2025-01-06 04:06:34 +02:00
Stevie Robinson
f507d5154e Fixed: Listening on all IPv4 Addresses
(cherry picked from commit 035c474f10c257331a5f47e863d24af82537e335)
2025-01-05 13:53:29 +02:00
Stevie Robinson
5f03e7142a Fixed: qBittorrent Ratio Limit Check
(cherry picked from commit 4dcc015fb19ceb57d2e8f4985c5137e765829d1c)
2025-01-05 13:53:17 +02:00
Bogdan
c0ebbee7c9 Bump version to 5.18.0 2025-01-05 13:52:57 +02:00
Bogdan
4051cf3d80 Fixed: Increase rate limit for PassThePopcorn 2025-01-02 23:16:48 +02:00
Weblate
9876ed64e2 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: 1 <1228553526@qq.com>
Co-authored-by: Alexander Balya <alexander.balya@gmail.com>
Co-authored-by: Ano10 <Ano10@users.noreply.translate.servarr.com>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Matti Meikäläinen <diefor-93@hotmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Tommy Au <smarttommyau@gmail.com>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: marapavelka <mara.pavelka@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
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/it/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ro/
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_TW/
Translation: Servarr/Radarr
2025-01-02 23:13:41 +02:00
Bogdan
2f26974ecc New: Add Tagalog language 2025-01-02 21:16:30 +02:00
Siddhant Naik
25f66a3029 New: Add Marathi language 2025-01-02 19:41:08 +02:00
Bogdan
0e25b2708c Fixed: Sending Manual Interaction Required notifications to Discord for unknown movies 2024-12-31 18:20:16 +02:00
Bogdan
410870d21e Check if backup folder is writable on backup
(cherry picked from commit 8aad79fd3e14eb885724a5e5790803c289be2f25)

Closes #10806
2024-12-31 12:16:28 +02:00
Bogdan
a64d931904 Suggest adding IP to RPC whitelist for on failed Transmission auth
(cherry picked from commit f05e552e8e6dc02cd26444073ab9a678dcb36492)
2024-12-31 12:12:52 +02:00
Bogdan
f0a9e76cfc Bump version to 5.17.2 2024-12-30 00:58:56 +02:00
Mark McDowall
6f23c465ee Don't send session information to Sentry
(cherry picked from commit fae24e98fb9230c2f3701caef457332952c6723f)
2024-12-28 03:32:11 +02:00
Bogdan
af60cca9ae Fixed: Advanced settings for Metadata consumers 2024-12-23 12:15:04 +02:00
Mark McDowall
d34d23a052 Fixed: Movies updated during Import List Sync not reflected in the UI
(cherry picked from commit 1c30ecd66dd0fd1dafaf9ab0e41a11a54eaac132)

Closes #10794
2024-12-23 12:08:25 +02:00
Bogdan
0a0da42543 Bump version to 5.17.1 2024-12-22 13:23:52 +02:00
Bogdan
e5419f6f06 Bump System.Memory
Closes #10791
2024-12-21 11:20:28 +02:00
Bogdan
88d9c08f1a Bump MailKit to 4.8.0 and Microsoft.Data.SqlClient to 2.1.7
Closes #10790
2024-12-21 11:15:14 +02:00
Bogdan
6b4259757c Add test for do not prefer repacks/propers 2024-12-20 20:33:18 +02:00
Mark McDowall
f1d7c56d94 Fixed: Custom Format score bypassing upgrades not being allowed
(cherry picked from commit ebe23104d4b29a3c900a982fb84e75c27ed531ab)

Co-authored-by: CeruleanRed <toni.suta@gmail.com>
2024-12-20 20:33:18 +02:00
Mark McDowall
c81b2e80ee Convert MediaInfo to TypeScript
(cherry picked from commit 4e4bf3507f20c0f8581c66804f8ef406c41952d8)

Closes #10753
2024-12-20 15:37:16 +02:00
Mark McDowall
5efefd804b Upgrade @typescript-eslint packages to 8.181.1
(cherry picked from commit ed10b63fa0c161cac7e0a2084e53785ab1798208)
2024-12-17 13:15:06 +02:00
Mark McDowall
38f9543526 Upgrade Font Awesome to 6.7.1
(cherry picked from commit 016b5718386593c030f14fcac307c93ee1ceeca6)
2024-12-17 13:11:07 +02:00
Mark McDowall
aae68e681e Upgrade babel to 7.26.0
(cherry picked from commit bfcd017012730c97eb587ae2d2e91f72ee7a1de3)
2024-12-17 13:08:36 +02:00
Bogdan
1d21bbf78f Bump version to 5.17.0 2024-12-16 20:24:54 +02:00
Stevie Robinson
99c3c8ce5b Replace URLs in translations with tokens
(cherry picked from commit 98d60e1a8e9abce6b31b3cdd745eff0fed181458)
2024-12-16 15:40:58 +02:00
Weblate
85171e40a5 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
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/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
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-12-16 15:28:28 +02:00
Bogdan
86b656d323 Use minor version for core-js in babel/preset-env 2024-12-16 12:40:37 +02:00
Bogdan
5ae5d1043a Improve opening add movie modal for Discover Overview 2024-12-16 01:25:03 +02:00
Mark McDowall
b801aa0935 New: Add metadata links to telegram messages
Co-authored-by: Ivar Stangeby <istangeby@gmail.com>

Fixed errors sending Telegram notifications when links aren't available

(cherry picked from commit 4eab168267db716a9e897a992e3a7f6889571f9f)
(cherry picked from commit 4d7a3d0909437268b4ad0a0dbeb59d45b4435118)

Closes #10242
Closes #10489
2024-12-15 15:20:16 +02:00
Stevie Robinson
b2b5aa1f79 New: Optionally as Instance Name to Telegram notifications
(cherry picked from commit 36633b5d08c19158f185c0fa5faabbaec607fcb5)

Closes #10757
2024-12-15 14:43:23 +02:00
Mark McDowall
8c6ba9a543 Fixed: Augmenting languages from indexer for release with stale indexer ID
(cherry picked from commit cb7489ce8fe933920ea04297bd2941496a0c07c6)

Closes #10768
2024-12-15 14:43:23 +02:00
Mark McDowall
4e024c51d3 Fixed: Movies without tags bypassing tags on Download Client
(cherry picked from commit c0e264cfc520ee387bfc882c95a5822c655e0d9b)

Closes #10765
2024-12-15 14:43:23 +02:00
Mark McDowall
e4106f0ede Upgrade TypeScript and core-js
(cherry picked from commit 148480909917f69ff3b2ca547ccb4716dd56606e)

Closes #10763
2024-12-15 14:43:20 +02:00
Bogdan
9032ac20ff Bump version to 5.16.3 2024-12-15 10:04:38 +02:00
Bogdan
23fce4bf2e Fixed: Refresh backup list on deletion
(cherry picked from commit 3b00112447361b19c04851a510e63f812597a043)
2024-12-15 05:29:12 +02:00
Mark McDowall
64fd8552f8 Fixed: Error getting processes in some cases
(cherry picked from commit b552d4e9f7ca7388404aa0d52566010a54cb0244)
2024-12-15 05:28:39 +02:00
Weblate
e016410c10 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Rodion <rodyon009@gmail.com>
Co-authored-by: Tomer Horowitz <tomerh2001@gmail.com>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: farebyting <farelbyting@gmail.com>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: hhjuhl <hans@kopula.dk>
Co-authored-by: kaisernet <afimark7@gmail.com>
Co-authored-by: keysuck <joshkkim@gmail.com>
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/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/id/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
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-12-14 02:33:47 +02:00
Bogdan
bea943adf8 New: Tooltip with extra genres on search and collections 2024-12-13 19:17:59 +02:00
Bogdan
9780d20f8a Improve is visible property check for discover movies 2024-12-13 19:14:41 +02:00
Bogdan
62722d45b0 Fixed: Using all movie genres for collection filters 2024-12-13 19:13:55 +02:00
Bogdan
27dd8e8cd5 New: Tooltip with extra genres on movie details page 2024-12-12 21:59:19 +02:00
Bogdan
6c47ede76b Fixed: Refreshing movie genres 2024-12-12 21:58:54 +02:00
Mark McDowall
7b9562bb38 Update React
(cherry picked from commit 4491df3ae7530f2167beebc3548dd01fd2cc1a12)

Towards #10703
2024-12-12 21:01:19 +02:00
Stevie Robinson
8b0b7c1cb0 New: Reactive search button on Wanted pages
(cherry picked from commit e8c3aa20bd92701a16dcd97c5e103b79b3683105)

Closes #10750
2024-12-12 20:42:55 +02:00
Mark McDowall
7ebd341cd6 Sync TimeSpanConverter with upstream
(cherry picked from commit 1374240321f08d1400faf95e84217e4b7a2d116b)

Closes #10756
2024-12-09 14:03:12 +02:00
Bogdan
6c85f166ff Bump version to 5.16.2 2024-12-08 11:22:50 +02:00
Servarr
45aabce107 Automated API Docs update 2024-12-04 14:09:02 +02:00
soup
0caa793df4 New: Add config file setting for CGNAT authentication bypass
(cherry picked from commit 4c41a4f368046f73f82306bbd73bec992392938b)
2024-12-04 13:30:56 +02:00
Stevie Robinson
9a107cc8d7 New: Add Languages to Webhook Notifications
(cherry picked from commit e039dc45e267cf717e00c0a49ba637012f37e3d7)

Closes #10733
2024-12-02 16:59:45 +02:00
Mark McDowall
a6d727fe2a New: Kometa metadata file creation disabled
(cherry picked from commit c62fc9d05bb9e1fe51b454d78e80bd9250e31f89)

Closes #10738
2024-12-02 16:56:12 +02:00
Servarr
01a53d3624 Automated API Docs update 2024-12-02 16:03:25 +02:00
Weblate
348c29c9d7 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Ardenet <1213193613@qq.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Robin Dadswell <robin@robindadswell.tech>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: mryx007 <mryx@mail.de>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_Hans/
Translation: Servarr/Radarr
2024-12-02 15:34:43 +02:00
Bogdan
64739712c6 Add return type for movie lookup and import endpoints
Closes #10737
2024-12-02 15:29:50 +02:00
Bogdan
6ac9cca953 Avoid default category on existing Transmission configurations
Co-authored-by: Mark McDowall <mark@mcdowall.ca>
(cherry picked from commit bd656ae7f66fc9224ef2a57857152ee5d54d54f8)
2024-12-02 15:12:07 +02:00
Bogdan
a2b38c5b7d New: Labels support for Transmission 4.0
(cherry picked from commit 675e3cd38a14ea33c27f2d66a4be2bf802e17d88)
2024-12-02 15:12:07 +02:00
Bogdan
3cc4105d71 Bump NLog, Npgsql, Ical.Net, IPAddressRange, ImageSharp and Polly 2024-12-02 14:30:38 +02:00
Mark McDowall
3449a5d3fe Fixed: Don't fail import if symlink target can't be resolved
(cherry picked from commit 8cb58a63d8ec1b290bc57ad2cf1e90809ceebce9)
2024-12-02 02:32:21 +02:00
Gylesie
5bac157d36 Remove unnecessary heap allocations in local IP check
(cherry picked from commit ed536a85ad5f2062bf6f01f80efddb19fa935f63)
2024-12-02 02:31:28 +02:00
Mark McDowall
114d260f42 Deprecate Sizeleft and Timeleft queue item properties
Rename SizeLeft and TimeLeft queue item properties

(cherry picked from commit b51a49097941e5f306cae5785c63985b319784fd)

Fixed: Error loading queue

(cherry picked from commit f9606518eef78117f1e06a8bcc34af57ab0d2454)
2024-12-01 19:25:45 +02:00
Bogdan
617b9c5d35 Console warnings for missing translations on development builds
(cherry picked from commit 67a1ecb0fea4e6c7dfdb68fbe3ef30d4c22398d8)

Closes #10669
2024-12-01 18:54:53 +02:00
Mark McDowall
ba4ccbb0bd Deluge communication improvements
(cherry picked from commit 183b8b574a4dd948b5fada94d0e645b87710f223)
2024-12-01 14:01:41 +02:00
Mark McDowall
b845268b3d New: Support for new SABnzbd history retention values
Closes #10699

(cherry picked from commit e361f18837d98c089f7dc9c0190221ca8e2cf225)
2024-12-01 14:01:09 +02:00
Bogdan
0fee552074 Bump version to 5.16.1 2024-12-01 14:00:15 +02:00
Mark McDowall
828b994ef4 Support Postgres with non-standard version string
(cherry picked from commit 40f4ef27b22113c1dae0d0cbdee8205132bed68a)
2024-12-01 11:47:19 +00:00
Weblate
7952fd325b Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Albrt9527 <2563009889@qq.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
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/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_HANS/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_TW/
Translation: Servarr/Radarr
2024-11-30 02:30:22 +02:00
Barry Mieny
4b4e598b67 New: Add Afrikaans language 2024-11-30 02:29:04 +02:00
Bogdan
71ccebd0f5 Fix cutoff fixture 2024-11-30 02:12:26 +02:00
Mark McDowall
2607c67912 Fixed: Prevent lack of internet from stopping all health checks from running
(cherry picked from commit dba3a8243988d3e9870b841696303191e1703a0d)

Closes #10694
2024-11-30 02:05:47 +02:00
Bogdan
a626b4f3c4 Fixed: Custom Format upgrading not respecting 'Upgrades Allowed'
(cherry picked from commit 91c5e6f12292e522ceb9094825525fb3684b97c6)

Closes #10691
2024-11-30 02:01:46 +02:00
Bogdan
1526bf29f4 Fixed path in downloading to root folder check message 2024-11-30 01:59:16 +02:00
bpoxy
2194772736 New: Add Albanian language (#10663) 2024-11-28 01:00:33 +02:00
Gauthier
cd490d6334 New: Add headers setting in webhook connection
(cherry picked from commit 78fb20282de73c0ea47375895a807235385d90e3)
2024-11-28 00:58:27 +02:00
bakerboy448
ff609848d8 New: Replace 'Ben the Man' release group parsing with 'Ben the Men'
Closes #10676

(cherry picked from commit 202190d032257b3cd19e42606385db7052b2aae4)
2024-11-27 15:59:51 -06:00
Weblate
15b6f7212d Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Mizuyoru_TW <mizuyoru.tw@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: hebian <zyf200913@163.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_TW/
Translation: Servarr/Radarr
2024-11-27 14:57:43 -06:00
Mark McDowall
af06a9f70d Webpack web target
(cherry picked from commit a90866a73e6cff9a286c23e60c74672f4c0d317a)
2024-11-27 11:12:43 +02:00
Servarr
c3fa440cf8 Multiple Translations updated by Weblate (#10688)
ignore-downstream


Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translation: Servarr/Radarr

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
2024-11-19 20:37:07 -06:00
Bogdan
0411d66520 Bump version to 5.16.0 2024-11-19 03:10:17 +02:00
Bogdan
179637fe8b Fixed: Release dates for Discover Movie posters 2024-11-19 00:40:44 +02:00
Weblate
09b4bf15cf Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
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/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translation: Servarr/Radarr
2024-11-17 14:00:33 +02:00
Mark McDowall
ea86d14ca7 Fixed: Normalize unicode characters when comparing paths for equality
(cherry picked from commit ceeec091f85d0094e07537b7f62f18292655a710)
2024-11-17 11:28:51 +02:00
Elias Benbourenane
2429dd91c6 Allow GetFileSize to follow symlinks
(cherry picked from commit ca0bb14027f3409014e7cf9ffa8e04e577001d77)
2024-11-17 11:28:35 +02:00
Mark McDowall
a752476cdb Fixed: Allow files to be moved from Torrent Blackhole even when remove is disabled
(cherry picked from commit f739fd0900695e2ff312d13985c87d84ae00ea75)
2024-11-17 11:27:35 +02:00
Bogdan
50ce480abf Pin ReportGenerator in Azure Pipelines for .NET 6 2024-11-15 22:18:40 +02:00
Weblate
0ef6e56e5d Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Ardenet <1213193613@qq.com>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@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/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
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/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/he/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/lv/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pl/
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/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_TW/
Translation: Servarr/Radarr
2024-11-07 17:04:54 +02:00
Bogdan
12d5014125 New: Track Kometa metadata files
Co-authored-by: Stevie Robinson <stevie.robinson@gmail.com>

Fixes #10059
Fixes #10419
Closes #10311
2024-11-05 14:31:08 +02:00
Bogdan
c8301d425c Fix translation token for Mount Health Check 2024-11-04 19:26:46 +02:00
Servarr
b1df9b2401 Automated API Docs update 2024-11-04 15:41:58 +02:00
Mark McDowall
ff09da3a69 New: Filter queue by status
(cherry picked from commit fb540040ef66e90c55b82539b85df378d6c76bd3)

Closes #10648
2024-11-04 15:05:57 +02:00
Mark McDowall
3b9bd696fb New: Favorite folders in Manual Import
(cherry picked from commit 3ddc6ac6de5c27a9aab915672321c8818dc5da48)

Closes #10630
2024-11-04 14:36:38 +02:00
Weblate
9ab3e6bab7 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Lars <lars.erik.heloe@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: mytelegrambot <lacsonluxur@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/da/
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/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ko/
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/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-11-04 13:26:17 +02:00
Mark McDowall
86f4f86a0a Fixed: Filtering queue by multiple qualities
(cherry picked from commit b8af3af9f16db96337832c2989f4e7ff3dc2ed30)

Closes #10647
2024-11-04 13:12:31 +02:00
Bogdan
40d95a04e3 Sync coding style with upstream for join methods
Towards #10637
2024-11-04 13:01:40 +02:00
Mark McDowall
ca724836ce Rename Manage Custom Formats to Manage Formats
(cherry picked from commit 0f225b05c00add562c9a6aa8cc4cf494e83176c1)

Closes #10629
2024-11-04 12:57:55 +02:00
Mark McDowall
10e3964111 New: Use instance name in PWA manifest
(cherry picked from commit 1fcfb88d2aa0126c4b3c878c8e310311ea57d04d)

Closes #10625
2024-11-04 12:54:59 +02:00
Mark McDowall
b22a86e1d7 New: Include source path with Webhook import event movie file
(cherry picked from commit 73208e2f60263b1236f094a2bf6c47ebd5a8a271)

Closes #10635
2024-11-04 12:53:12 +02:00
Mark McDowall
5976d66511 New: Reject files during import that have no audio tracks
(cherry picked from commit 978349e24135572889095c743d0e7fac734ba7e0)

Closes #10643
2024-11-04 12:51:14 +02:00
Bogdan
b4eff4d4f9 Show a movie path as example in Mount Health Check
Closes #10649
2024-11-04 12:38:48 +02:00
Mark McDowall
1414a09111 New: Add individual edit to Manage Custom Formats
(cherry picked from commit e006b405323c276eb5b7f2dd97b97c80394a6930)
2024-11-04 12:38:30 +02:00
Mark McDowall
b30efd0c62 Use current time for cache break in development
(cherry picked from commit 020ed32fcfab1c6fbe57af5ea650300272c93fd7)
2024-11-04 12:37:12 +02:00
Mark McDowall
def6950db4 Fixed: Use download client name for history column
(cherry picked from commit 1df0ba9e5aef2d2745a45c546c869837ac8e68db)
2024-11-04 12:36:55 +02:00
Mark McDowall
f23c2dbaba Increase retries for DebouncerFixture
(cherry picked from commit 78cf13d341e6690bf6079dd1819d060d002155a7)
2024-11-04 12:36:26 +02:00
Bogdan
186e9cdd23 Bump version to 5.15.1 2024-11-03 11:41:20 +02:00
Bogdan
394f34eb2a Fixed: Root folder existence for import lists and movie collections 2024-11-02 21:59:32 +02:00
Weblate
d9f508280d Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Daniel <statoxxl@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: HUi <huynguyeexn@gmail.com>
Co-authored-by: Kuzmich <kuzmich55@gmail.com>
Co-authored-by: Moon55 <dylan.gurdak@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: http://translate.servarr.com/projects/servarr/radarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/bg/
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/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/id/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/lv/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_TW/
Translation: Servarr/Radarr
2024-11-02 21:12:23 +02:00
Bogdan
b5505800de Fix file browser translations 2024-11-02 21:07:10 +02:00
Bogdan
48a79eb7d3 Fixed: Loading queue with pending releases for deleted movies 2024-10-31 18:33:04 +02:00
Bogdan
b42f7e09f9 Fixed: Cleaning the French preposition 'à' from titles 2024-10-31 11:33:08 +02:00
Bogdan
8f507ac726 Fixed: Parse "Català" and "Catalán" as Catalan 2024-10-29 20:49:00 +02:00
Bogdan
06d54e0ec2 Update JetBrains logos
Closes #10603
2024-10-29 10:00:57 +02:00
Bogdan
3708d58847 Fixed: Custom filtering movies by year
Fixes #10610
2024-10-28 07:49:55 +02:00
Bogdan
0049ccd39f Inherit trigger from pushed command models
(cherry picked from commit 0bc4903954b955fce0c368ef7fd2a6f3761d6a93)

Closes #10592
2024-10-27 08:20:57 +02:00
Bogdan
ab8a2d190e Improve message for grab errors due to no matching tags
Co-authored-by: zakary <zak@ary.dev>
(cherry picked from commit df672487cf1d5f067849367a2bfb0068defc315d)

Closes #10593
2024-10-27 08:10:38 +02:00
Hadrien Patte
25bb52b206 Use OperatingSystem class to get OS information
(cherry picked from commit 135b5c2ddd8f0a274b0d59eb07f75aaf1446b9da)
2024-10-27 08:08:01 +02:00
Bogdan
63c6f70e67 Fixed: Changing movies to another root folder without moving files 2024-10-27 08:07:44 +02:00
Bogdan
79cd6269f4 Fixed: Status check for completed directories in Deluge
(cherry picked from commit 33139d4b53c1adad769c7e2b0510e8990c66b84a)
2024-10-27 08:06:29 +02:00
Bogdan
879c872179 Cleanse exceptions in event logs
(cherry picked from commit 404e6d68ea526ab521cd39ecda1bf3b02285765d)
2024-10-27 08:06:11 +02:00
Bogdan
d4993cf69b Bump version to 5.15.0 2024-10-26 17:41:27 +03:00
481 changed files with 12968 additions and 8426 deletions

View File

@@ -6,7 +6,7 @@
"features": {
"ghcr.io/devcontainers/features/node:1": {
"nodeGypDependencies": true,
"version": "16",
"version": "20",
"nvmVersion": "latest"
}
},

View File

@@ -1,33 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="70px" height="70px" viewBox="0 0 70 70" style="enable-background:new 0 0 70 70;" xml:space="preserve">
<g>
<g>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="-1.3318" y1="43.7371" x2="67.0419" y2="26.0967">
<stop offset="0.1237" style="stop-color:#7866FF"/>
<stop offset="0.5376" style="stop-color:#FE2EB6"/>
<stop offset="0.8548" style="stop-color:#FD0486"/>
</linearGradient>
<polygon style="fill:url(#SVGID_1_);" points="67.3,16 43.7,0 0,31.1 11.1,70 58.9,60.3 "/>
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="45.9148" y1="38.9098" x2="67.6577" y2="9.0989">
<stop offset="0.1237" style="stop-color:#FF0080"/>
<stop offset="0.2587" style="stop-color:#FE0385"/>
<stop offset="0.4109" style="stop-color:#FA0C92"/>
<stop offset="0.5713" style="stop-color:#F41BA9"/>
<stop offset="0.7363" style="stop-color:#EB2FC8"/>
<stop offset="0.8656" style="stop-color:#E343E6"/>
</linearGradient>
<polygon style="fill:url(#SVGID_2_);" points="67.3,16 43.7,0 38,15.7 38,47.8 70,47.8 "/>
</g>
<g>
<rect x="13.4" y="13.4" style="fill:#000000;" width="43.2" height="43.2"/>
<rect x="17.4" y="48.5" style="fill:#FFFFFF;" width="16.2" height="2.7"/>
<g>
<path style="fill:#FFFFFF;" d="M17.4,19.1h6.9c5.6,0,9.5,3.8,9.5,8.9V28c0,5-3.9,8.9-9.5,8.9h-6.9V19.1z M21.4,22.7v10.7h3
c3.2,0,5.4-2.2,5.4-5.3V28c0-3.2-2.2-5.4-5.4-5.4H21.4z"/>
<polygon style="fill:#FFFFFF;" points="40.3,22.7 34.9,22.7 34.9,19.1 49.6,19.1 49.6,22.7 44.2,22.7 44.2,37 40.3,37 "/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1,66 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="120.1px" height="130.2px" viewBox="0 0 120.1 130.2" style="enable-background:new 0 0 120.1 130.2;" xml:space="preserve"
>
<g>
<linearGradient id="XMLID_2_" gradientUnits="userSpaceOnUse" x1="31.8412" y1="120.5578" x2="110.2402" y2="73.24">
<stop offset="0" style="stop-color:#FCEE39"/>
<stop offset="1" style="stop-color:#F37B3D"/>
</linearGradient>
<path id="XMLID_3041_" style="fill:url(#XMLID_2_);" d="M118.6,71.8c0.9-0.8,1.4-1.9,1.5-3.2c0.1-2.6-1.8-4.7-4.4-4.9
c-1.2-0.1-2.4,0.4-3.3,1.1l0,0l-83.8,45.9c-1.9,0.8-3.6,2.2-4.7,4.1c-2.9,4.8-1.3,11,3.6,13.9c3.4,2,7.5,1.8,10.7-0.2l0,0l0,0
c0.2-0.2,0.5-0.3,0.7-0.5l78-54.8C117.3,72.9,118.4,72.1,118.6,71.8L118.6,71.8L118.6,71.8z"/>
<linearGradient id="XMLID_3_" gradientUnits="userSpaceOnUse" x1="48.3607" y1="6.9083" x2="119.9179" y2="69.5546">
<stop offset="0" style="stop-color:#EF5A6B"/>
<stop offset="0.57" style="stop-color:#F26F4E"/>
<stop offset="1" style="stop-color:#F37B3D"/>
</linearGradient>
<path id="XMLID_3049_" style="fill:url(#XMLID_3_);" d="M118.8,65.1L118.8,65.1L55,2.5C53.6,1,51.6,0,49.3,0
c-4.3,0-7.7,3.5-7.7,7.7v0c0,2.1,0.8,3.9,2.1,5.3l0,0l0,0c0.4,0.4,0.8,0.7,1.2,1l67.4,57.7l0,0c0.8,0.7,1.8,1.2,3,1.3
c2.6,0.1,4.7-1.8,4.9-4.4C120.2,67.3,119.7,66,118.8,65.1z"/>
<linearGradient id="XMLID_4_" gradientUnits="userSpaceOnUse" x1="52.9467" y1="63.6407" x2="10.5379" y2="37.1562">
<stop offset="0" style="stop-color:#7C59A4"/>
<stop offset="0.3852" style="stop-color:#AF4C92"/>
<stop offset="0.7654" style="stop-color:#DC4183"/>
<stop offset="0.957" style="stop-color:#ED3D7D"/>
</linearGradient>
<path id="XMLID_3042_" style="fill:url(#XMLID_4_);" d="M57.1,59.5C57,59.5,17.7,28.5,16.9,28l0,0l0,0c-0.6-0.3-1.2-0.6-1.8-0.9
c-5.8-2.2-12.2,0.8-14.4,6.6c-1.9,5.1,0.2,10.7,4.6,13.4l0,0l0,0C6,47.5,6.6,47.8,7.3,48c0.4,0.2,45.4,18.8,45.4,18.8l0,0
c1.8,0.8,3.9,0.3,5.1-1.2C59.3,63.7,59,61,57.1,59.5z"/>
<linearGradient id="XMLID_5_" gradientUnits="userSpaceOnUse" x1="52.1736" y1="3.7019" x2="10.7706" y2="37.8971">
<stop offset="0" style="stop-color:#EF5A6B"/>
<stop offset="0.364" style="stop-color:#EE4E72"/>
<stop offset="1" style="stop-color:#ED3D7D"/>
</linearGradient>
<path id="XMLID_3057_" style="fill:url(#XMLID_5_);" d="M49.3,0c-1.7,0-3.3,0.6-4.6,1.5L4.9,28.3c-0.1,0.1-0.2,0.1-0.2,0.2l-0.1,0
l0,0c-1.7,1.2-3.1,3-3.9,5.1C-1.5,39.4,1.5,45.9,7.3,48c3.6,1.4,7.5,0.7,10.4-1.4l0,0l0,0c0.7-0.5,1.3-1,1.8-1.6l34.6-31.2l0,0
c1.8-1.4,3-3.6,3-6.1v0C57.1,3.5,53.6,0,49.3,0z"/>
<g id="XMLID_3008_">
<rect id="XMLID_3033_" x="34.6" y="37.4" style="fill:#000000;" width="51" height="51"/>
<rect id="XMLID_3032_" x="39" y="78.8" style="fill:#FFFFFF;" width="19.1" height="3.2"/>
<g id="XMLID_3009_">
<path id="XMLID_3030_" style="fill:#FFFFFF;" d="M38.8,50.8l1.5-1.4c0.4,0.5,0.8,0.8,1.3,0.8c0.6,0,0.9-0.4,0.9-1.2l0-5.3l2.3,0
l0,5.3c0,1-0.3,1.8-0.8,2.3c-0.5,0.5-1.3,0.8-2.3,0.8C40.2,52.2,39.4,51.6,38.8,50.8z"/>
<path id="XMLID_3028_" style="fill:#FFFFFF;" d="M45.3,43.8l6.7,0v1.9l-4.4,0V47l4,0l0,1.8l-4,0l0,1.3l4.5,0l0,2l-6.7,0
L45.3,43.8z"/>
<path id="XMLID_3026_" style="fill:#FFFFFF;" d="M55,45.8l-2.5,0l0-2l7.3,0l0,2l-2.5,0l0,6.3l-2.3,0L55,45.8z"/>
<path id="XMLID_3022_" style="fill:#FFFFFF;" d="M39,54l4.3,0c1,0,1.8,0.3,2.3,0.7c0.3,0.3,0.5,0.8,0.5,1.4v0
c0,1-0.5,1.5-1.3,1.9c1,0.3,1.6,0.9,1.6,2v0c0,1.4-1.2,2.3-3.1,2.3l-4.3,0L39,54z M43.8,56.6c0-0.5-0.4-0.7-1-0.7l-1.5,0l0,1.5
l1.4,0C43.4,57.3,43.8,57.1,43.8,56.6L43.8,56.6z M43,59l-1.8,0l0,1.5H43c0.7,0,1.1-0.3,1.1-0.8v0C44.1,59.2,43.7,59,43,59z"/>
<path id="XMLID_3019_" style="fill:#FFFFFF;" d="M46.8,54l3.9,0c1.3,0,2.1,0.3,2.7,0.9c0.5,0.5,0.7,1.1,0.7,1.9v0
c0,1.3-0.7,2.1-1.7,2.6l2,2.9l-2.6,0l-1.7-2.5h-1l0,2.5l-2.3,0L46.8,54z M50.6,58c0.8,0,1.2-0.4,1.2-1v0c0-0.7-0.5-1-1.2-1
l-1.5,0v2H50.6z"/>
<path id="XMLID_3016_" style="fill:#FFFFFF;" d="M56.8,54l2.2,0l3.5,8.4l-2.5,0l-0.6-1.5l-3.2,0l-0.6,1.5l-2.4,0L56.8,54z
M58.8,59l-0.9-2.3L57,59L58.8,59z"/>
<path id="XMLID_3014_" style="fill:#FFFFFF;" d="M62.8,54l2.3,0l0,8.3l-2.3,0L62.8,54z"/>
<path id="XMLID_3012_" style="fill:#FFFFFF;" d="M65.7,54l2.1,0l3.4,4.4l0-4.4l2.3,0l0,8.3l-2,0L68,57.8l0,4.6l-2.3,0L65.7,54z"
/>
<path id="XMLID_3010_" style="fill:#FFFFFF;" d="M73.7,61.1l1.3-1.5c0.8,0.7,1.7,1,2.7,1c0.6,0,1-0.2,1-0.6v0
c0-0.4-0.3-0.5-1.4-0.8c-1.8-0.4-3.1-0.9-3.1-2.6v0c0-1.5,1.2-2.7,3.2-2.7c1.4,0,2.5,0.4,3.4,1.1l-1.2,1.6
c-0.8-0.5-1.6-0.8-2.3-0.8c-0.6,0-0.8,0.2-0.8,0.5v0c0,0.4,0.3,0.5,1.4,0.8c1.9,0.4,3.1,1,3.1,2.6v0c0,1.7-1.3,2.7-3.4,2.7
C76.1,62.5,74.7,62,73.7,61.1z"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -1,50 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="70px" height="70px" viewBox="0 0 70 70" style="enable-background:new 0 0 70 70;" xml:space="preserve">
<g>
<g>
<g>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="22.9451" y1="75.7869" x2="74.7868" y2="20.6415">
<stop offset="1.612903e-002" style="stop-color:#B35BA3"/>
<stop offset="0.4044" style="stop-color:#C41E57"/>
<stop offset="0.4677" style="stop-color:#C41E57"/>
<stop offset="0.6505" style="stop-color:#EB8523"/>
<stop offset="0.9516" style="stop-color:#FEBD11"/>
</linearGradient>
<polygon style="fill:url(#SVGID_1_);" points="49.8,15.2 36,36.7 58.4,70 70,23.1 "/>
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="17.7187" y1="73.2922" x2="69.5556" y2="18.1519">
<stop offset="1.612903e-002" style="stop-color:#B35BA3"/>
<stop offset="0.4044" style="stop-color:#C41E57"/>
<stop offset="0.4677" style="stop-color:#C41E57"/>
<stop offset="0.7043" style="stop-color:#EB8523"/>
</linearGradient>
<polygon style="fill:url(#SVGID_2_);" points="51.1,15.7 49,0 18.8,33.6 27.6,42.3 20.8,70 58.4,70 "/>
</g>
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="1.8281" y1="53.4275" x2="48.8245" y2="9.2255">
<stop offset="1.612903e-002" style="stop-color:#B35BA3"/>
<stop offset="0.6613" style="stop-color:#C41E57"/>
</linearGradient>
<polygon style="fill:url(#SVGID_3_);" points="49,0 11.6,0 0,47.1 55.6,47.1 "/>
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="49.8935" y1="-11.5569" x2="48.8588" y2="24.0352">
<stop offset="0.5" style="stop-color:#C41E57"/>
<stop offset="0.6668" style="stop-color:#D13F48"/>
<stop offset="0.7952" style="stop-color:#D94F39"/>
<stop offset="0.8656" style="stop-color:#DD5433"/>
</linearGradient>
<polygon style="fill:url(#SVGID_4_);" points="55.3,47.1 51.1,15.7 49,0 41.7,23 "/>
</g>
<g>
<rect x="13.4" y="13.5" transform="matrix(-1 2.577289e-003 -2.577289e-003 -1 70.0288 70.081)" style="fill:#000000;" width="43.2" height="43.2"/>
<rect x="17.6" y="48.6" transform="matrix(1 -2.577289e-003 2.577289e-003 1 -0.1287 6.634109e-002)" style="fill:#FFFFFF;" width="16.2" height="2.7"/>
<path style="fill:#FFFFFF;" d="M17.4,19.1l8.2,0c2.3,0,4,0.6,5.2,1.8c1,1,1.5,2.4,1.5,4.1l0,0.1c0,1.5-0.3,2.6-1.1,3.5
c-0.7,0.9-1.6,1.6-2.8,2l4.4,6.4l-4.6,0l-3.7-5.5l-3.3,0l0,5.5l-3.9,0L17.4,19.1z M25.3,27.8c1,0,1.7-0.2,2.2-0.7
c0.5-0.5,0.8-1.1,0.8-1.8l0-0.1c0-0.9-0.3-1.5-0.8-1.9c-0.5-0.4-1.3-0.6-2.3-0.6l-3.9,0l0,5.1L25.3,27.8z"/>
<path style="fill:#FFFFFF;" d="M36,33.2l-1.9,0l0-3.3l2.5,0l0.6-3.8l-2.3,0l0-3.3l2.8,0l0.6-3.7l3.4,0l-0.6,3.7l3.7,0l0.6-3.7
l3.4,0l-0.6,3.7l1.9,0l0,3.3l-2.5,0L47,29.9l2.3,0l0,3.3l-2.8,0L45.8,37l-3.4,0l0.7-3.8l-3.7,0L38.7,37l-3.4,0L36,33.2z
M43.7,29.9l0.6-3.8l-3.7,0L40,29.9L43.7,29.9z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -1,42 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="70px" height="70px" viewBox="0 0 70 70" style="enable-background:new 0 0 70 70;" xml:space="preserve">
<defs>
<linearGradient id="linear-gradient" x1="70.22612" y1="27.79912" x2="-5.13024" y2="63.12242" gradientTransform="matrix(1, 0, 0, -1, 0, 71.27997)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#c90f5e"/>
<stop offset="0.22111" stop-color="#c90f5e"/>
<stop offset="0.2356" stop-color="#c90f5e"/>
<stop offset="0.35559" stop-color="#ca135c"/>
<stop offset="0.46633" stop-color="#ce1e57"/>
<stop offset="0.5735" stop-color="#d4314e"/>
<stop offset="0.67844" stop-color="#dc4b41"/>
<stop offset="0.78179" stop-color="#e66d31"/>
<stop offset="0.88253" stop-color="#f3961d"/>
<stop offset="0.94241" stop-color="#fcb20f"/>
</linearGradient>
<linearGradient id="linear-gradient-2" x1="24.65904" y1="61.99608" x2="46.04762" y2="2.93445" gradientTransform="matrix(1, 0, 0, -1, 0, 71.27997)" gradientUnits="userSpaceOnUse">
<stop offset="0.04188" stop-color="#077cfb"/>
<stop offset="0.44503" stop-color="#c90f5e"/>
<stop offset="0.95812" stop-color="#077cfb"/>
</linearGradient>
<linearGradient id="linear-gradient-3" x1="17.39552" y1="63.34592" x2="33.19389" y2="7.20092" gradientTransform="matrix(1, 0, 0, -1, 0, 71.27997)" gradientUnits="userSpaceOnUse">
<stop offset="0.27749" stop-color="#c90f5e"/>
<stop offset="0.97382" stop-color="#fcb20f"/>
</linearGradient>
</defs>
<title>rider</title>
<g>
<polygon points="70 27.237 63.391 23.75 20.926 0 3.827 17.921 21.619 41.068 60.537 44.397 70 27.237" fill="url(#linear-gradient)"/>
<polygon points="50.423 16.132 44.271 1.107 27.643 17.471 11.768 50.194 49.411 70 70 57.98 50.423 16.132" fill="url(#linear-gradient-2)"/>
<polygon points="20.926 0 0 14.095 7.779 62.172 27.848 69.889 53.78 48.823 20.926 0" fill="url(#linear-gradient-3)"/>
</g>
<g>
<rect x="13.30219" y="13.19311" width="43.61371" height="43.61371"/>
<g>
<path d="M17.22741,18.86293h8.39564a7.38416,7.38416,0,0,1,5.34268,1.85358,5.86989,5.86989,0,0,1,1.52648,4.1433h0A5.74339,5.74339,0,0,1,28.567,30.5296l4.47041,6.54206H28.34891L24.42368,31.1838h-3.162v5.88785H17.22741V18.86293h0ZM25.296,27.69471c1.96262,0,3.053-1.09034,3.053-2.61682h0c0-1.74455-1.19938-2.61682-3.162-2.61682H21.15265v5.23365H25.296Z" fill="#fff"/>
<path d="M36.09034,18.86293H43.2866c5.77882,0,9.70405,3.92523,9.70405,9.15888h0c0,5.12461-3.92523,9.15888-9.70405,9.15888H36.09034V18.86293Zm4.03427,3.59813V33.47352h3.162a5.23727,5.23727,0,0,0,5.56075-5.45171h0a5.26493,5.26493,0,0,0-5.56075-5.56075h-3.162Z" fill="#fff"/>
</g>
<rect x="17.22741" y="48.62925" width="16.35514" height="2.72586" fill="#fff"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -1,36 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="70px" height="70px" viewBox="0 0 70 70" style="enable-background:new 0 0 70 70;" xml:space="preserve">
<g>
<g>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="25.0676" y1="1.4599" x2="43.1829" y2="66.675">
<stop offset="0.2849" style="stop-color:#00CDD7"/>
<stop offset="0.9409" style="stop-color:#2086D7"/>
</linearGradient>
<polygon style="fill:url(#SVGID_1_);" points="9.4,63.3 0,7.3 17.5,0.1 28.6,6.7 38.8,1.2 60.1,9.4 48.1,70 "/>
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="30.7199" y1="9.7343" x2="61.365" y2="54.6713">
<stop offset="0.1398" style="stop-color:#FFF045"/>
<stop offset="0.3656" style="stop-color:#00CDD7"/>
</linearGradient>
<polygon style="fill:url(#SVGID_2_);" points="70,23.7 61,1.4 44.6,0 19.3,24.3 26.1,55.6 38.8,64.6 70,46 62.3,31.7 "/>
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="61.0819" y1="15.2899" x2="65.1065" y2="29.5436">
<stop offset="0.2849" style="stop-color:#00CDD7"/>
<stop offset="0.9409" style="stop-color:#2086D7"/>
</linearGradient>
<polygon style="fill:url(#SVGID_3_);" points="56,20.4 62.3,31.7 70,23.7 64.4,9.8 "/>
</g>
<g>
<g>
<rect x="13.4" y="13.4" style="fill:#000000;" width="43.2" height="43.2"/>
<rect x="17.5" y="48.5" style="fill:#FFFFFF;" width="16.2" height="2.7"/>
<path style="fill:#FFFFFF;" d="M38.7,34.3l2.3-2.8c1.6,1.3,3.3,2.2,5.3,2.2c1.6,0,2.5-0.6,2.5-1.7v-0.1c0-1-0.6-1.5-3.6-2.3
c-3.6-0.9-5.8-1.9-5.8-5.5v-0.1c0-3.3,2.6-5.4,6.2-5.4c2.6,0,4.8,0.8,6.6,2.3l-2,3c-1.6-1.1-3.1-1.8-4.6-1.8
c-1.5,0-2.3,0.7-2.3,1.6v0.1c0,1.2,0.8,1.6,3.8,2.4c3.6,1,5.6,2.3,5.6,5.4v0.1c0,3.6-2.7,5.6-6.5,5.6
C43.5,37.2,40.8,36.2,38.7,34.3"/>
</g>
<polygon style="fill:#FFFFFF;" points="35.2,19 32.5,29.4 29.5,19 26.5,19 23.4,29.4 20.7,19 16.6,19 21.7,36.9 25,36.9 28,26.5
30.9,36.9 34.3,36.9 39.4,19 "/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -15,7 +15,7 @@ Note that only one type of a given movie is supported. If you want both a 4k ver
* Adding new movies with lots of information, such as trailers, ratings, etc.
* Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
* Can watch for better quality of the movies you have and do an automatic upgrade. *e.g. from DVD to Blu-Ray*
* Can watch for better quality of the movies you have and do an automatic upgrade. _eg. from DVD to Blu-Ray_
* Automatic failed download handling will try another release if one fails
* Manual search so you can pick any release or to see why a release was not downloaded automatically
* Full integration with SABnzbd and NZBGet
@@ -68,12 +68,12 @@ Support this project by becoming a sponsor. Your logo will show up here with a l
## JetBrains
Thank you to [<img src="/Logo/jetbrains.svg" alt="JetBrains" width="32"> JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools.
Thank you to [<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.png" alt="JetBrains" width="96">](http://www.jetbrains.com/) for providing us with free licenses to their great tools.
* [<img src="/Logo/resharper.svg" alt="ReSharper" width="32"> ReSharper](http://www.jetbrains.com/resharper/)
* [<img src="/Logo/webstorm.svg" alt="WebStorm" width="32"> WebStorm](http://www.jetbrains.com/webstorm/)
* [<img src="/Logo/rider.svg" alt="Rider" width="32"> Rider](http://www.jetbrains.com/rider/)
* [<img src="/Logo/dottrace.svg" alt="dotTrace" width="32"> dotTrace](http://www.jetbrains.com/dottrace/)
* [<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/ReSharper_icon.png" alt="ReSharper" width="32"> ReSharper](http://www.jetbrains.com/resharper/)
* [<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/WebStorm_icon.png" alt="WebStorm" width="32"> WebStorm](http://www.jetbrains.com/webstorm/)
* [<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/Rider_icon.png" alt="Rider" width="32"> Rider](http://www.jetbrains.com/rider/)
* [<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/dotTrace_icon.png" alt="dotTrace" width="32"> dotTrace](http://www.jetbrains.com/dottrace/)
## DigitalOcean
@@ -87,4 +87,4 @@ This project is also supported by DigitalOcean
### License
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
* Copyright 2010-2022
* Copyright 2010-2025

View File

@@ -9,7 +9,7 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '5.14.0'
majorVersion: '5.20.0'
minorVersion: $[counter('minorVersion', 2000)]
radarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
@@ -1116,20 +1116,20 @@ stages:
vmImage: ${{ variables.windowsImage }}
steps:
- checkout: self # Need history for Sonar analysis
- task: SonarCloudPrepare@2
- task: SonarCloudPrepare@3
env:
SONAR_SCANNER_OPTS: ''
inputs:
SonarCloud: 'SonarCloud'
organization: 'radarr'
scannerMode: 'CLI'
scannerMode: 'cli'
configMode: 'manual'
cliProjectKey: 'Radarr_Radarr.UI'
cliProjectName: 'RadarrUI'
cliProjectVersion: '$(radarrVersion)'
cliSources: './frontend'
- task: SonarCloudAnalyze@2
- task: SonarCloudAnalyze@3
- job: Api_Docs
displayName: API Docs
dependsOn: Prepare
@@ -1205,12 +1205,12 @@ stages:
submodules: true
- powershell: Set-Service SCardSvr -StartupType Manual
displayName: Enable Windows Test Service
- task: SonarCloudPrepare@2
- task: SonarCloudPrepare@3
condition: eq(variables['System.PullRequest.IsFork'], 'False')
inputs:
SonarCloud: 'SonarCloud'
organization: 'radarr'
scannerMode: 'MSBuild'
scannerMode: 'dotnet'
projectKey: 'Radarr_Radarr'
projectName: 'Radarr'
projectVersion: '$(radarrVersion)'
@@ -1223,10 +1223,10 @@ stages:
./build.sh --backend -f net6.0 -r win-x64
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
displayName: Coverage Unit Tests
- task: SonarCloudAnalyze@2
- task: SonarCloudAnalyze@3
condition: eq(variables['System.PullRequest.IsFork'], 'False')
displayName: Publish SonarCloud Results
- task: reportgenerator@5
- task: reportgenerator@5.3.11
displayName: Generate Coverage Report
inputs:
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'

15
docs.sh
View File

@@ -1,13 +1,18 @@
#!/bin/bash
set -e
FRAMEWORK="net6.0"
PLATFORM=$1
ARCHITECTURE="${2:-x64}"
if [ "$PLATFORM" = "Windows" ]; then
RUNTIME="win-x64"
RUNTIME="win-$ARCHITECTURE"
elif [ "$PLATFORM" = "Linux" ]; then
RUNTIME="linux-x64"
RUNTIME="linux-$ARCHITECTURE"
elif [ "$PLATFORM" = "Mac" ]; then
RUNTIME="osx-x64"
RUNTIME="osx-$ARCHITECTURE"
else
echo "Platform must be provided as first arguement: Windows, Linux or Mac"
echo "Platform must be provided as first argument: Windows, Linux or Mac"
exit 1
fi
@@ -35,7 +40,7 @@ dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p
dotnet new tool-manifest
dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli
dotnet tool run swagger tofile --output ./src/Radarr.Api.V3/openapi.json "$outputFolder/net6.0/$RUNTIME/$application" v3 &
dotnet tool run swagger tofile --output ./src/Radarr.Api.V3/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v3 &
sleep 45

View File

@@ -210,7 +210,6 @@ module.exports = {
'no-undef-init': 'off',
'no-undefined': 'off',
'no-unused-vars': ['error', { args: 'none', ignoreRestSiblings: true }],
'no-use-before-define': 'error',
// Node.js and CommonJS

View File

@@ -26,6 +26,7 @@ module.exports = (env) => {
const config = {
mode: isProduction ? 'production' : 'development',
devtool: isProduction ? 'source-map' : 'eval-source-map',
target: 'web',
stats: {
children: false
@@ -186,7 +187,7 @@ module.exports = (env) => {
loose: true,
debug: false,
useBuiltIns: 'entry',
corejs: 3
corejs: '3.39'
}
]
]

View File

@@ -41,6 +41,7 @@ function HistoryDetails(props: HistoryDetailsProps) {
indexer,
releaseGroup,
movieMatchType,
releaseSource,
customFormatScore,
nzbInfoUrl,
downloadClient,
@@ -53,6 +54,31 @@ function HistoryDetails(props: HistoryDetailsProps) {
const downloadClientNameInfo = downloadClientName ?? downloadClient;
let releaseSourceMessage = '';
switch (releaseSource) {
case 'Unknown':
releaseSourceMessage = translate('Unknown');
break;
case 'Rss':
releaseSourceMessage = translate('Rss');
break;
case 'Search':
releaseSourceMessage = translate('Search');
break;
case 'UserInvokedSearch':
releaseSourceMessage = translate('UserInvokedSearch');
break;
case 'InteractiveSearch':
releaseSourceMessage = translate('InteractiveSearch');
break;
case 'ReleasePush':
releaseSourceMessage = translate('ReleasePush');
break;
default:
releaseSourceMessage = '';
}
return (
<DescriptionList>
<DescriptionListItem
@@ -88,6 +114,14 @@ function HistoryDetails(props: HistoryDetailsProps) {
/>
) : null}
{releaseSource ? (
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('ReleaseSource')}
data={releaseSourceMessage}
/>
) : null}
{nzbInfoUrl ? (
<span>
<DescriptionListItemTitle>

View File

@@ -37,7 +37,7 @@ interface HistoryDetailsModalProps {
sourceTitle: string;
data: HistoryData;
downloadId?: string;
isMarkingAsFailed: boolean;
isMarkingAsFailed?: boolean;
onMarkAsFailedPress: () => void;
onModalClose: () => void;
}

View File

@@ -154,9 +154,14 @@ function HistoryRow(props: HistoryRowProps) {
}
if (name === 'downloadClient') {
const downloadClientName =
'downloadClientName' in data ? data.downloadClientName : null;
const downloadClient =
'downloadClient' in data ? data.downloadClient : null;
return (
<TableRowCell key={name} className={styles.downloadClient}>
{'downloadClient' in data ? data.downloadClient : ''}
{downloadClientName ?? downloadClient ?? ''}
</TableRowCell>
);
}

View File

@@ -91,6 +91,10 @@
margin-left: 5px;
}
.genres {
pointer-events: all;
}
.links {
margin-left: 5px;
pointer-events: all;

View File

@@ -10,6 +10,7 @@ import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
import MovieStatusLabel from 'Movie/Details/MovieStatusLabel';
import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar';
import MovieGenres from 'Movie/MovieGenres';
import MoviePoster from 'Movie/MoviePoster';
import formatRuntime from 'Utilities/Date/formatRuntime';
import translate from 'Utilities/String/translate';
@@ -249,9 +250,7 @@ class AddNewMovieSearchResult extends Component {
name={icons.GENRE}
size={13}
/>
<span className={styles.genres}>
{genres.slice(0, 3).join(', ')}
</span>
<MovieGenres className={styles.genres} genres={genres} />
</Label> :
null
}
@@ -280,7 +279,7 @@ class AddNewMovieSearchResult extends Component {
}
canFlip={true}
kind={kinds.INVERSE}
position={tooltipPositions.BOTTOM}
position={tooltipPositions.TOP}
/>
{

View File

@@ -81,7 +81,6 @@ ImportMovieRow.propTypes = {
selectedMovie: PropTypes.object,
isExistingMovie: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
queued: PropTypes.bool.isRequired,
isSelected: PropTypes.bool,
onSelectedChange: PropTypes.func.isRequired,
onInputChange: PropTypes.func.isRequired

View File

@@ -131,7 +131,7 @@ class ImportMovieSelectMovie extends Component {
id={this._buttonId}
>
<Link
ref={ref}
// ref={ref}
className={styles.button}
component="div"
onPress={this.onPress}
@@ -255,7 +255,7 @@ class ImportMovieSelectMovie extends Component {
items.map((item) => {
return (
<ImportMovieSearchResultConnector
key={item.tvdbId}
key={item.tmdbId}
tmdbId={item.tmdbId}
title={item.title}
year={item.year}

View File

@@ -1,11 +1,16 @@
import Column from 'Components/Table/Column';
import { SortDirection } from 'Helpers/Props/sortDirections';
import { ValidationFailure } from 'typings/pending';
import { FilterBuilderProp, PropertyFilter } from './AppState';
export interface Error {
responseJSON: {
message: string;
};
status?: number;
responseJSON:
| {
message: string | undefined;
}
| ValidationFailure[]
| undefined;
}
export interface AppSectionDeleteState {
@@ -51,6 +56,16 @@ export interface AppSectionItemState<T> {
item: T;
}
export interface AppSectionProviderState<T>
extends AppSectionDeleteState,
AppSectionSaveState {
isFetching: boolean;
isPopulated: boolean;
error: Error;
items: T[];
pendingChanges: Partial<T>;
}
interface AppSectionState<T> {
isFetching: boolean;
isPopulated: boolean;

View File

@@ -1,15 +1,18 @@
import BlocklistAppState from './BlocklistAppState';
import CalendarAppState from './CalendarAppState';
import CommandAppState from './CommandAppState';
import HistoryAppState from './HistoryAppState';
import HistoryAppState, { MovieHistoryAppState } from './HistoryAppState';
import InteractiveImportAppState from './InteractiveImportAppState';
import MovieBlocklistAppState from './MovieBlocklistAppState';
import MovieCollectionAppState from './MovieCollectionAppState';
import MovieCreditAppState from './MovieCreditAppState';
import MovieFilesAppState from './MovieFilesAppState';
import MoviesAppState, { MovieIndexAppState } from './MoviesAppState';
import OrganizePreviewAppState from './OrganizePreviewAppState';
import ParseAppState from './ParseAppState';
import PathsAppState from './PathsAppState';
import QueueAppState from './QueueAppState';
import ReleasesAppState from './ReleasesAppState';
import RootFolderAppState from './RootFolderAppState';
import SettingsAppState from './SettingsAppState';
import SystemAppState from './SystemAppState';
@@ -66,14 +69,18 @@ interface AppState {
commands: CommandAppState;
history: HistoryAppState;
interactiveImport: InteractiveImportAppState;
movieBlocklist: MovieBlocklistAppState;
movieCollections: MovieCollectionAppState;
movieCredits: MovieCreditAppState;
movieFiles: MovieFilesAppState;
movieHistory: MovieHistoryAppState;
movieIndex: MovieIndexAppState;
movies: MoviesAppState;
organizePreview: OrganizePreviewAppState;
parse: ParseAppState;
paths: PathsAppState;
queue: QueueAppState;
releases: ReleasesAppState;
rootFolders: RootFolderAppState;
settings: SettingsAppState;
system: SystemAppState;

View File

@@ -5,6 +5,8 @@ import AppSectionState, {
} from 'App/State/AppSectionState';
import History from 'typings/History';
export type MovieHistoryAppState = AppSectionState<History>;
interface HistoryAppState
extends AppSectionState<History>,
AppSectionFilterState<History>,

View File

@@ -1,11 +1,20 @@
import AppSectionState from 'App/State/AppSectionState';
import RecentFolder from 'InteractiveImport/Folder/RecentFolder';
import ImportMode from 'InteractiveImport/ImportMode';
import InteractiveImport from 'InteractiveImport/InteractiveImport';
interface FavoriteFolder {
folder: string;
}
interface RecentFolder {
folder: string;
lastUsed: string;
}
interface InteractiveImportAppState extends AppSectionState<InteractiveImport> {
originalItems: InteractiveImport[];
importMode: ImportMode;
favoriteFolders: FavoriteFolder[];
recentFolders: RecentFolder[];
}

View File

@@ -0,0 +1,6 @@
import { AppSectionProviderState } from 'App/State/AppSectionState';
import Metadata from 'typings/Metadata';
type MetadataAppState = AppSectionProviderState<Metadata>;
export default MetadataAppState;

View File

@@ -0,0 +1,6 @@
import AppSectionState from 'App/State/AppSectionState';
import Blocklist from 'typings/Blocklist';
type MovieBlocklistAppState = AppSectionState<Blocklist>;
export default MovieBlocklistAppState;

View File

@@ -1,7 +1,11 @@
import AppSectionState from 'App/State/AppSectionState';
import AppSectionState, {
AppSectionSaveState,
} from 'App/State/AppSectionState';
import MovieCollection from 'typings/MovieCollection';
interface MovieCollectionAppState extends AppSectionState<MovieCollection> {
interface MovieCollectionAppState
extends AppSectionState<MovieCollection>,
AppSectionSaveState {
itemMap: Record<number, number>;
}

View File

@@ -1,6 +1,6 @@
import AppSectionState from 'App/State/AppSectionState';
import MovieCredit from 'typings/MovieCredit';
interface MovieCreditAppState extends AppSectionState<MovieCredit> {}
type MovieCreditAppState = AppSectionState<MovieCredit>;
export default MovieCreditAppState;

View File

@@ -64,6 +64,8 @@ interface MoviesAppState
deleteOptions: {
addImportExclusion: boolean;
};
pendingChanges: Partial<Movie>;
}
export default MoviesAppState;

View File

@@ -0,0 +1,13 @@
import ModelBase from 'App/ModelBase';
import AppSectionState from 'App/State/AppSectionState';
export interface OrganizePreviewModel extends ModelBase {
movieId: number;
movieFileId: number;
existingPath: string;
newPath: string;
}
type OrganizePreviewAppState = AppSectionState<OrganizePreviewModel>;
export default OrganizePreviewAppState;

View File

@@ -0,0 +1,10 @@
import AppSectionState, {
AppSectionFilterState,
} from 'App/State/AppSectionState';
import Release from 'typings/Release';
interface ReleasesAppState
extends AppSectionState<Release>,
AppSectionFilterState<Release> {}
export default ReleasesAppState;

View File

@@ -20,6 +20,7 @@ import NamingConfig from 'typings/Settings/NamingConfig';
import NamingExample from 'typings/Settings/NamingExample';
import ReleaseProfile from 'typings/Settings/ReleaseProfile';
import UiSettings from 'typings/Settings/UiSettings';
import MetadataAppState from './MetadataAppState';
export interface DownloadClientAppState
extends AppSectionState<DownloadClient>,
@@ -36,8 +37,7 @@ export interface NamingAppState
extends AppSectionItemState<NamingConfig>,
AppSectionSaveState {}
export interface NamingExamplesAppState
extends AppSectionItemState<NamingExample> {}
export type NamingExamplesAppState = AppSectionItemState<NamingExample>;
export interface ImportListAppState
extends AppSectionState<ImportList>,
@@ -97,6 +97,7 @@ interface SettingsAppState {
indexerFlags: IndexerFlagSettingsAppState;
indexers: IndexerAppState;
languages: LanguageSettingsAppState;
metadata: MetadataAppState;
naming: NamingAppState;
namingExamples: NamingExamplesAppState;
notifications: NotificationAppState;

View File

@@ -22,7 +22,7 @@ function createMapStateToProps() {
return {
...collection,
movies: [...collection.movies].sort((a, b) => b.year - a.year),
genres: Array.from(new Set(allGenres)).slice(0, 3)
genres: Array.from(new Set(allGenres))
};
}
);

View File

@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Link from 'Components/Link/Link';
import MonitorToggleButton from 'Components/MonitorToggleButton';
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
import EditMovieModal from 'Movie/Edit/EditMovieModal';
import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar';
import MoviePoster from 'Movie/MoviePoster';
import translate from 'Utilities/String/translate';
@@ -172,7 +172,7 @@ class CollectionMovie extends Component {
collectionId={collectionId}
/>
<EditMovieModalConnector
<EditMovieModal
isOpen={isEditMovieModalOpen}
movieId={id}
onModalClose={this.onEditMovieModalClose}

View File

@@ -10,6 +10,7 @@ import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import MonitorToggleButton from 'Components/MonitorToggleButton';
import { icons, sizes } from 'Helpers/Props';
import MovieGenres from 'Movie/MovieGenres';
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
import dimensions from 'Styles/Variables/dimensions';
import fonts from 'Styles/Variables/fonts';
@@ -242,12 +243,10 @@ class CollectionOverview extends Component {
size={sizes.MEDIUM}
>
<Icon
name={icons.PROFILE}
name={icons.GENRE}
size={13}
/>
<span className={styles.genres}>
{genres.join(', ')}
</span>
<MovieGenres className={styles.genres} genres={genres} />
</Label>
}

View File

@@ -16,6 +16,7 @@ import MovieFilterBuilderRowValue from './MovieFilterBuilderRowValue';
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
import QualityProfileFilterBuilderRowValue from './QualityProfileFilterBuilderRowValue';
import QueueStatusFilterBuilderRowValue from './QueueStatusFilterBuilderRowValue';
import ReleaseStatusFilterBuilderRowValue from './ReleaseStatusFilterBuilderRowValue';
import TagFilterBuilderRowValueConnector from './TagFilterBuilderRowValueConnector';
import styles from './FilterBuilderRow.css';
@@ -80,6 +81,9 @@ function getRowValueConnector(selectedFilterBuilderProp) {
case filterBuilderValueTypes.QUALITY_PROFILE:
return QualityProfileFilterBuilderRowValue;
case filterBuilderValueTypes.QUEUE_STATUS:
return QueueStatusFilterBuilderRowValue;
case filterBuilderValueTypes.MOVIE:
return MovieFilterBuilderRowValue;

View File

@@ -58,7 +58,7 @@ function getValue(input, selectedFilterBuilderProp) {
if (selectedFilterBuilderProp.type === filterBuilderTypes.NUMBER) {
const { numberFractionDigits = 0 } = selectedFilterBuilderProp;
return Number(input).toFixed(numberFractionDigits);
return Number(Number(input).toFixed(numberFractionDigits));
}
return input;

View File

@@ -0,0 +1,67 @@
import React from 'react';
import translate from 'Utilities/String/translate';
import FilterBuilderRowValue from './FilterBuilderRowValue';
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
const statusTagList = [
{
id: 'queued',
get name() {
return translate('Queued');
},
},
{
id: 'paused',
get name() {
return translate('Paused');
},
},
{
id: 'downloading',
get name() {
return translate('Downloading');
},
},
{
id: 'completed',
get name() {
return translate('Completed');
},
},
{
id: 'failed',
get name() {
return translate('Failed');
},
},
{
id: 'warning',
get name() {
return translate('Warning');
},
},
{
id: 'delay',
get name() {
return translate('Delay');
},
},
{
id: 'downloadClientUnavailable',
get name() {
return translate('DownloadClientUnavailable');
},
},
{
id: 'fallback',
get name() {
return translate('Fallback');
},
},
];
function QueueStatusFilterBuilderRowValue(props: FilterBuilderRowValueProps) {
return <FilterBuilderRowValue {...props} tagList={statusTagList} />;
}
export default QueueStatusFilterBuilderRowValue;

View File

@@ -2,7 +2,7 @@ import React from 'react';
import translate from 'Utilities/String/translate';
import FilterBuilderRowValue from './FilterBuilderRowValue';
const protocols = [
const statusTagList = [
{ id: 'tba', name: 'TBA' },
{
id: 'announced',
@@ -33,7 +33,7 @@ const protocols = [
function ReleaseStatusFilterBuilderRowValue(props) {
return (
<FilterBuilderRowValue
tagList={protocols}
tagList={statusTagList}
{...props}
/>
);

View File

@@ -1,156 +0,0 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import KeyValueListInputItem from './KeyValueListInputItem';
import styles from './KeyValueListInput.css';
class KeyValueListInput extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isFocused: false
};
}
//
// Listeners
onItemChange = (index, itemValue) => {
const {
name,
value,
onChange
} = this.props;
const newValue = [...value];
if (index == null) {
newValue.push(itemValue);
} else {
newValue.splice(index, 1, itemValue);
}
onChange({
name,
value: newValue
});
};
onRemoveItem = (index) => {
const {
name,
value,
onChange
} = this.props;
const newValue = [...value];
newValue.splice(index, 1);
onChange({
name,
value: newValue
});
};
onFocus = () => {
this.setState({
isFocused: true
});
};
onBlur = () => {
this.setState({
isFocused: false
});
const {
name,
value,
onChange
} = this.props;
const newValue = value.reduce((acc, v) => {
if (v.key || v.value) {
acc.push(v);
}
return acc;
}, []);
if (newValue.length !== value.length) {
onChange({
name,
value: newValue
});
}
};
//
// Render
render() {
const {
className,
value,
keyPlaceholder,
valuePlaceholder,
hasError,
hasWarning
} = this.props;
const { isFocused } = this.state;
return (
<div className={classNames(
className,
isFocused && styles.isFocused,
hasError && styles.hasError,
hasWarning && styles.hasWarning
)}
>
{
[...value, { key: '', value: '' }].map((v, index) => {
return (
<KeyValueListInputItem
key={index}
index={index}
keyValue={v.key}
value={v.value}
keyPlaceholder={keyPlaceholder}
valuePlaceholder={valuePlaceholder}
isNew={index === value.length}
onChange={this.onItemChange}
onRemove={this.onRemoveItem}
onFocus={this.onFocus}
onBlur={this.onBlur}
/>
);
})
}
</div>
);
}
}
KeyValueListInput.propTypes = {
className: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.arrayOf(PropTypes.object).isRequired,
hasError: PropTypes.bool,
hasWarning: PropTypes.bool,
keyPlaceholder: PropTypes.string,
valuePlaceholder: PropTypes.string,
onChange: PropTypes.func.isRequired
};
KeyValueListInput.defaultProps = {
className: styles.inputContainer,
value: []
};
export default KeyValueListInput;

View File

@@ -0,0 +1,104 @@
import classNames from 'classnames';
import React, { useCallback, useState } from 'react';
import { InputOnChange } from 'typings/inputs';
import KeyValueListInputItem from './KeyValueListInputItem';
import styles from './KeyValueListInput.css';
interface KeyValue {
key: string;
value: string;
}
export interface KeyValueListInputProps {
className?: string;
name: string;
value: KeyValue[];
hasError?: boolean;
hasWarning?: boolean;
keyPlaceholder?: string;
valuePlaceholder?: string;
onChange: InputOnChange<KeyValue[]>;
}
function KeyValueListInput({
className = styles.inputContainer,
name,
value = [],
hasError = false,
hasWarning = false,
keyPlaceholder,
valuePlaceholder,
onChange,
}: KeyValueListInputProps): JSX.Element {
const [isFocused, setIsFocused] = useState(false);
const handleItemChange = useCallback(
(index: number | null, itemValue: KeyValue) => {
const newValue = [...value];
if (index === null) {
newValue.push(itemValue);
} else {
newValue.splice(index, 1, itemValue);
}
onChange({ name, value: newValue });
},
[value, name, onChange]
);
const handleRemoveItem = useCallback(
(index: number) => {
const newValue = [...value];
newValue.splice(index, 1);
onChange({ name, value: newValue });
},
[value, name, onChange]
);
const onFocus = useCallback(() => setIsFocused(true), []);
const onBlur = useCallback(() => {
setIsFocused(false);
const newValue = value.reduce((acc: KeyValue[], v) => {
if (v.key || v.value) {
acc.push(v);
}
return acc;
}, []);
if (newValue.length !== value.length) {
onChange({ name, value: newValue });
}
}, [value, name, onChange]);
return (
<div
className={classNames(
className,
isFocused && styles.isFocused,
hasError && styles.hasError,
hasWarning && styles.hasWarning
)}
>
{[...value, { key: '', value: '' }].map((v, index) => (
<KeyValueListInputItem
key={index}
index={index}
keyValue={v.key}
value={v.value}
keyPlaceholder={keyPlaceholder}
valuePlaceholder={valuePlaceholder}
isNew={index === value.length}
onChange={handleItemChange}
onRemove={handleRemoveItem}
onFocus={onFocus}
onBlur={onBlur}
/>
))}
</div>
);
}
export default KeyValueListInput;

View File

@@ -5,13 +5,19 @@
&:last-child {
margin-bottom: 0;
border-bottom: 0;
}
}
.inputWrapper {
.keyInputWrapper {
flex: 1 0 0;
}
.valueInputWrapper {
flex: 1 0 0;
min-width: 40px;
}
.buttonWrapper {
flex: 0 0 22px;
}
@@ -20,4 +26,10 @@
.valueInput {
width: 100%;
border: none;
background-color: transparent;
color: var(--textColor);
&::placeholder {
color: var(--helpTextColor);
}
}

View File

@@ -2,10 +2,11 @@
// Please do not change this file!
interface CssExports {
'buttonWrapper': string;
'inputWrapper': string;
'itemContainer': string;
'keyInput': string;
'keyInputWrapper': string;
'valueInput': string;
'valueInputWrapper': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -1,124 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import IconButton from 'Components/Link/IconButton';
import { icons } from 'Helpers/Props';
import TextInput from './TextInput';
import styles from './KeyValueListInputItem.css';
class KeyValueListInputItem extends Component {
//
// Listeners
onKeyChange = ({ value: keyValue }) => {
const {
index,
value,
onChange
} = this.props;
onChange(index, { key: keyValue, value });
};
onValueChange = ({ value }) => {
// TODO: Validate here or validate at a lower level component
const {
index,
keyValue,
onChange
} = this.props;
onChange(index, { key: keyValue, value });
};
onRemovePress = () => {
const {
index,
onRemove
} = this.props;
onRemove(index);
};
onFocus = () => {
this.props.onFocus();
};
onBlur = () => {
this.props.onBlur();
};
//
// Render
render() {
const {
keyValue,
value,
keyPlaceholder,
valuePlaceholder,
isNew
} = this.props;
return (
<div className={styles.itemContainer}>
<div className={styles.inputWrapper}>
<TextInput
className={styles.keyInput}
name="key"
value={keyValue}
placeholder={keyPlaceholder}
onChange={this.onKeyChange}
onFocus={this.onFocus}
onBlur={this.onBlur}
/>
</div>
<div className={styles.inputWrapper}>
<TextInput
className={styles.valueInput}
name="value"
value={value}
placeholder={valuePlaceholder}
onChange={this.onValueChange}
onFocus={this.onFocus}
onBlur={this.onBlur}
/>
</div>
<div className={styles.buttonWrapper}>
{
isNew ?
null :
<IconButton
name={icons.REMOVE}
tabIndex={-1}
onPress={this.onRemovePress}
/>
}
</div>
</div>
);
}
}
KeyValueListInputItem.propTypes = {
index: PropTypes.number,
keyValue: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
keyPlaceholder: PropTypes.string.isRequired,
valuePlaceholder: PropTypes.string.isRequired,
isNew: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired,
onRemove: PropTypes.func.isRequired,
onFocus: PropTypes.func.isRequired,
onBlur: PropTypes.func.isRequired
};
KeyValueListInputItem.defaultProps = {
keyPlaceholder: 'Key',
valuePlaceholder: 'Value'
};
export default KeyValueListInputItem;

View File

@@ -0,0 +1,89 @@
import React, { useCallback } from 'react';
import IconButton from 'Components/Link/IconButton';
import { icons } from 'Helpers/Props';
import TextInput from './TextInput';
import styles from './KeyValueListInputItem.css';
interface KeyValueListInputItemProps {
index: number;
keyValue: string;
value: string;
keyPlaceholder?: string;
valuePlaceholder?: string;
isNew: boolean;
onChange: (index: number, itemValue: { key: string; value: string }) => void;
onRemove: (index: number) => void;
onFocus: () => void;
onBlur: () => void;
}
function KeyValueListInputItem({
index,
keyValue,
value,
keyPlaceholder = 'Key',
valuePlaceholder = 'Value',
isNew,
onChange,
onRemove,
onFocus,
onBlur,
}: KeyValueListInputItemProps): JSX.Element {
const handleKeyChange = useCallback(
({ value: keyValue }: { value: string }) => {
onChange(index, { key: keyValue, value });
},
[index, value, onChange]
);
const handleValueChange = useCallback(
({ value }: { value: string }) => {
onChange(index, { key: keyValue, value });
},
[index, keyValue, onChange]
);
const handleRemovePress = useCallback(() => {
onRemove(index);
}, [index, onRemove]);
return (
<div className={styles.itemContainer}>
<div className={styles.keyInputWrapper}>
<TextInput
className={styles.keyInput}
name="key"
value={keyValue}
placeholder={keyPlaceholder}
onChange={handleKeyChange}
onFocus={onFocus}
onBlur={onBlur}
/>
</div>
<div className={styles.valueInputWrapper}>
<TextInput
className={styles.valueInput}
name="value"
value={value}
placeholder={valuePlaceholder}
onChange={handleValueChange}
onFocus={onFocus}
onBlur={onBlur}
/>
</div>
<div className={styles.buttonWrapper}>
{isNew ? null : (
<IconButton
name={icons.REMOVE}
tabIndex={-1}
onPress={handleRemovePress}
/>
)}
</div>
</div>
);
}
export default KeyValueListInputItem;

View File

@@ -14,6 +14,8 @@ function getType({ type, selectOptionsProviderAction }) {
return inputTypes.CHECK;
case 'device':
return inputTypes.DEVICE;
case 'keyValueList':
return inputTypes.KEY_VALUE_LIST;
case 'password':
return inputTypes.PASSWORD;
case 'number':
@@ -137,6 +139,8 @@ ProviderFieldFormGroup.propTypes = {
type: PropTypes.string.isRequired,
advanced: PropTypes.bool.isRequired,
hidden: PropTypes.string,
isDisabled: PropTypes.bool,
provider: PropTypes.string,
pending: PropTypes.bool.isRequired,
errors: PropTypes.arrayOf(PropTypes.object).isRequired,
warnings: PropTypes.arrayOf(PropTypes.object).isRequired,

View File

@@ -1,7 +1,7 @@
import React, { ComponentPropsWithoutRef } from 'react';
import styles from './TableRowCell.css';
export interface TableRowCellProps extends ComponentPropsWithoutRef<'td'> {}
export type TableRowCellProps = ComponentPropsWithoutRef<'td'>;
export default function TableRowCell({
className = styles.cell,

View File

@@ -1,4 +1,5 @@
import React from 'react';
import { SortDirection } from 'Helpers/Props/sortDirections';
type PropertyFunction<T> = () => T;
@@ -9,6 +10,7 @@ interface Column {
className?: string;
columnLabel?: string;
isSortable?: boolean;
fixedSortDirection?: SortDirection;
isVisible: boolean;
isModifiable?: boolean;
}

View File

@@ -15,7 +15,7 @@ function TraktRating(props: TraktRatingProps) {
const { ratings, iconSize = 14, hideIcon = false } = props;
const traktImage =
'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTguMC4wLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+PCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj48c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiAgICAgdmlld0JveD0iMCAwIDE0NC44IDE0NC44IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAxNDQuOCAxNDQuOCIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PGc+ICAgIDxwYXRoIGZpbGw9IiNFRDIyMjQiIGQ9Ik0yOS41LDExMS44YzEwLjYsMTEuNiwyNS45LDE4LjgsNDIuOSwxOC44YzguNywwLDE2LjktMS45LDI0LjMtNS4zTDU2LjMsODVMMjkuNSwxMTEuOHoiLz4gICAgPHBhdGggZmlsbD0iI0VEMjIyNCIgZD0iTTU2LjEsNjAuNkwyNS41LDkxLjFMMjEuNCw4N2wzMi4yLTMyLjJoMGwzNy42LTM3LjZjLTUuOS0yLTEyLjItMy4xLTE4LjgtMy4xYy0zMi4yLDAtNTguMywyNi4xLTU4LjMsNTguMyAgICAgICBjMCwxMy4xLDQuMywyNS4yLDExLjcsMzVsMzAuNS0zMC41bDIuMSwybDQzLjcsNDMuN2MwLjktMC41LDEuNy0xLDIuNS0xLjZMNTYuMyw3Mi43TDI3LDEwMmwtNC4xLTQuMWwzMy40LTMzLjRsMi4xLDJsNTEsNTAuOSAgICAgICBjMC44LTAuNiwxLjUtMS4zLDIuMi0xLjlsLTU1LTU1TDU2LjEsNjAuNnoiLz4gICAgPHBhdGggZmlsbD0iI0VEMUMyNCIgZD0iTTExNS43LDExMS40YzkuMy0xMC4zLDE1LTI0LDE1LTM5YzAtMjMuNC0xMy44LTQzLjUtMzMuNi01Mi44TDYwLjQsNTYuMkwxMTUuNywxMTEuNHogTTc0LjUsNjYuOGwtNC4xLTQuMSAgICAgICBsMjguOS0yOC45bDQuMSw0LjFMNzQuNSw2Ni44eiBNMTAxLjksMjcuMUw2OC42LDYwLjRsLTQuMS00LjFMOTcuOCwyM0wxMDEuOSwyNy4xeiIvPiAgICA8Zz4gICAgICAgPGc+ICAgICAgICAgIDxwYXRoIGZpbGw9IiNFRDIyMjQiIGQ9Ik03Mi40LDE0NC44QzMyLjUsMTQ0LjgsMCwxMTIuMywwLDcyLjRDMCwzMi41LDMyLjUsMCw3Mi40LDBzNzIuNCwzMi41LDcyLjQsNzIuNCAgICAgICAgICAgICBDMTQ0LjgsMTEyLjMsMTEyLjMsMTQ0LjgsNzIuNCwxNDQuOHogTTcyLjQsNy4zQzM2LjUsNy4zLDcuMywzNi41LDcuMyw3Mi40czI5LjIsNjUuMSw2NS4xLDY1LjFzNjUuMS0yOS4yLDY1LjEtNjUuMSAgICAgICAgICAgICBTMTA4LjMsNy4zLDcyLjQsNy4zeiIvPiAgICAgICA8L2c+ICAgIDwvZz48L2c+PC9zdmc+';
'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyBpZD0iTGF5ZXJfMiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgNDggNDgiPgogIDxkZWZzPgogICAgPHN0eWxlPgogICAgICAuY2xzLTEgewogICAgICAgIGZpbGw6ICM5ZjQyYzY7CiAgICAgIH0KCiAgICAgIC5jbHMtMiB7CiAgICAgICAgZmlsbDogI2ZmZjsKICAgICAgfQogICAgPC9zdHlsZT4KICA8L2RlZnM+CiAgPGcgaWQ9Il94MkRfLXByb2R1Y3Rpb24iPgogICAgPGcgaWQ9ImxvZ29tYXJrLmNpcmNsZS5jb2xvciI+CiAgICAgIDxwYXRoIGlkPSJiYWNrZ3JvdW5kIiBjbGFzcz0iY2xzLTEiIGQ9Ik00OCwyNGMwLDYuNjItMi42OSwxMi42Mi03LjAzLDE2Ljk3LTQuMzQsNC4zNC0xMC4zNCw3LjAzLTE2Ljk3LDcuMDNDMTAuNzUsNDgsMCwzNy4yNSwwLDI0YzAtNi42MywyLjY5LTEyLjYzLDcuMDMtMTYuOTdDMTEuMzcsMi42OCwxNy4zNywwLDI0LDBzMTIuNjMsMi42OCwxNi45Nyw3LjAzYy4xNC4xNC4yNy4yOC40LjQyLjQ4LjUuOTQsMS4wMiwxLjM3LDEuNTYuMjEuMjYuNDEuNTIuNi43OS40My41Ny44MiwxLjE2LDEuMTgsMS43Ni4xOC4yOS4zNS41OC41MS44Ny4zNS42NC42OCwxLjI5Ljk2LDEuOTcsMS4zLDIuOTQsMi4wMSw2LjE4LDIuMDEsOS42WiIvPgogICAgICA8ZyBpZD0iY2hlY2tib3giPgogICAgICAgIDxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTEyLjkzLDE4LjY3bC0xLjQ3LDEuNDYsMTQuNCwxNC40LDEuNDctMS40Ny00LjMyLTQuMzEsMTkuNzMtMTkuNzRjLS40My0uNTQtLjg5LTEuMDYtMS4zNy0xLjU2bC0xOS44MywxOS44My04LjYxLTguNjFaTTI4LjAyLDMyLjM3bDEuNDYtMS40Ni0yLjE1LTIuMTYsMTcuMTktMTcuMTljLS4zNi0uNi0uNzUtMS4xOS0xLjE4LTEuNzZsLTE4Ljk0LDE4Ljk1LDMuNjIsMy42MlpNMzAuMTgsMzAuMjFsMTUuODEtMTUuODFjLS4yOC0uNjgtLjYxLTEuMzMtLjk2LTEuOTdsLTE2LjMyLDE2LjMyLDEuNDcsMS40NlpNMTMuNjIsMTcuOTdsNy45Miw3LjkyLDEuNDctMS40Ny03LjkyLTcuOTItMS40NywxLjQ3Wk0yNS4xNywyMi4yN2wtNy45Mi03LjkyLTEuNDcsMS40Nyw3LjkyLDcuOTIsMS40Ny0xLjQ3Wk0yNCw0MS4zMmMtOS41NSwwLTE3LjMyLTcuNzctMTcuMzItMTcuMzJTMTQuNDUsNi42NywyNCw2LjY3YzIuNiwwLDUuMTEuNTYsNy40NCwxLjY4bC44OS0xLjg3Yy0yLjYxLTEuMjUtNS40Mi0xLjg4LTguMzMtMS44OEMxMy4zMSw0LjYsNC42MSwxMy4zLDQuNjEsMjRzOC43LDE5LjQsMTkuNCwxOS40YzcuNjQsMCwxNC41OS00LjUxLDE3LjcxLTExLjQ4bC0xLjg5LS44NWMtMi43OSw2LjIzLTksMTAuMjYtMTUuODIsMTAuMjZaIi8+CiAgICAgIDwvZz4KICAgIDwvZz4KICA8L2c+Cjwvc3ZnPg==';
const { value = 0, votes = 0 } = ratings.trakt;

View File

@@ -1,5 +1,5 @@
{
"name": "Radarr",
"name": "__INSTANCE_NAME__",
"icons": [
{
"src": "__URL_BASE__/Content/Images/Icons/android-chrome-192x192.png",

View File

@@ -85,10 +85,16 @@ $hoverScale: 1.05;
flex: 1 0 auto;
}
.overviewContainer {
display: flex;
justify-content: space-between;
flex: 0 1 1000px;
flex-direction: column;
}
.overview {
composes: link;
flex: 0 1 1000px;
overflow: hidden;
min-height: 0;
}

View File

@@ -11,6 +11,7 @@ interface CssExports {
'link': string;
'lists': string;
'overview': string;
'overviewContainer': string;
'poster': string;
'posterContainer': string;
'title': string;

View File

@@ -133,14 +133,20 @@ class DiscoverMovieOverview extends Component {
/>
</div>
<MoviePoster
className={styles.poster}
<Link
className={styles.link}
style={elementStyle}
images={images}
size={250}
lazy={false}
overflow={true}
/>
{...linkProps}
>
<MoviePoster
className={styles.poster}
style={elementStyle}
images={images}
size={250}
lazy={false}
overflow={true}
/>
</Link>
</div>
</div>
@@ -242,11 +248,13 @@ class DiscoverMovieOverview extends Component {
</div>
<div className={styles.details}>
<div className={styles.overview}>
<TextTruncate
line={Math.floor(overviewHeight / (defaultFontSize * lineHeight))}
text={overview}
/>
<div className={styles.overviewContainer}>
<Link className={styles.overview} {...linkProps}>
<TextTruncate
line={Math.floor(overviewHeight / (defaultFontSize * lineHeight))}
text={overview}
/>
</Link>
</div>
<DiscoverMovieOverviewInfo
@@ -255,7 +263,6 @@ class DiscoverMovieOverview extends Component {
{...overviewOptions}
{...otherProps}
/>
</div>
</div>
</div>

View File

@@ -49,7 +49,7 @@ function isVisible(row, props) {
valueProp
} = row;
return _.has(props, valueProp) && (_.get(props, showProp) || props.sortKey === name);
return _.has(props, valueProp) && _.get(props, valueProp) !== null && (props[showProp] || props.sortKey === name);
}
function getInfoRowProps(row, props) {

View File

@@ -50,15 +50,13 @@ function DiscoverMoviePosterInfo(props) {
}
if (sortKey === 'inCinemas' && inCinemas) {
const inCinemasDate = getRelativeDate(
inCinemas,
const inCinemasDate = getRelativeDate({
date: inCinemas,
shortDateFormat,
showRelativeDates,
{
timeFormat,
timeForToday: false
}
);
timeFormat,
timeForToday: false
});
return (
<div className={styles.info} title={translate('InCinemas')}>
@@ -68,15 +66,13 @@ function DiscoverMoviePosterInfo(props) {
}
if (sortKey === 'digitalRelease' && digitalRelease) {
const digitalReleaseDate = getRelativeDate(
digitalRelease,
const digitalReleaseDate = getRelativeDate({
date: digitalRelease,
shortDateFormat,
showRelativeDates,
{
timeFormat,
timeForToday: false
}
);
timeFormat,
timeForToday: false
});
return (
<div className={styles.info} title={translate('DigitalRelease')}>
@@ -86,15 +82,13 @@ function DiscoverMoviePosterInfo(props) {
}
if (sortKey === 'physicalRelease' && physicalRelease) {
const physicalReleaseDate = getRelativeDate(
physicalRelease,
const physicalReleaseDate = getRelativeDate({
date: physicalRelease,
shortDateFormat,
showRelativeDates,
{
timeFormat,
timeForToday: false
}
);
timeFormat,
timeForToday: false
});
return (
<div className={styles.info} title={translate('PhysicalRelease')}>

View File

@@ -8,6 +8,7 @@ export const LANGUAGE = 'language';
export const PROTOCOL = 'protocol';
export const QUALITY = 'quality';
export const QUALITY_PROFILE = 'qualityProfile';
export const QUEUE_STATUS = 'queueStatus';
export const MOVIE = 'movie';
export const RELEASE_STATUS = 'releaseStatus';
export const MINIMUM_AVAILABILITY = 'minimumAvailability';

View File

@@ -15,6 +15,7 @@ import {
faFileVideo as farFileVideo,
faFolder as farFolder,
faHdd as farHdd,
faHeart as farHeart,
faKeyboard as farKeyboard,
faObjectGroup as farObjectGroup,
faObjectUngroup as farObjectUngroup,
@@ -174,6 +175,7 @@ export const GENRE = fasTheaterMasks;
export const GROUP = farObjectGroup;
export const HEALTH = fasMedkit;
export const HEART = fasHeart;
export const HEART_OUTLINE = farHeart;
export const HISTORY = fasHistory;
export const HOUSEKEEPING = fasHome;
export const IGNORE = fasTimesCircle;

View File

@@ -0,0 +1,5 @@
.actions {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 70px;
}

View File

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

View File

@@ -0,0 +1,48 @@
import React, { SyntheticEvent, useCallback } from 'react';
import { useDispatch } from 'react-redux';
import IconButton from 'Components/Link/IconButton';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRowButton from 'Components/Table/TableRowButton';
import { icons } from 'Helpers/Props';
import { removeFavoriteFolder } from 'Store/Actions/interactiveImportActions';
import translate from 'Utilities/String/translate';
import styles from './FavoriteFolderRow.css';
interface FavoriteFolderRowProps {
folder: string;
onPress: (folder: string) => unknown;
}
function FavoriteFolderRow({ folder, onPress }: FavoriteFolderRowProps) {
const dispatch = useDispatch();
const handlePress = useCallback(() => {
onPress(folder);
}, [folder, onPress]);
const handleRemoveFavoritePress = useCallback(
(e: SyntheticEvent) => {
e.stopPropagation();
dispatch(removeFavoriteFolder({ folder }));
},
[folder, dispatch]
);
return (
<TableRowButton onPress={handlePress}>
<TableRowCell>{folder}</TableRowCell>
<TableRowCell className={styles.actions}>
<IconButton
title={translate('FavoriteFolderRemove')}
kind="danger"
name={icons.HEART}
onPress={handleRemoveFavoritePress}
/>
</TableRowCell>
</TableRowButton>
);
}
export default FavoriteFolderRow;

View File

@@ -1,7 +1,12 @@
.recentFoldersContainer {
.foldersContainer {
margin-top: 15px;
}
.foldersTitle {
border-bottom: 1px solid var(--borderColor);
font-size: 21px;
}
.buttonsContainer {
margin-top: 30px;
}

View File

@@ -5,7 +5,8 @@ interface CssExports {
'buttonContainer': string;
'buttonIcon': string;
'buttonsContainer': string;
'recentFoldersContainer': string;
'foldersContainer': string;
'foldersTitle': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
@@ -14,14 +14,23 @@ import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { icons, kinds, sizes } from 'Helpers/Props';
import { executeCommand } from 'Store/Actions/commandActions';
import {
addRecentFolder,
removeRecentFolder,
} from 'Store/Actions/interactiveImportActions';
import { addRecentFolder } from 'Store/Actions/interactiveImportActions';
import translate from 'Utilities/String/translate';
import FavoriteFolderRow from './FavoriteFolderRow';
import RecentFolderRow from './RecentFolderRow';
import styles from './InteractiveImportSelectFolderModalContent.css';
const favoriteFoldersColumns = [
{
name: 'folder',
label: () => translate('Folder'),
},
{
name: 'actions',
label: '',
},
];
const recentFoldersColumns = [
{
name: 'folder',
@@ -49,15 +58,22 @@ function InteractiveImportSelectFolderModalContent(
const { modalTitle, onFolderSelect, onModalClose } = props;
const [folder, setFolder] = useState('');
const dispatch = useDispatch();
const recentFolders = useSelector(
const { favoriteFolders, recentFolders } = useSelector(
createSelector(
(state: AppState) => state.interactiveImport.recentFolders,
(recentFolders) => {
return recentFolders;
(state: AppState) => state.interactiveImport,
(interactiveImport) => {
return {
favoriteFolders: interactiveImport.favoriteFolders,
recentFolders: interactiveImport.recentFolders,
};
}
)
);
const favoriteFolderMap = useMemo(() => {
return new Map(favoriteFolders.map((f) => [f.folder, f]));
}, [favoriteFolders]);
const onPathChange = useCallback(
({ value }: { value: string }) => {
setFolder(value);
@@ -90,13 +106,6 @@ function InteractiveImportSelectFolderModalContent(
onFolderSelect(folder);
}, [folder, onFolderSelect, dispatch]);
const onRemoveRecentFolderPress = useCallback(
(folderToRemove: string) => {
dispatch(removeRecentFolder({ folder: folderToRemove }));
},
[dispatch]
);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
@@ -110,8 +119,34 @@ function InteractiveImportSelectFolderModalContent(
onChange={onPathChange}
/>
{favoriteFolders.length ? (
<div className={styles.foldersContainer}>
<div className={styles.foldersTitle}>
{translate('FavoriteFolders')}
</div>
<Table columns={favoriteFoldersColumns}>
<TableBody>
{favoriteFolders.map((favoriteFolder) => {
return (
<FavoriteFolderRow
key={favoriteFolder.folder}
folder={favoriteFolder.folder}
onPress={onRecentPathPress}
/>
);
})}
</TableBody>
</Table>
</div>
) : null}
{recentFolders.length ? (
<div className={styles.recentFoldersContainer}>
<div className={styles.foldersContainer}>
<div className={styles.foldersTitle}>
{translate('RecentFolders')}
</div>
<Table columns={recentFoldersColumns}>
<TableBody>
{recentFolders
@@ -123,8 +158,8 @@ function InteractiveImportSelectFolderModalContent(
key={recentFolder.folder}
folder={recentFolder.folder}
lastUsed={recentFolder.lastUsed}
isFavorite={favoriteFolderMap.has(recentFolder.folder)}
onPress={onRecentPathPress}
onRemoveRecentFolderPress={onRemoveRecentFolderPress}
/>
);
})}

View File

@@ -1,6 +0,0 @@
interface RecentFolder {
folder: string;
lastUsed: string;
}
export default RecentFolder;

View File

@@ -1,5 +1,5 @@
.actions {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 40px;
width: 70px;
}

View File

@@ -1,65 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import IconButton from 'Components/Link/IconButton';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRowButton from 'Components/Table/TableRowButton';
import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './RecentFolderRow.css';
class RecentFolderRow extends Component {
//
// Listeners
onPress = () => {
this.props.onPress(this.props.folder);
};
onRemovePress = (event) => {
event.stopPropagation();
const {
folder,
onRemoveRecentFolderPress
} = this.props;
onRemoveRecentFolderPress(folder);
};
//
// Render
render() {
const {
folder,
lastUsed
} = this.props;
return (
<TableRowButton onPress={this.onPress}>
<TableRowCell>{folder}</TableRowCell>
<RelativeDateCell date={lastUsed} />
<TableRowCell className={styles.actions}>
<IconButton
title={translate('Remove')}
name={icons.REMOVE}
onPress={this.onRemovePress}
/>
</TableRowCell>
</TableRowButton>
);
}
}
RecentFolderRow.propTypes = {
folder: PropTypes.string.isRequired,
lastUsed: PropTypes.string.isRequired,
onPress: PropTypes.func.isRequired,
onRemoveRecentFolderPress: PropTypes.func.isRequired
};
export default RecentFolderRow;

View File

@@ -0,0 +1,85 @@
import React, { SyntheticEvent, useCallback } from 'react';
import { useDispatch } from 'react-redux';
import IconButton from 'Components/Link/IconButton';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRowButton from 'Components/Table/TableRowButton';
import { icons } from 'Helpers/Props';
import {
addFavoriteFolder,
removeFavoriteFolder,
removeRecentFolder,
} from 'Store/Actions/interactiveImportActions';
import translate from 'Utilities/String/translate';
import styles from './RecentFolderRow.css';
interface RecentFolderRowProps {
folder: string;
lastUsed: string;
isFavorite: boolean;
onPress: (folder: string) => unknown;
}
function RecentFolderRow({
folder,
lastUsed,
isFavorite,
onPress,
}: RecentFolderRowProps) {
const dispatch = useDispatch();
const handlePress = useCallback(() => {
onPress(folder);
}, [folder, onPress]);
const handleFavoritePress = useCallback(
(e: SyntheticEvent) => {
e.stopPropagation();
if (isFavorite) {
dispatch(removeFavoriteFolder({ folder }));
} else {
dispatch(addFavoriteFolder({ folder }));
}
},
[folder, isFavorite, dispatch]
);
const handleRemovePress = useCallback(
(e: SyntheticEvent) => {
e.stopPropagation();
dispatch(removeRecentFolder({ folder }));
},
[folder, dispatch]
);
return (
<TableRowButton onPress={handlePress}>
<TableRowCell>{folder}</TableRowCell>
<RelativeDateCell date={lastUsed} />
<TableRowCell className={styles.actions}>
<IconButton
title={
isFavorite
? translate('FavoriteFolderRemove')
: translate('FavoriteFolderAdd')
}
kind={isFavorite ? 'danger' : 'default'}
name={isFavorite ? icons.HEART : icons.HEART_OUTLINE}
onPress={handleFavoritePress}
/>
<IconButton
title={translate('Remove')}
name={icons.REMOVE}
onPress={handleRemovePress}
/>
</TableRowCell>
</TableRowButton>
);
}
export default RecentFolderRow;

View File

@@ -24,6 +24,7 @@ import {
reprocessInteractiveImportItems,
updateInteractiveImportItem,
} from 'Store/Actions/interactiveImportActions';
import CustomFormat from 'typings/CustomFormat';
import { SelectStateInputProps } from 'typings/props';
import Rejection from 'typings/Rejection';
import formatBytes from 'Utilities/Number/formatBytes';
@@ -52,7 +53,7 @@ interface InteractiveImportRowProps {
quality?: QualityModel;
languages?: Language[];
size: number;
customFormats?: object[];
customFormats?: CustomFormat[];
customFormatScore?: number;
indexerFlags: number;
rejections: Rejection[];

View File

@@ -2,6 +2,7 @@ import ModelBase from 'App/ModelBase';
import Language from 'Language/Language';
import Movie from 'Movie/Movie';
import { QualityModel } from 'Quality/Quality';
import CustomFormat from 'typings/CustomFormat';
import Rejection from 'typings/Rejection';
export interface InteractiveImportCommandOptions {
@@ -27,7 +28,7 @@ interface InteractiveImport extends ModelBase {
languages: Language[];
movie?: Movie;
qualityWeight: number;
customFormats: object[];
customFormats: CustomFormat[];
indexerFlags: number;
rejections: Rejection[];
movieFileId?: number;

View File

@@ -65,7 +65,7 @@ interface RowItemData {
}
function Row({ index, style, data }: ListChildComponentProps<RowItemData>) {
const { items, columns, onMovieSelect } = data;
const { items, onMovieSelect } = data;
const movie = index >= items.length ? null : items[index];
const handlePress = useCallback(() => {
@@ -88,13 +88,11 @@ function Row({ index, style, data }: ListChildComponentProps<RowItemData>) {
onPress={handlePress}
>
<SelectMovieRow
id={movie.id}
key={movie.id}
title={movie.title}
tmdbId={movie.tmdbId}
imdbId={movie.imdbId}
year={movie.year}
columns={columns}
onMovieSelect={onMovieSelect}
/>
</VirtualTableRowButton>
);

View File

@@ -1,55 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Label from 'Components/Label';
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
import styles from './SelectMovieRow.css';
class SelectMovieRow extends Component {
//
// Listeners
onPress = () => {
this.props.onMovieSelect(this.props.id);
};
//
// Render
render() {
return (
<>
<VirtualTableRowCell className={styles.title}>
{this.props.title}
</VirtualTableRowCell>
<VirtualTableRowCell className={styles.year}>
{this.props.year}
</VirtualTableRowCell>
<VirtualTableRowCell className={styles.imdbId}>
{
this.props.imdbId ?
<Label>{this.props.imdbId}</Label> :
null
}
</VirtualTableRowCell>
<VirtualTableRowCell className={styles.tmdbId}>
<Label>{this.props.tmdbId}</Label>
</VirtualTableRowCell>
</>
);
}
}
SelectMovieRow.propTypes = {
id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
tmdbId: PropTypes.number.isRequired,
imdbId: PropTypes.string,
year: PropTypes.number.isRequired,
onMovieSelect: PropTypes.func.isRequired
};
export default SelectMovieRow;

View File

@@ -0,0 +1,33 @@
import React from 'react';
import Label from 'Components/Label';
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
import styles from './SelectMovieRow.css';
interface SelectMovieRowProps {
title: string;
tmdbId: number;
imdbId?: string;
year: number;
}
function SelectMovieRow({ title, year, tmdbId, imdbId }: SelectMovieRowProps) {
return (
<>
<VirtualTableRowCell className={styles.title}>
{title}
</VirtualTableRowCell>
<VirtualTableRowCell className={styles.year}>{year}</VirtualTableRowCell>
<VirtualTableRowCell className={styles.imdbId}>
{imdbId ? <Label>{imdbId}</Label> : null}
</VirtualTableRowCell>
<VirtualTableRowCell className={styles.tmdbId}>
<Label>{tmdbId}</Label>
</VirtualTableRowCell>
</>
);
}
export default SelectMovieRow;

View File

@@ -1,240 +0,0 @@
import PropTypes from 'prop-types';
import React, { Fragment } from 'react';
import Alert from 'Components/Alert';
import Icon from 'Components/Icon';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu';
import PageMenuButton from 'Components/Menu/PageMenuButton';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { align, icons, kinds, sortDirections } from 'Helpers/Props';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import InteractiveSearchFilterModalConnector from './InteractiveSearchFilterModalConnector';
import InteractiveSearchRowConnector from './InteractiveSearchRowConnector';
import styles from './InteractiveSearch.css';
const columns = [
{
name: 'protocol',
label: () => translate('Source'),
isSortable: true,
isVisible: true
},
{
name: 'age',
label: () => translate('Age'),
isSortable: true,
isVisible: true
},
{
name: 'title',
label: () => translate('Title'),
isSortable: true,
isVisible: true
},
{
name: 'indexer',
label: () => translate('Indexer'),
isSortable: true,
isVisible: true
},
{
name: 'history',
label: () => translate('History'),
isSortable: true,
fixedSortDirection: sortDirections.ASCENDING,
isVisible: true
},
{
name: 'size',
label: () => translate('Size'),
isSortable: true,
isVisible: true
},
{
name: 'peers',
label: () => translate('Peers'),
isSortable: true,
isVisible: true
},
{
name: 'languages',
label: () => translate('Language'),
isSortable: true,
isVisible: true
},
{
name: 'qualityWeight',
label: () => translate('Quality'),
isSortable: true,
isVisible: true
},
{
name: 'customFormatScore',
label: React.createElement(Icon, {
name: icons.SCORE,
title: () => translate('CustomFormatScore')
}),
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, {
name: icons.DANGER,
title: () => translate('Rejections')
}),
isSortable: true,
fixedSortDirection: sortDirections.ASCENDING,
isVisible: true
},
{
name: 'releaseWeight',
label: React.createElement(Icon, { name: icons.DOWNLOAD }),
isSortable: true,
fixedSortDirection: sortDirections.ASCENDING,
isVisible: true
}
];
function InteractiveSearch(props) {
const {
searchPayload,
isFetching,
isPopulated,
error,
totalReleasesCount,
items,
selectedFilterKey,
filters,
customFilters,
sortKey,
sortDirection,
longDateFormat,
timeFormat,
onSortPress,
onFilterSelect,
onGrabPress
} = props;
const errorMessage = getErrorMessage(error);
const type = 'movies';
return (
<div>
<div className={styles.filterMenuContainer}>
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
buttonComponent={PageMenuButton}
filterModalConnectorComponent={InteractiveSearchFilterModalConnector}
filterModalConnectorComponentProps={{ type }}
onFilterSelect={onFilterSelect}
/>
</div>
{
isFetching ? <LoadingIndicator /> : null
}
{
!isFetching && error ?
<Alert kind={kinds.DANGER} className={styles.alert}>
{
errorMessage ?
<Fragment>
{translate('InteractiveSearchResultsFailedErrorMessage', { message: errorMessage.charAt(0).toLowerCase() + errorMessage.slice(1) })}
</Fragment> :
translate('MovieSearchResultsLoadError')
}
</Alert> :
null
}
{
!isFetching && isPopulated && !totalReleasesCount ?
<Alert kind={kinds.INFO} className={styles.alert}>
{translate('NoResultsFound')}
</Alert> :
null
}
{
!!totalReleasesCount && isPopulated && !items.length ?
<Alert kind={kinds.WARNING} className={styles.alert}>
{translate('AllResultsHiddenFilter')}
</Alert> :
null
}
{
isPopulated && !!items.length ?
<Table
columns={columns}
sortKey={sortKey}
sortDirection={sortDirection}
onSortPress={onSortPress}
>
<TableBody>
{
items.map((item) => {
return (
<InteractiveSearchRowConnector
key={`${item.indexerId}-${item.guid}`}
{...item}
searchPayload={searchPayload}
longDateFormat={longDateFormat}
timeFormat={timeFormat}
onGrabPress={onGrabPress}
/>
);
})
}
</TableBody>
</Table> :
null
}
{
totalReleasesCount !== items.length && !!items.length ?
<Alert kind={kinds.INFO} className={styles.alert}>
{translate('SomeResultsHiddenFilter')}
</Alert> :
null
}
</div>
);
}
InteractiveSearch.propTypes = {
searchPayload: PropTypes.object.isRequired,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
totalReleasesCount: PropTypes.number.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
sortKey: PropTypes.string,
sortDirection: PropTypes.string,
longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
onSortPress: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired,
onGrabPress: PropTypes.func.isRequired
};
export default InteractiveSearch;

View File

@@ -0,0 +1,263 @@
import React, { useCallback, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState';
import ReleasesAppState from 'App/State/ReleasesAppState';
import Alert from 'Components/Alert';
import Icon from 'Components/Icon';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu';
import PageMenuButton from 'Components/Menu/PageMenuButton';
import Column from 'Components/Table/Column';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { align, icons, kinds, sortDirections } from 'Helpers/Props';
import { SortDirection } from 'Helpers/Props/sortDirections';
import { fetchMovieBlocklist } from 'Store/Actions/movieBlocklistActions';
import { fetchMovieHistory } from 'Store/Actions/movieHistoryActions';
import {
fetchReleases,
grabRelease,
setReleasesFilter,
setReleasesSort,
} from 'Store/Actions/releaseActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import InteractiveSearchFilterModal from './InteractiveSearchFilterModal';
import InteractiveSearchPayload from './InteractiveSearchPayload';
import InteractiveSearchRow from './InteractiveSearchRow';
import styles from './InteractiveSearch.css';
const columns: Column[] = [
{
name: 'protocol',
label: () => translate('Source'),
isSortable: true,
isVisible: true,
},
{
name: 'age',
label: () => translate('Age'),
isSortable: true,
isVisible: true,
},
{
name: 'title',
label: () => translate('Title'),
isSortable: true,
isVisible: true,
},
{
name: 'indexer',
label: () => translate('Indexer'),
isSortable: true,
isVisible: true,
},
{
name: 'history',
label: () => translate('History'),
isSortable: true,
fixedSortDirection: sortDirections.ASCENDING,
isVisible: true,
},
{
name: 'size',
label: () => translate('Size'),
isSortable: true,
isVisible: true,
},
{
name: 'peers',
label: () => translate('Peers'),
isSortable: true,
isVisible: true,
},
{
name: 'languages',
label: () => translate('Language'),
isSortable: true,
isVisible: true,
},
{
name: 'qualityWeight',
label: () => translate('Quality'),
isSortable: true,
isVisible: true,
},
{
name: 'customFormatScore',
label: React.createElement(Icon, {
name: icons.SCORE,
title: () => translate('CustomFormatScore'),
}),
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, {
name: icons.DANGER,
title: () => translate('Rejections'),
}),
isSortable: true,
fixedSortDirection: sortDirections.ASCENDING,
isVisible: true,
},
{
name: 'releaseWeight',
label: React.createElement(Icon, { name: icons.DOWNLOAD }),
isSortable: true,
fixedSortDirection: sortDirections.ASCENDING,
isVisible: true,
},
];
interface InteractiveSearchProps {
searchPayload: InteractiveSearchPayload;
}
function InteractiveSearch({ searchPayload }: InteractiveSearchProps) {
const {
isFetching,
isPopulated,
error,
items,
totalItems,
selectedFilterKey,
filters,
customFilters,
sortKey,
sortDirection,
}: ReleasesAppState & ClientSideCollectionAppState = useSelector(
createClientSideCollectionSelector('releases')
);
const dispatch = useDispatch();
const handleFilterSelect = useCallback(
(selectedFilterKey: string | number) => {
dispatch(setReleasesFilter({ selectedFilterKey }));
},
[dispatch]
);
const handleSortPress = useCallback(
(sortKey: string, sortDirection?: SortDirection) => {
dispatch(setReleasesSort({ sortKey, sortDirection }));
},
[dispatch]
);
const handleGrabPress = useCallback(
(payload: object) => {
dispatch(grabRelease(payload));
},
[dispatch]
);
useEffect(
() => {
// Only fetch releases if they are not already being fetched and not yet populated.
if (!isFetching && !isPopulated) {
dispatch(fetchReleases(searchPayload));
const { movieId } = searchPayload;
if (movieId) {
dispatch(fetchMovieBlocklist({ movieId }));
dispatch(fetchMovieHistory({ movieId }));
}
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
const errorMessage = getErrorMessage(error);
return (
<div>
<div className={styles.filterMenuContainer}>
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
buttonComponent={PageMenuButton}
filterModalConnectorComponent={InteractiveSearchFilterModal}
filterModalConnectorComponentProps={{ type: 'movies' }}
onFilterSelect={handleFilterSelect}
/>
</div>
{isFetching ? <LoadingIndicator /> : null}
{!isFetching && error ? (
<Alert kind={kinds.DANGER} className={styles.alert}>
{errorMessage ? (
<>
{translate('InteractiveSearchResultsFailedErrorMessage', {
message:
errorMessage.charAt(0).toLowerCase() + errorMessage.slice(1),
})}
</>
) : (
translate('MovieSearchResultsLoadError')
)}
</Alert>
) : null}
{!isFetching && isPopulated && !totalItems ? (
<Alert kind={kinds.INFO} className={styles.alert}>
{translate('NoResultsFound')}
</Alert>
) : null}
{!!totalItems && isPopulated && !items.length ? (
<Alert kind={kinds.WARNING} className={styles.alert}>
{translate('AllResultsHiddenFilter')}
</Alert>
) : null}
{isPopulated && !!items.length ? (
<Table
columns={columns}
sortKey={sortKey}
sortDirection={sortDirection}
onSortPress={handleSortPress}
>
<TableBody>
{items.map((item) => {
return (
<InteractiveSearchRow
key={`${item.indexerId}-${item.guid}`}
{...item}
searchPayload={searchPayload}
onGrabPress={handleGrabPress}
/>
);
})}
</TableBody>
</Table>
) : null}
{totalItems !== items.length && !!items.length ? (
<Alert kind={kinds.INFO} className={styles.alert}>
{translate('SomeResultsHiddenFilter')}
</Alert>
) : null}
</div>
);
}
export default InteractiveSearch;

View File

@@ -1,109 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { clearMovieHistory, fetchMovieHistory } from 'Store/Actions/movieHistoryActions';
import * as releaseActions from 'Store/Actions/releaseActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import InteractiveSearch from './InteractiveSearch';
function createMapStateToProps(appState) {
return createSelector(
(state) => state.releases.items.length,
createClientSideCollectionSelector('releases'),
createUISettingsSelector(),
(totalReleasesCount, releases, uiSettings) => {
return {
totalReleasesCount,
longDateFormat: uiSettings.longDateFormat,
timeFormat: uiSettings.timeFormat,
...releases
};
}
);
}
function createMapDispatchToProps(dispatch, props) {
return {
dispatchFetchReleases(payload) {
dispatch(releaseActions.fetchReleases(payload));
},
dispatchFetchMovieHistory({ movieId }) {
dispatch(fetchMovieHistory({ movieId }));
},
dispatchClearMovieHistory() {
dispatch(clearMovieHistory());
},
onSortPress(sortKey, sortDirection) {
dispatch(releaseActions.setReleasesSort({ sortKey, sortDirection }));
},
onFilterSelect(selectedFilterKey) {
dispatch(releaseActions.setReleasesFilter({ selectedFilterKey }));
},
onGrabPress(payload) {
dispatch(releaseActions.grabRelease(payload));
}
};
}
class InteractiveSearchConnector extends Component {
//
// Lifecycle
componentDidMount() {
const {
searchPayload,
isPopulated,
dispatchFetchReleases,
dispatchFetchMovieHistory
} = this.props;
// If search results are not yet isPopulated fetch them,
// otherwise re-show the existing props.
if (!isPopulated) {
dispatchFetchReleases(searchPayload);
}
dispatchFetchMovieHistory(searchPayload);
}
componentWillUnmount() {
this.props.dispatchClearMovieHistory();
}
//
// Render
render() {
const {
dispatchFetchReleases,
dispatchFetchMovieHistory,
dispatchClearMovieHistory,
...otherProps
} = this.props;
return (
<InteractiveSearch
{...otherProps}
/>
);
}
}
InteractiveSearchConnector.propTypes = {
searchPayload: PropTypes.object.isRequired,
isPopulated: PropTypes.bool.isRequired,
dispatchFetchReleases: PropTypes.func.isRequired,
dispatchFetchMovieHistory: PropTypes.func.isRequired,
dispatchClearMovieHistory: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveSearchConnector);

View File

@@ -0,0 +1,55 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import FilterModal from 'Components/Filter/FilterModal';
import { setReleasesFilter } from 'Store/Actions/releaseActions';
function createReleasesSelector() {
return createSelector(
(state: AppState) => state.releases.items,
(releases) => {
return releases;
}
);
}
function createFilterBuilderPropsSelector() {
return createSelector(
(state: AppState) => state.releases.filterBuilderProps,
(filterBuilderProps) => {
return filterBuilderProps;
}
);
}
interface InteractiveSearchFilterModalProps {
isOpen: boolean;
}
export default function InteractiveSearchFilterModal({
...otherProps
}: InteractiveSearchFilterModalProps) {
const sectionItems = useSelector(createReleasesSelector());
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
const dispatch = useDispatch();
const dispatchSetFilter = useCallback(
(payload: unknown) => {
dispatch(setReleasesFilter(payload));
},
[dispatch]
);
return (
<FilterModal
// TODO: Don't spread all the props
{...otherProps}
sectionItems={sectionItems}
filterBuilderProps={filterBuilderProps}
customFilterType="releases"
dispatchSetFilter={dispatchSetFilter}
/>
);
}

View File

@@ -1,29 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import FilterModal from 'Components/Filter/FilterModal';
import { setReleasesFilter } from 'Store/Actions/releaseActions';
function createMapStateToProps() {
return createSelector(
(state) => state.releases.items,
(state) => state.releases.filterBuilderProps,
(sectionItems, filterBuilderProps) => {
return {
sectionItems,
filterBuilderProps,
customFilterType: 'releases'
};
}
);
}
function createMapDispatchToProps(dispatch, props) {
return {
dispatchSetFilter(payload) {
const action = setReleasesFilter;
dispatch(action(payload));
}
};
}
export default connect(createMapStateToProps, createMapDispatchToProps)(FilterModal);

View File

@@ -0,0 +1,7 @@
interface MovieSearchPayload {
movieId: number;
}
type InteractiveSearchPayload = MovieSearchPayload;
export default InteractiveSearchPayload;

View File

@@ -1,5 +1,8 @@
import React, { useCallback, useState } from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
import AppState from 'App/State/AppState';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
@@ -8,21 +11,18 @@ 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 type DownloadProtocol from 'DownloadClient/DownloadProtocol';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import Language from 'Language/Language';
import MovieFormats from 'Movie/MovieFormats';
import MovieLanguages from 'Movie/MovieLanguages';
import MovieQuality from 'Movie/MovieQuality';
import { QualityModel } from 'Quality/Quality';
import CustomFormat from 'typings/CustomFormat';
import MovieBlocklist from 'typings/MovieBlocklist';
import MovieHistory from 'typings/MovieHistory';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import Release from 'typings/Release';
import formatDateTime from 'Utilities/Date/formatDateTime';
import formatAge from 'Utilities/Number/formatAge';
import formatBytes from 'Utilities/Number/formatBytes';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import translate from 'Utilities/String/translate';
import InteractiveSearchPayload from './InteractiveSearchPayload';
import OverrideMatchModal from './OverrideMatch/OverrideMatchModal';
import Peers from './Peers';
import styles from './InteractiveSearchRow.css';
@@ -71,37 +71,42 @@ function getDownloadTooltip(
return translate('AddToDownloadQueue');
}
interface InteractiveSearchRowProps {
guid: string;
protocol: DownloadProtocol;
age: number;
ageHours: number;
ageMinutes: number;
publishDate: string;
title: string;
infoUrl: string;
indexerId: number;
indexer: string;
size: number;
seeders?: number;
leechers?: number;
quality: QualityModel;
languages: Language[];
customFormats: CustomFormat[];
customFormatScore: number;
mappedMovieId?: number;
indexerFlags: string[];
rejections: string[];
downloadAllowed: boolean;
isGrabbing: boolean;
isGrabbed: boolean;
grabError?: string;
historyFailedData?: MovieHistory;
historyGrabbedData?: MovieHistory;
blocklistData?: MovieBlocklist;
longDateFormat: string;
timeFormat: string;
searchPayload: object;
function releaseHistorySelector({ guid }: Release) {
return createSelector(
(state: AppState) => state.movieHistory.items,
(state: AppState) => state.movieBlocklist.items,
(movieHistory, movieBlocklist) => {
let historyFailedData = null;
let blocklistedData = null;
const historyGrabbedData = movieHistory.find(
({ eventType, data }) =>
eventType === 'grabbed' && 'guid' in data && data.guid === guid
);
if (historyGrabbedData) {
historyFailedData = movieHistory.find(
({ eventType, sourceTitle }) =>
eventType === 'downloadFailed' &&
sourceTitle === historyGrabbedData.sourceTitle
);
blocklistedData = movieBlocklist.find(
(item) => item.sourceTitle === historyGrabbedData.sourceTitle
);
}
return {
historyGrabbedData,
historyFailedData,
blocklistedData,
};
}
);
}
interface InteractiveSearchRowProps extends Release {
searchPayload: InteractiveSearchPayload;
onGrabPress(...args: unknown[]): void;
}
@@ -130,16 +135,18 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
downloadAllowed,
isGrabbing = false,
isGrabbed = false,
longDateFormat,
timeFormat,
grabError,
historyGrabbedData = {} as MovieHistory,
historyFailedData = {} as MovieHistory,
blocklistData = {} as MovieBlocklist,
searchPayload,
onGrabPress,
} = props;
const { longDateFormat, timeFormat } = useSelector(
createUISettingsSelector()
);
const { historyGrabbedData, historyFailedData, blocklistedData } =
useSelector(releaseHistorySelector(props));
const [isConfirmGrabModalOpen, setIsConfirmGrabModalOpen] = useState(false);
const [isOverrideModalOpen, setIsOverrideModalOpen] = useState(false);
@@ -211,44 +218,52 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
<TableRowCell className={styles.history}>
{historyGrabbedData?.date && !historyFailedData?.date ? (
<Icon
name={icons.DOWNLOADING}
kind={kinds.DEFAULT}
title={`${translate('Grabbed')}: ${formatDateTime(
historyGrabbedData.date,
longDateFormat,
timeFormat,
{ includeSeconds: true }
)}`}
<Tooltip
anchor={<Icon name={icons.DOWNLOADING} kind={kinds.DEFAULT} />}
tooltip={translate('GrabbedAt', {
date: formatDateTime(
historyGrabbedData.date,
longDateFormat,
timeFormat,
{ includeSeconds: true }
),
})}
kind={kinds.INVERSE}
position={tooltipPositions.LEFT}
/>
) : null}
{historyFailedData?.date ? (
<Icon
name={icons.DOWNLOADING}
kind={kinds.DANGER}
title={`${translate('Failed')}: ${formatDateTime(
historyFailedData.date,
longDateFormat,
timeFormat,
{ includeSeconds: true }
)}`}
<Tooltip
anchor={<Icon name={icons.DOWNLOADING} kind={kinds.DANGER} />}
tooltip={translate('FailedAt', {
date: formatDateTime(
historyFailedData.date,
longDateFormat,
timeFormat,
{ includeSeconds: true }
),
})}
kind={kinds.INVERSE}
position={tooltipPositions.LEFT}
/>
) : null}
{blocklistData?.date ? (
{blocklistedData?.date ? (
<Icon
className={
historyGrabbedData || historyFailedData ? styles.blocklist : ''
}
name={icons.BLOCKLIST}
kind={kinds.DANGER}
title={`${translate('Blocklisted')}: ${formatDateTime(
blocklistData.date,
longDateFormat,
timeFormat,
{ includeSeconds: true }
)}`}
title={translate('BlocklistedAt', {
date: formatDateTime(
blocklistedData.date,
longDateFormat,
timeFormat,
{ includeSeconds: true }
),
})}
/>
) : null}
</TableRowCell>

View File

@@ -1,62 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import InteractiveSearchRow from './InteractiveSearchRow';
function createMapStateToProps() {
return createSelector(
(state, { guid }) => guid,
(state) => state.movieHistory.items,
(state) => state.movieBlocklist.items,
(guid, movieHistory, movieBlocklist) => {
let blocklistData = {};
let historyFailedData = {};
const historyGrabbedData = movieHistory.find((movie) => movie.eventType === 'grabbed' && movie.data.guid === guid);
if (historyGrabbedData) {
historyFailedData = movieHistory.find((movie) => movie.eventType === 'downloadFailed' && movie.sourceTitle === historyGrabbedData.sourceTitle);
blocklistData = movieBlocklist.find((item) => item.sourceTitle === historyGrabbedData.sourceTitle);
}
return {
historyGrabbedData,
historyFailedData,
blocklistData
};
}
);
}
class InteractiveSearchRowConnector extends Component {
//
// Render
render() {
const {
historyGrabbedData,
historyFailedData,
blocklistData,
...otherProps
} = this.props;
return (
<InteractiveSearchRow
historyGrabbedData={historyGrabbedData}
historyFailedData={historyFailedData}
blocklistData={blocklistData}
{...otherProps}
/>
);
}
}
InteractiveSearchRowConnector.propTypes = {
historyGrabbedData: PropTypes.object,
historyFailedData: PropTypes.object,
blocklistData: PropTypes.object
};
export default connect(createMapStateToProps)(InteractiveSearchRowConnector);

View File

@@ -1,9 +1,8 @@
import PropTypes from 'prop-types';
import React from 'react';
import Label from 'Components/Label';
import { kinds } from 'Helpers/Props';
function getKind(seeders) {
function getKind(seeders: number = 0) {
if (seeders > 50) {
return kinds.PRIMARY;
}
@@ -19,7 +18,7 @@ function getKind(seeders) {
return kinds.DANGER;
}
function getPeersTooltipPart(peers, peersUnit) {
function getPeersTooltipPart(peers: number | undefined, peersUnit: string) {
if (peers == null) {
return `Unknown ${peersUnit}s`;
}
@@ -31,27 +30,27 @@ function getPeersTooltipPart(peers, peersUnit) {
return `${peers} ${peersUnit}s`;
}
function Peers(props) {
const {
seeders,
leechers
} = props;
interface PeersProps {
seeders?: number;
leechers?: number;
}
function Peers(props: PeersProps) {
const { seeders, leechers } = props;
const kind = getKind(seeders);
return (
<Label
kind={kind}
title={`${getPeersTooltipPart(seeders, 'seeder')}, ${getPeersTooltipPart(leechers, 'leecher')}`}
title={`${getPeersTooltipPart(seeders, 'seeder')}, ${getPeersTooltipPart(
leechers,
'leecher'
)}`}
>
{seeders == null ? '-' : seeders} / {leechers == null ? '-' : leechers}
</Label>
);
}
Peers.propTypes = {
seeders: PropTypes.number,
leechers: PropTypes.number
};
export default Peers;

View File

@@ -1,37 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import DeleteMovieModalContentConnector from './DeleteMovieModalContentConnector';
function DeleteMovieModal(props) {
const {
isOpen,
onModalClose,
previousMovie,
...otherProps
} = props;
return (
<Modal
isOpen={isOpen}
size={sizes.MEDIUM}
onModalClose={onModalClose}
>
<DeleteMovieModalContentConnector
{...otherProps}
onModalClose={onModalClose}
previousMovie={previousMovie}
/>
</Modal>
);
}
DeleteMovieModal.propTypes = {
...DeleteMovieModalContentConnector.propTypes,
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired,
previousMovie: PropTypes.string
};
export default DeleteMovieModal;

View File

@@ -0,0 +1,24 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import DeleteMovieModalContent, {
DeleteMovieModalContentProps,
} from './DeleteMovieModalContent';
interface DeleteMovieModalProps extends DeleteMovieModalContentProps {
isOpen: boolean;
}
function DeleteMovieModal({
isOpen,
onModalClose,
...otherProps
}: DeleteMovieModalProps) {
return (
<Modal isOpen={isOpen} size={sizes.MEDIUM} onModalClose={onModalClose}>
<DeleteMovieModalContent {...otherProps} onModalClose={onModalClose} />
</Modal>
);
}
export default DeleteMovieModal;

View File

@@ -1,163 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
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 { icons, inputTypes, kinds } from 'Helpers/Props';
import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
import styles from './DeleteMovieModalContent.css';
class DeleteMovieModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
deleteFiles: false
};
}
//
// Listeners
onDeleteFilesChange = ({ value }) => {
this.setState({ deleteFiles: value });
};
onDeleteMovieConfirmed = () => {
const deleteFiles = this.state.deleteFiles;
const addImportExclusion = this.props.deleteOptions.addImportExclusion;
this.setState({ deleteFiles: false });
this.props.onDeletePress(deleteFiles, addImportExclusion);
};
//
// Render
render() {
const {
title,
path,
statistics = {},
deleteOptions,
onModalClose,
onDeleteOptionChange
} = this.props;
const {
movieFileCount = 0,
sizeOnDisk = 0
} = statistics;
const deleteFiles = this.state.deleteFiles;
const addImportExclusion = deleteOptions.addImportExclusion;
return (
<ModalContent
onModalClose={onModalClose}
>
<ModalHeader>
{translate('DeleteHeader', { title })}
</ModalHeader>
<ModalBody>
<div className={styles.pathContainer}>
<Icon
className={styles.pathIcon}
name={icons.FOLDER}
/>
{path}
</div>
<FormGroup>
<FormLabel>
{translate('AddListExclusion')}
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="addImportExclusion"
value={addImportExclusion}
helpText={translate('AddListExclusionMovieHelpText')}
kind={kinds.DANGER}
onChange={onDeleteOptionChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{movieFileCount === 0 ? translate('DeleteMovieFolder') : translate('DeleteMovieFiles', { movieFileCount })}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="deleteFiles"
value={deleteFiles}
helpText={movieFileCount === 0 ? translate('DeleteMovieFolderHelpText') : translate('DeleteMovieFilesHelpText')}
kind={kinds.DANGER}
onChange={this.onDeleteFilesChange}
/>
</FormGroup>
{
deleteFiles ?
<div className={styles.deleteFilesMessage}>
<div><InlineMarkdown data={translate('DeleteMovieFolderConfirmation', { path })} blockClassName={styles.folderPath} /></div>
{
movieFileCount ?
<div className={styles.deleteCount}>
{translate('DeleteMovieFolderMovieCount', { movieFileCount, size: formatBytes(sizeOnDisk) })}
</div> :
null
}
</div> :
null
}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
{translate('Close')}
</Button>
<Button
kind={kinds.DANGER}
onPress={this.onDeleteMovieConfirmed}
>
{translate('Delete')}
</Button>
</ModalFooter>
</ModalContent>
);
}
}
DeleteMovieModalContent.propTypes = {
title: PropTypes.string.isRequired,
path: PropTypes.string.isRequired,
statistics: PropTypes.object.isRequired,
hasFile: PropTypes.bool.isRequired,
deleteOptions: PropTypes.object.isRequired,
onDeleteOptionChange: PropTypes.func.isRequired,
onDeletePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
DeleteMovieModalContent.defaultProps = {
statistics: {}
};
export default DeleteMovieModalContent;

View File

@@ -0,0 +1,149 @@
import React, { useCallback, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
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 { icons, inputTypes, kinds } from 'Helpers/Props';
import { Statistics } from 'Movie/Movie';
import useMovie from 'Movie/useMovie';
import { deleteMovie, setDeleteOption } from 'Store/Actions/movieActions';
import { CheckInputChanged } from 'typings/inputs';
import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
import styles from './DeleteMovieModalContent.css';
export interface DeleteMovieModalContentProps {
movieId: number;
onModalClose: () => void;
}
function DeleteMovieModalContent({
movieId,
onModalClose,
}: DeleteMovieModalContentProps) {
const dispatch = useDispatch();
const {
title,
path,
collection,
statistics = {} as Statistics,
} = useMovie(movieId)!;
const { addImportExclusion } = useSelector(
(state: AppState) => state.movies.deleteOptions
);
const { movieFileCount = 0, sizeOnDisk = 0 } = statistics;
const [deleteFiles, setDeleteFiles] = useState(false);
const handleDeleteFilesChange = useCallback(
({ value }: CheckInputChanged) => {
setDeleteFiles(value);
},
[]
);
const handleDeleteMovieConfirmed = useCallback(() => {
dispatch(
deleteMovie({
id: movieId,
collectionTmdbId: collection?.tmdbId,
deleteFiles,
addImportExclusion,
})
);
}, [movieId, collection, addImportExclusion, deleteFiles, dispatch]);
const handleDeleteOptionChange = useCallback(
({ name, value }: CheckInputChanged) => {
dispatch(setDeleteOption({ [name]: value }));
},
[dispatch]
);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('DeleteHeader', { title })}</ModalHeader>
<ModalBody>
<div className={styles.pathContainer}>
<Icon className={styles.pathIcon} name={icons.FOLDER} />
{path}
</div>
<FormGroup>
<FormLabel>{translate('AddListExclusion')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="addImportExclusion"
value={addImportExclusion}
helpText={translate('AddListExclusionMovieHelpText')}
kind={kinds.DANGER}
onChange={handleDeleteOptionChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>
{movieFileCount === 0
? translate('DeleteMovieFolder')
: translate('DeleteMovieFiles', { movieFileCount })}
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="deleteFiles"
value={deleteFiles}
helpText={
movieFileCount === 0
? translate('DeleteMovieFolderHelpText')
: translate('DeleteMovieFilesHelpText')
}
kind={kinds.DANGER}
onChange={handleDeleteFilesChange}
/>
</FormGroup>
{deleteFiles ? (
<div className={styles.deleteFilesMessage}>
<div>
<InlineMarkdown
data={translate('DeleteMovieFolderConfirmation', { path })}
blockClassName={styles.folderPath}
/>
</div>
{movieFileCount ? (
<div className={styles.deleteCount}>
{translate('DeleteMovieFolderMovieCount', {
movieFileCount,
size: formatBytes(sizeOnDisk),
})}
</div>
) : null}
</div>
) : null}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>{translate('Close')}</Button>
<Button kind={kinds.DANGER} onPress={handleDeleteMovieConfirmed}>
{translate('Delete')}
</Button>
</ModalFooter>
</ModalContent>
);
}
export default DeleteMovieModalContent;

View File

@@ -1,45 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { deleteMovie, setDeleteOption } from 'Store/Actions/movieActions';
import createMovieSelector from 'Store/Selectors/createMovieSelector';
import DeleteMovieModalContent from './DeleteMovieModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.movies.deleteOptions,
createMovieSelector(),
(deleteOptions, movie) => {
return {
...movie,
deleteOptions
};
}
);
}
function createMapDispatchToProps(dispatch, props) {
return {
onDeleteOptionChange(option) {
dispatch(
setDeleteOption({
[option.name]: option.value
})
);
},
onDeletePress(deleteFiles, addImportExclusion) {
dispatch(
deleteMovie({
id: props.movieId,
collectionTmdbId: this.collection?.tmdbId,
deleteFiles,
addImportExclusion
})
);
props.onModalClose(true);
}
};
}
export default connect(createMapStateToProps, createMapDispatchToProps)(DeleteMovieModalContent);

View File

@@ -21,18 +21,19 @@ import TmdbRating from 'Components/TmdbRating';
import Popover from 'Components/Tooltip/Popover';
import Tooltip from 'Components/Tooltip/Tooltip';
import TraktRating from 'Components/TraktRating';
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
import { icons, kinds, sizes, sortDirections, tooltipPositions } from 'Helpers/Props';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
import EditMovieModal from 'Movie/Edit/EditMovieModal';
import getMovieStatusDetails from 'Movie/getMovieStatusDetails';
import MovieHistoryModal from 'Movie/History/MovieHistoryModal';
import MovieCollectionLabelConnector from 'Movie/MovieCollectionLabelConnector';
import MovieCollectionLabel from 'Movie/MovieCollectionLabel';
import MovieGenres from 'Movie/MovieGenres';
import MoviePoster from 'Movie/MoviePoster';
import MovieInteractiveSearchModal from 'Movie/Search/MovieInteractiveSearchModal';
import MovieFileEditorTable from 'MovieFile/Editor/MovieFileEditorTable';
import ExtraFileTable from 'MovieFile/Extras/ExtraFileTable';
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
import OrganizePreviewModal from 'Organize/OrganizePreviewModal';
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
import fonts from 'Styles/Variables/fonts';
import * as keyCodes from 'Utilities/Constants/keyCodes';
@@ -608,7 +609,7 @@ class MovieDetails extends Component {
size={sizes.LARGE}
>
<div className={styles.collection}>
<MovieCollectionLabelConnector
<MovieCollectionLabel
tmdbId={collection.tmdbId}
/>
</div>
@@ -651,9 +652,7 @@ class MovieDetails extends Component {
name={translate('Genres')}
size={sizes.LARGE}
>
<span className={styles.genres}>
{genres.join(', ')}
</span>
<MovieGenres className={styles.genres} genres={genres} />
</InfoLabel> :
null
}
@@ -725,13 +724,13 @@ class MovieDetails extends Component {
</FieldSet>
</div>
<OrganizePreviewModalConnector
<OrganizePreviewModal
isOpen={isOrganizeModalOpen}
movieId={id}
onModalClose={this.onOrganizeModalClose}
/>
<EditMovieModalConnector
<EditMovieModal
isOpen={isEditMovieModalOpen}
movieId={id}
onModalClose={this.onEditMovieModalClose}
@@ -748,17 +747,20 @@ class MovieDetails extends Component {
isOpen={isDeleteMovieModalOpen}
movieId={id}
onModalClose={this.onDeleteMovieModalClose}
nextMovieRelativePath={`/movie/${nextMovie.titleSlug}`}
/>
<InteractiveImportModal
isOpen={isInteractiveImportModalOpen}
movieId={id}
modalTitle={translate('ManageFiles')}
title={title}
folder={path}
initialSortKey="relativePath"
initialSortDirection={sortDirections.ASCENDING}
showMovie={false}
allowMovieChange={false}
showFilterExistingFiles={true}
showDelete={true}
showImportMode={false}
modalTitle={translate('ManageFiles')}
onModalClose={this.onInteractiveImportModalClose}
/>

View File

@@ -8,11 +8,9 @@ import * as commandNames from 'Commands/commandNames';
import { executeCommand } from 'Store/Actions/commandActions';
import { clearExtraFiles, fetchExtraFiles } from 'Store/Actions/extraFileActions';
import { toggleMovieMonitored } from 'Store/Actions/movieActions';
import { clearMovieBlocklist, fetchMovieBlocklist } from 'Store/Actions/movieBlocklistActions';
import { clearMovieCredits, fetchMovieCredits } from 'Store/Actions/movieCreditsActions';
import { clearMovieFiles, fetchMovieFiles } from 'Store/Actions/movieFileActions';
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions';
import { fetchImportListSchema } from 'Store/Actions/settingsActions';
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
@@ -188,12 +186,6 @@ function createMapDispatchToProps(dispatch, props) {
dispatchClearExtraFiles() {
dispatch(clearExtraFiles());
},
dispatchClearReleases() {
dispatch(clearReleases());
},
dispatchCancelFetchReleases() {
dispatch(cancelFetchReleases());
},
dispatchFetchQueueDetails({ movieId }) {
dispatch(fetchQueueDetails({ movieId }));
},
@@ -211,12 +203,6 @@ function createMapDispatchToProps(dispatch, props) {
},
onGoToMovie(titleSlug) {
dispatch(push(`${window.Radarr.urlBase}/movie/${titleSlug}`));
},
dispatchFetchMovieBlocklist({ movieId }) {
dispatch(fetchMovieBlocklist({ movieId }));
},
dispatchClearMovieBlocklist() {
dispatch(clearMovieBlocklist());
}
};
}
@@ -270,7 +256,6 @@ class MovieDetailsConnector extends Component {
const movieId = this.props.id;
this.props.dispatchFetchMovieFiles({ movieId });
this.props.dispatchFetchMovieBlocklist({ movieId });
this.props.dispatchFetchExtraFiles({ movieId });
this.props.dispatchFetchMovieCredits({ movieId });
this.props.dispatchFetchQueueDetails({ movieId });
@@ -278,13 +263,10 @@ class MovieDetailsConnector extends Component {
};
unpopulate = () => {
this.props.dispatchCancelFetchReleases();
this.props.dispatchClearMovieBlocklist();
this.props.dispatchClearMovieFiles();
this.props.dispatchClearExtraFiles();
this.props.dispatchClearMovieCredits();
this.props.dispatchClearQueueDetails();
this.props.dispatchClearReleases();
};
//
@@ -341,15 +323,11 @@ MovieDetailsConnector.propTypes = {
dispatchClearExtraFiles: PropTypes.func.isRequired,
dispatchFetchMovieCredits: PropTypes.func.isRequired,
dispatchClearMovieCredits: PropTypes.func.isRequired,
dispatchClearReleases: PropTypes.func.isRequired,
dispatchCancelFetchReleases: PropTypes.func.isRequired,
dispatchToggleMovieMonitored: PropTypes.func.isRequired,
dispatchFetchQueueDetails: PropTypes.func.isRequired,
dispatchClearQueueDetails: PropTypes.func.isRequired,
dispatchFetchImportListSchema: PropTypes.func.isRequired,
dispatchExecuteCommand: PropTypes.func.isRequired,
dispatchFetchMovieBlocklist: PropTypes.func.isRequired,
dispatchClearMovieBlocklist: PropTypes.func.isRequired,
onGoToMovie: PropTypes.func.isRequired
};

View File

@@ -92,6 +92,19 @@ function MovieDetailsLinks(props: MovieDetailsLinksProps) {
MDBList
</Label>
</Link>
<Link
className={styles.link}
to={`https://www.blu-ray.com/search/?quicksearch=1&quicksearch_keyword=${imdbId}&section=theatrical`}
>
<Label
className={styles.linkLabel}
kind={kinds.INFO}
size={sizes.LARGE}
>
Blu-ray
</Label>
</Link>
</>
) : null}

View File

@@ -1,26 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import EditMovieModalContentConnector from './EditMovieModalContentConnector';
function EditMovieModal({ isOpen, onModalClose, ...otherProps }) {
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<EditMovieModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
EditMovieModal.propTypes = {
...EditMovieModalContentConnector.propTypes,
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default EditMovieModal;

View File

@@ -0,0 +1,32 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import Modal from 'Components/Modal/Modal';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import EditMovieModalContent, {
EditMovieModalContentProps,
} from './EditMovieModalContent';
interface EditMovieModalProps extends EditMovieModalContentProps {
isOpen: boolean;
}
function EditMovieModal({
isOpen,
onModalClose,
...otherProps
}: EditMovieModalProps) {
const dispatch = useDispatch();
const handleModalClose = useCallback(() => {
dispatch(clearPendingChanges({ section: 'movies' }));
onModalClose();
}, [dispatch, onModalClose]);
return (
<Modal isOpen={isOpen} onModalClose={handleModalClose}>
<EditMovieModalContent {...otherProps} onModalClose={handleModalClose} />
</Modal>
);
}
export default EditMovieModal;

View File

@@ -1,40 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import EditMovieModal from './EditMovieModal';
const mapDispatchToProps = {
clearPendingChanges
};
class EditMovieModalConnector extends Component {
//
// Listeners
onModalClose = () => {
this.props.clearPendingChanges({ section: 'movies' });
this.props.onModalClose();
};
//
// Render
render() {
return (
<EditMovieModal
{...this.props}
onModalClose={this.onModalClose}
/>
);
}
}
EditMovieModalConnector.propTypes = {
...EditMovieModal.propTypes,
onModalClose: PropTypes.func.isRequired,
clearPendingChanges: PropTypes.func.isRequired
};
export default connect(undefined, mapDispatchToProps)(EditMovieModalConnector);

View File

@@ -1,217 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
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 Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import SpinnerButton from 'Components/Link/SpinnerButton';
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 Popover from 'Components/Tooltip/Popover';
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
import MoveMovieModal from 'Movie/MoveMovie/MoveMovieModal';
import translate from 'Utilities/String/translate';
import styles from './EditMovieModalContent.css';
class EditMovieModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isConfirmMoveModalOpen: false
};
}
//
// Listeners
onCancelPress = () => {
this.setState({ isConfirmMoveModalOpen: false });
};
onSavePress = () => {
const {
isPathChanging,
onSavePress
} = this.props;
if (isPathChanging && !this.state.isConfirmMoveModalOpen) {
this.setState({ isConfirmMoveModalOpen: true });
} else {
this.setState({ isConfirmMoveModalOpen: false });
onSavePress(false);
}
};
onMoveMoviePress = () => {
this.setState({ isConfirmMoveModalOpen: false });
this.props.onSavePress(true);
};
//
// Render
render() {
const {
title,
item,
isSaving,
originalPath,
onInputChange,
onModalClose,
onDeleteMoviePress,
...otherProps
} = this.props;
const {
monitored,
qualityProfileId,
minimumAvailability,
// Id,
path,
tags
} = item;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('Edit')} - {title}
</ModalHeader>
<ModalBody>
<Form
{...otherProps}
>
<FormGroup>
<FormLabel>{translate('Monitored')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="monitored"
helpText={translate('MonitoredHelpText')}
{...monitored}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>
{translate('MinimumAvailability')}
<Popover
anchor={
<Icon
className={styles.labelIcon}
name={icons.INFO}
/>
}
title={translate('MinimumAvailability')}
body={<MovieMinimumAvailabilityPopoverContent />}
position={tooltipPositions.RIGHT}
/>
</FormLabel>
<FormInputGroup
type={inputTypes.AVAILABILITY_SELECT}
name="minimumAvailability"
{...minimumAvailability}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('QualityProfile')}</FormLabel>
<FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileId"
{...qualityProfileId}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Path')}</FormLabel>
<FormInputGroup
type={inputTypes.PATH}
name="path"
{...path}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Tags')}</FormLabel>
<FormInputGroup
type={inputTypes.TAG}
name="tags"
{...tags}
onChange={onInputChange}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button
className={styles.deleteButton}
kind={kinds.DANGER}
onPress={onDeleteMoviePress}
>
{translate('Delete')}
</Button>
<Button
onPress={onModalClose}
>
{translate('Cancel')}
</Button>
<SpinnerButton
isSpinning={isSaving}
onPress={this.onSavePress}
>
{translate('Save')}
</SpinnerButton>
</ModalFooter>
<MoveMovieModal
originalPath={originalPath}
destinationPath={path.value}
isOpen={this.state.isConfirmMoveModalOpen}
onModalClose={this.onCancelPress}
onSavePress={this.onSavePress}
onMoveMoviePress={this.onMoveMoviePress}
/>
</ModalContent>
);
}
}
EditMovieModalContent.propTypes = {
movieId: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
item: PropTypes.object.isRequired,
isSaving: PropTypes.bool.isRequired,
isPathChanging: PropTypes.bool.isRequired,
originalPath: PropTypes.string.isRequired,
onInputChange: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired,
onDeleteMoviePress: PropTypes.func.isRequired
};
export default EditMovieModalContent;

View File

@@ -0,0 +1,235 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
import AppState from 'App/State/AppState';
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 Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
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 Popover from 'Components/Tooltip/Popover';
import usePrevious from 'Helpers/Hooks/usePrevious';
import {
icons,
inputTypes,
kinds,
sizes,
tooltipPositions,
} from 'Helpers/Props';
import MoveMovieModal from 'Movie/MoveMovie/MoveMovieModal';
import useMovie from 'Movie/useMovie';
import { saveMovie, setMovieValue } from 'Store/Actions/movieActions';
import selectSettings from 'Store/Selectors/selectSettings';
import { InputChanged } from 'typings/inputs';
import translate from 'Utilities/String/translate';
import styles from './EditMovieModalContent.css';
export interface EditMovieModalContentProps {
movieId: number;
onModalClose: () => void;
onDeleteMoviePress: () => void;
}
function EditMovieModalContent({
movieId,
onModalClose,
onDeleteMoviePress,
}: EditMovieModalContentProps) {
const dispatch = useDispatch();
const {
title,
monitored,
minimumAvailability,
qualityProfileId,
path,
tags,
} = useMovie(movieId)!;
const { isSaving, saveError, pendingChanges } = useSelector(
(state: AppState) => state.movies
);
const wasSaving = usePrevious(isSaving);
const isPathChanging = pendingChanges.path && path !== pendingChanges.path;
const [isConfirmMoveModalOpen, setIsConfirmMoveModalOpen] = useState(false);
const { settings, ...otherSettings } = useMemo(() => {
return selectSettings(
{
monitored,
minimumAvailability,
qualityProfileId,
path,
tags,
},
pendingChanges,
saveError
);
}, [
monitored,
minimumAvailability,
qualityProfileId,
path,
tags,
pendingChanges,
saveError,
]);
const handleInputChange = useCallback(
({ name, value }: InputChanged) => {
// @ts-expect-error actions aren't typed
dispatch(setMovieValue({ name, value }));
},
[dispatch]
);
const handleCancelPress = useCallback(() => {
setIsConfirmMoveModalOpen(false);
}, []);
const handleSavePress = useCallback(() => {
if (isPathChanging && !isConfirmMoveModalOpen) {
setIsConfirmMoveModalOpen(true);
} else {
setIsConfirmMoveModalOpen(false);
dispatch(
saveMovie({
id: movieId,
moveFiles: false,
})
);
}
}, [movieId, isPathChanging, isConfirmMoveModalOpen, dispatch]);
const handleMoveMoviePress = useCallback(() => {
setIsConfirmMoveModalOpen(false);
dispatch(
saveMovie({
id: movieId,
moveFiles: true,
})
);
}, [movieId, dispatch]);
useEffect(() => {
if (!isSaving && wasSaving && !saveError) {
onModalClose();
}
}, [isSaving, wasSaving, saveError, onModalClose]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('EditMovieModalHeader', { title })}</ModalHeader>
<ModalBody>
<Form {...otherSettings}>
<FormGroup size={sizes.MEDIUM}>
<FormLabel>{translate('Monitored')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="monitored"
helpText={translate('MonitoredMovieHelpText')}
{...settings.monitored}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup size={sizes.MEDIUM}>
<FormLabel>
{translate('MinimumAvailability')}
<Popover
anchor={<Icon className={styles.labelIcon} name={icons.INFO} />}
title={translate('MinimumAvailability')}
body={<MovieMinimumAvailabilityPopoverContent />}
position={tooltipPositions.RIGHT}
/>
</FormLabel>
<FormInputGroup
type={inputTypes.AVAILABILITY_SELECT}
name="minimumAvailability"
{...settings.minimumAvailability}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup size={sizes.MEDIUM}>
<FormLabel>{translate('QualityProfile')}</FormLabel>
<FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileId"
{...settings.qualityProfileId}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup size={sizes.MEDIUM}>
<FormLabel>{translate('Path')}</FormLabel>
<FormInputGroup
type={inputTypes.PATH}
name="path"
{...settings.path}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup size={sizes.MEDIUM}>
<FormLabel>{translate('Tags')}</FormLabel>
<FormInputGroup
type={inputTypes.TAG}
name="tags"
{...settings.tags}
onChange={handleInputChange}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button
className={styles.deleteButton}
kind={kinds.DANGER}
onPress={onDeleteMoviePress}
>
{translate('Delete')}
</Button>
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
<SpinnerErrorButton
error={saveError}
isSpinning={isSaving}
onPress={handleSavePress}
>
{translate('Save')}
</SpinnerErrorButton>
</ModalFooter>
<MoveMovieModal
originalPath={path}
destinationPath={pendingChanges.path}
isOpen={isConfirmMoveModalOpen}
onModalClose={handleCancelPress}
onSavePress={handleSavePress}
onMoveMoviePress={handleMoveMoviePress}
/>
</ModalContent>
);
}
export default EditMovieModalContent;

View File

@@ -1,115 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { saveMovie, setMovieValue } from 'Store/Actions/movieActions';
import createMovieSelector from 'Store/Selectors/createMovieSelector';
import selectSettings from 'Store/Selectors/selectSettings';
import EditMovieModalContent from './EditMovieModalContent';
function createIsPathChangingSelector() {
return createSelector(
(state) => state.movies.pendingChanges,
createMovieSelector(),
(pendingChanges, movie) => {
const path = pendingChanges.path;
if (path == null) {
return false;
}
return movie.path !== path;
}
);
}
function createMapStateToProps() {
return createSelector(
(state) => state.movies,
createMovieSelector(),
createIsPathChangingSelector(),
(moviesState, movie, isPathChanging) => {
const {
isSaving,
saveError,
pendingChanges
} = moviesState;
const movieSettings = {
monitored: movie.monitored,
qualityProfileId: movie.qualityProfileId,
minimumAvailability: movie.minimumAvailability,
path: movie.path,
tags: movie.tags
};
const settings = selectSettings(movieSettings, pendingChanges, saveError);
return {
title: movie.title,
isSaving,
saveError,
isPathChanging,
originalPath: movie.path,
item: settings.settings,
...settings
};
}
);
}
const mapDispatchToProps = {
dispatchSetMovieValue: setMovieValue,
dispatchSaveMovie: saveMovie
};
class EditMovieModalContentConnector extends Component {
//
// Lifecycle
componentDidUpdate(prevProps, prevState) {
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
this.props.onModalClose();
}
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.props.dispatchSetMovieValue({ name, value });
};
onSavePress = (moveFiles) => {
this.props.dispatchSaveMovie({
id: this.props.movieId,
moveFiles
});
};
//
// Render
render() {
return (
<EditMovieModalContent
{...this.props}
onInputChange={this.onInputChange}
onSavePress={this.onSavePress}
onMoveMoviePress={this.onMoveMoviePress}
/>
);
}
}
EditMovieModalContentConnector.propTypes = {
movieId: PropTypes.number,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
dispatchSetMovieValue: PropTypes.func.isRequired,
dispatchSaveMovie: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(EditMovieModalContentConnector);

View File

@@ -1,33 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import MovieHistoryModalContentConnector from './MovieHistoryModalContentConnector';
function MovieHistoryModal(props) {
const {
isOpen,
onModalClose,
...otherProps
} = props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
size={sizes.EXTRA_EXTRA_LARGE}
>
<MovieHistoryModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
MovieHistoryModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default MovieHistoryModal;

View File

@@ -0,0 +1,28 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import MovieHistoryModalContent, {
MovieHistoryModalContentProps,
} from 'Movie/History/MovieHistoryModalContent';
interface MovieHistoryModalProps extends MovieHistoryModalContentProps {
isOpen: boolean;
}
function MovieHistoryModal({
isOpen,
onModalClose,
...otherProps
}: MovieHistoryModalProps) {
return (
<Modal
isOpen={isOpen}
size={sizes.EXTRA_EXTRA_LARGE}
onModalClose={onModalClose}
>
<MovieHistoryModalContent {...otherProps} onModalClose={onModalClose} />
</Modal>
);
}
export default MovieHistoryModal;

View File

@@ -1,141 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
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 Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import MovieHistoryRowConnector from './MovieHistoryRowConnector';
const columns = [
{
name: 'eventType',
isVisible: true
},
{
name: 'sourceTitle',
label: () => translate('SourceTitle'),
isVisible: true
},
{
name: 'languages',
label: () => translate('Languages'),
isVisible: true
},
{
name: 'quality',
label: () => translate('Quality'),
isVisible: true
},
{
name: 'customFormats',
label: () => translate('CustomFormats'),
isSortable: false,
isVisible: true
},
{
name: 'customFormatScore',
label: React.createElement(Icon, {
name: icons.SCORE,
title: () => translate('CustomFormatScore')
}),
isSortable: true,
isVisible: true
},
{
name: 'date',
label: () => translate('Date'),
isVisible: true
},
{
name: 'actions',
isVisible: true
}
];
class MovieHistoryModalContent extends Component {
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
items,
onMarkAsFailedPress,
onModalClose
} = this.props;
const hasItems = !!items.length;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('History')}
</ModalHeader>
<ModalBody>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<Alert kind={kinds.DANGER}>{translate('HistoryLoadError')}</Alert>
}
{
isPopulated && !hasItems && !error &&
<div>{translate('NoHistory')}</div>
}
{
isPopulated && hasItems && !error &&
<Table columns={columns}>
<TableBody>
{
items.map((item) => {
return (
<MovieHistoryRowConnector
key={item.id}
{...item}
onMarkAsFailedPress={onMarkAsFailedPress}
/>
);
})
}
</TableBody>
</Table>
}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
{translate('Close')}
</Button>
</ModalFooter>
</ModalContent>
);
}
}
MovieHistoryModalContent.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
onMarkAsFailedPress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default MovieHistoryModalContent;

View File

@@ -0,0 +1,148 @@
import React, { useCallback, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import Alert from 'Components/Alert';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import Column from 'Components/Table/Column';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { icons, kinds } from 'Helpers/Props';
import {
clearMovieHistory,
fetchMovieHistory,
movieHistoryMarkAsFailed,
} from 'Store/Actions/movieHistoryActions';
import translate from 'Utilities/String/translate';
import MovieHistoryRow from './MovieHistoryRow';
const columns: Column[] = [
{
name: 'eventType',
label: '',
isVisible: true,
},
{
name: 'sourceTitle',
label: () => translate('SourceTitle'),
isVisible: true,
},
{
name: 'languages',
label: () => translate('Languages'),
isVisible: true,
},
{
name: 'quality',
label: () => translate('Quality'),
isVisible: true,
},
{
name: 'customFormats',
label: () => translate('CustomFormats'),
isSortable: false,
isVisible: true,
},
{
name: 'customFormatScore',
label: React.createElement(Icon, {
name: icons.SCORE,
title: () => translate('CustomFormatScore'),
}),
isSortable: true,
isVisible: true,
},
{
name: 'date',
label: () => translate('Date'),
isVisible: true,
},
{
name: 'actions',
label: '',
isVisible: true,
},
];
export interface MovieHistoryModalContentProps {
movieId: number;
onModalClose: () => void;
}
function MovieHistoryModalContent({
movieId,
onModalClose,
}: MovieHistoryModalContentProps) {
const dispatch = useDispatch();
const { isFetching, isPopulated, error, items } = useSelector(
(state: AppState) => state.movieHistory
);
const hasItems = !!items.length;
const handleMarkAsFailedPress = useCallback(
(historyId: number) => {
dispatch(
movieHistoryMarkAsFailed({
historyId,
movieId,
})
);
},
[movieId, dispatch]
);
useEffect(() => {
dispatch(fetchMovieHistory({ movieId }));
return () => {
dispatch(clearMovieHistory());
};
}, [movieId, dispatch]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('History')}</ModalHeader>
<ModalBody>
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
{!isFetching && !!error ? (
<Alert kind={kinds.DANGER}>{translate('HistoryLoadError')}</Alert>
) : null}
{isPopulated && !hasItems && !error ? (
<div>{translate('NoHistory')}</div>
) : null}
{isPopulated && hasItems && !error && (
<Table columns={columns}>
<TableBody>
{items.map((item) => {
return (
<MovieHistoryRow
key={item.id}
{...item}
onMarkAsFailedPress={handleMarkAsFailedPress}
/>
);
})}
</TableBody>
</Table>
)}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>{translate('Close')}</Button>
</ModalFooter>
</ModalContent>
);
}
export default MovieHistoryModalContent;

View File

@@ -1,76 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { clearMovieHistory, fetchMovieHistory, movieHistoryMarkAsFailed } from 'Store/Actions/movieHistoryActions';
import MovieHistoryModalContent from './MovieHistoryModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.movieHistory,
(movieHistory) => {
return movieHistory;
}
);
}
const mapDispatchToProps = {
fetchMovieHistory,
clearMovieHistory,
movieHistoryMarkAsFailed
};
class MovieHistoryModalContentConnector extends Component {
//
// Lifecycle
componentDidMount() {
const {
movieId
} = this.props;
this.props.fetchMovieHistory({
movieId
});
}
componentWillUnmount() {
this.props.clearMovieHistory();
}
//
// Listeners
onMarkAsFailedPress = (historyId) => {
const {
movieId
} = this.props;
this.props.movieHistoryMarkAsFailed({
historyId,
movieId
});
};
//
// Render
render() {
return (
<MovieHistoryModalContent
{...this.props}
onMarkAsFailedPress={this.onMarkAsFailedPress}
/>
);
}
}
MovieHistoryModalContentConnector.propTypes = {
movieId: PropTypes.number.isRequired,
fetchMovieHistory: PropTypes.func.isRequired,
clearMovieHistory: PropTypes.func.isRequired,
movieHistoryMarkAsFailed: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(MovieHistoryModalContentConnector);

View File

@@ -1,181 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import HistoryDetailsModal from 'Activity/History/Details/HistoryDetailsModal';
import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import { icons, kinds } from 'Helpers/Props';
import MovieFormats from 'Movie/MovieFormats';
import MovieLanguages from 'Movie/MovieLanguages';
import MovieQuality from 'Movie/MovieQuality';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import translate from 'Utilities/String/translate';
import styles from './MovieHistoryRow.css';
class MovieHistoryRow extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isMarkAsFailedModalOpen: false,
isDetailsModalOpen: false
};
}
//
// Listeners
onMarkAsFailedPress = () => {
this.setState({ isMarkAsFailedModalOpen: true });
};
onConfirmMarkAsFailed = () => {
this.props.onMarkAsFailedPress(this.props.id);
this.setState({ isMarkAsFailedModalOpen: false });
};
onMarkAsFailedModalClose = () => {
this.setState({ isMarkAsFailedModalOpen: false });
};
onDetailsPress = () => {
this.setState({ isDetailsModalOpen: true });
};
onDetailsModalClose = () => {
this.setState({ isDetailsModalOpen: false });
};
//
// Render
render() {
const {
eventType,
sourceTitle,
quality,
customFormats,
customFormatScore,
languages,
qualityCutoffNotMet,
date,
data,
downloadId,
isMarkingAsFailed,
shortDateFormat,
timeFormat
} = this.props;
const {
isMarkAsFailedModalOpen
} = this.state;
return (
<TableRow>
<HistoryEventTypeCell
eventType={eventType}
data={data}
/>
<TableRowCell className={styles.sourceTitle}>
{sourceTitle}
</TableRowCell>
<TableRowCell>
<MovieLanguages
languages={languages}
/>
</TableRowCell>
<TableRowCell>
<MovieQuality
quality={quality}
isCutoffNotMet={qualityCutoffNotMet}
/>
</TableRowCell>
<TableRowCell>
<MovieFormats formats={customFormats} />
</TableRowCell>
<TableRowCell className={styles.customFormatScore}>
{formatCustomFormatScore(customFormatScore, customFormats.length)}
</TableRowCell>
<RelativeDateCell
date={date}
includeSeconds={true}
includeTime={true}
/>
<TableRowCell className={styles.actions}>
<IconButton
name={icons.INFO}
onPress={this.onDetailsPress}
/>
{
eventType === 'grabbed' &&
<IconButton
title={translate('MarkAsFailed')}
name={icons.REMOVE}
size={14}
onPress={this.onMarkAsFailedPress}
/>
}
</TableRowCell>
<ConfirmModal
isOpen={isMarkAsFailedModalOpen}
kind={kinds.DANGER}
title={translate('MarkAsFailed')}
message={translate('MarkAsFailedMessageText', [sourceTitle])}
confirmLabel={translate('MarkAsFailed')}
onConfirm={this.onConfirmMarkAsFailed}
onCancel={this.onMarkAsFailedModalClose}
/>
<HistoryDetailsModal
isOpen={this.state.isDetailsModalOpen}
eventType={eventType}
sourceTitle={sourceTitle}
data={data}
downloadId={downloadId}
isMarkingAsFailed={isMarkingAsFailed}
shortDateFormat={shortDateFormat}
timeFormat={timeFormat}
onMarkAsFailedPress={this.onMarkAsFailedPress}
onModalClose={this.onDetailsModalClose}
/>
</TableRow>
);
}
}
MovieHistoryRow.propTypes = {
id: PropTypes.number.isRequired,
eventType: PropTypes.string.isRequired,
sourceTitle: PropTypes.string.isRequired,
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
quality: PropTypes.object.isRequired,
customFormats: PropTypes.arrayOf(PropTypes.object),
customFormatScore: PropTypes.number.isRequired,
qualityCutoffNotMet: PropTypes.bool.isRequired,
date: PropTypes.string.isRequired,
data: PropTypes.object.isRequired,
downloadId: PropTypes.string,
isMarkingAsFailed: PropTypes.bool,
movie: PropTypes.object.isRequired,
shortDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
onMarkAsFailedPress: PropTypes.func.isRequired
};
export default MovieHistoryRow;

View File

@@ -0,0 +1,134 @@
import React, { useCallback, useState } from 'react';
import HistoryDetailsModal from 'Activity/History/Details/HistoryDetailsModal';
import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import { icons, kinds } from 'Helpers/Props';
import Language from 'Language/Language';
import MovieFormats from 'Movie/MovieFormats';
import MovieLanguages from 'Movie/MovieLanguages';
import MovieQuality from 'Movie/MovieQuality';
import { QualityModel } from 'Quality/Quality';
import CustomFormat from 'typings/CustomFormat';
import { HistoryData, HistoryEventType } from 'typings/History';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import translate from 'Utilities/String/translate';
import styles from './MovieHistoryRow.css';
interface MovieHistoryRowProps {
id: number;
eventType: HistoryEventType;
sourceTitle: string;
languages?: Language[];
quality: QualityModel;
qualityCutoffNotMet: boolean;
customFormats?: CustomFormat[];
customFormatScore: number;
date: string;
data: HistoryData;
downloadId?: string;
onMarkAsFailedPress: (historyId: number) => void;
}
function MovieHistoryRow({
id,
eventType,
sourceTitle,
languages = [],
quality,
qualityCutoffNotMet,
customFormats = [],
customFormatScore,
date,
data,
downloadId,
onMarkAsFailedPress,
}: MovieHistoryRowProps) {
const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false);
const [isMarkAsFailedModalOpen, setIsMarkAsFailedModalOpen] = useState(false);
const handleDetailsPress = useCallback(() => {
setIsDetailsModalOpen(true);
}, [setIsDetailsModalOpen]);
const handleDetailsModalClose = useCallback(() => {
setIsDetailsModalOpen(false);
}, [setIsDetailsModalOpen]);
const handleMarkAsFailedPress = useCallback(() => {
setIsMarkAsFailedModalOpen(true);
}, []);
const handleConfirmMarkAsFailed = useCallback(() => {
onMarkAsFailedPress(id);
setIsMarkAsFailedModalOpen(false);
}, [id, onMarkAsFailedPress]);
const handleMarkAsFailedModalClose = useCallback(() => {
setIsMarkAsFailedModalOpen(false);
}, []);
return (
<TableRow>
<HistoryEventTypeCell eventType={eventType} data={data} />
<TableRowCell className={styles.sourceTitle}>{sourceTitle}</TableRowCell>
<TableRowCell>
<MovieLanguages languages={languages} />
</TableRowCell>
<TableRowCell>
<MovieQuality quality={quality} isCutoffNotMet={qualityCutoffNotMet} />
</TableRowCell>
<TableRowCell>
<MovieFormats formats={customFormats} />
</TableRowCell>
<TableRowCell className={styles.customFormatScore}>
{formatCustomFormatScore(customFormatScore, customFormats.length)}
</TableRowCell>
<RelativeDateCell date={date} includeSeconds={true} includeTime={true} />
<TableRowCell className={styles.actions}>
<IconButton name={icons.INFO} onPress={handleDetailsPress} />
{eventType === 'grabbed' ? (
<IconButton
title={translate('MarkAsFailed')}
name={icons.REMOVE}
size={14}
onPress={handleMarkAsFailedPress}
/>
) : null}
</TableRowCell>
<ConfirmModal
isOpen={isMarkAsFailedModalOpen}
kind={kinds.DANGER}
title={translate('MarkAsFailed')}
message={translate('MarkAsFailedConfirmation', { sourceTitle })}
confirmLabel={translate('MarkAsFailed')}
onConfirm={handleConfirmMarkAsFailed}
onCancel={handleMarkAsFailedModalClose}
/>
<HistoryDetailsModal
isOpen={isDetailsModalOpen}
eventType={eventType}
sourceTitle={sourceTitle}
data={data}
downloadId={downloadId}
onMarkAsFailedPress={handleMarkAsFailedPress}
onModalClose={handleDetailsModalClose}
/>
</TableRow>
);
}
export default MovieHistoryRow;

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