1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-04-18 21:35:51 -04:00

Compare commits

..

92 Commits

Author SHA1 Message Date
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
234 changed files with 6181 additions and 2912 deletions
+1 -1
View File
@@ -87,4 +87,4 @@ This project is also supported by DigitalOcean
### License ### License
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html) * [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
* Copyright 2010-2024 * Copyright 2010-2025
+8 -8
View File
@@ -9,7 +9,7 @@ variables:
testsFolder: './_tests' testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '5.16.2' majorVersion: '5.19.2'
minorVersion: $[counter('minorVersion', 2000)] minorVersion: $[counter('minorVersion', 2000)]
radarrVersion: '$(majorVersion).$(minorVersion)' radarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(radarrVersion)' buildName: '$(Build.SourceBranchName).$(radarrVersion)'
@@ -1116,20 +1116,20 @@ stages:
vmImage: ${{ variables.windowsImage }} vmImage: ${{ variables.windowsImage }}
steps: steps:
- checkout: self # Need history for Sonar analysis - checkout: self # Need history for Sonar analysis
- task: SonarCloudPrepare@2 - task: SonarCloudPrepare@3
env: env:
SONAR_SCANNER_OPTS: '' SONAR_SCANNER_OPTS: ''
inputs: inputs:
SonarCloud: 'SonarCloud' SonarCloud: 'SonarCloud'
organization: 'radarr' organization: 'radarr'
scannerMode: 'CLI' scannerMode: 'cli'
configMode: 'manual' configMode: 'manual'
cliProjectKey: 'Radarr_Radarr.UI' cliProjectKey: 'Radarr_Radarr.UI'
cliProjectName: 'RadarrUI' cliProjectName: 'RadarrUI'
cliProjectVersion: '$(radarrVersion)' cliProjectVersion: '$(radarrVersion)'
cliSources: './frontend' cliSources: './frontend'
- task: SonarCloudAnalyze@2 - task: SonarCloudAnalyze@3
- job: Api_Docs - job: Api_Docs
displayName: API Docs displayName: API Docs
dependsOn: Prepare dependsOn: Prepare
@@ -1205,12 +1205,12 @@ stages:
submodules: true submodules: true
- powershell: Set-Service SCardSvr -StartupType Manual - powershell: Set-Service SCardSvr -StartupType Manual
displayName: Enable Windows Test Service displayName: Enable Windows Test Service
- task: SonarCloudPrepare@2 - task: SonarCloudPrepare@3
condition: eq(variables['System.PullRequest.IsFork'], 'False') condition: eq(variables['System.PullRequest.IsFork'], 'False')
inputs: inputs:
SonarCloud: 'SonarCloud' SonarCloud: 'SonarCloud'
organization: 'radarr' organization: 'radarr'
scannerMode: 'MSBuild' scannerMode: 'dotnet'
projectKey: 'Radarr_Radarr' projectKey: 'Radarr_Radarr'
projectName: 'Radarr' projectName: 'Radarr'
projectVersion: '$(radarrVersion)' projectVersion: '$(radarrVersion)'
@@ -1223,7 +1223,7 @@ stages:
./build.sh --backend -f net6.0 -r win-x64 ./build.sh --backend -f net6.0 -r win-x64
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
displayName: Coverage Unit Tests displayName: Coverage Unit Tests
- task: SonarCloudAnalyze@2 - task: SonarCloudAnalyze@3
condition: eq(variables['System.PullRequest.IsFork'], 'False') condition: eq(variables['System.PullRequest.IsFork'], 'False')
displayName: Publish SonarCloud Results displayName: Publish SonarCloud Results
- task: reportgenerator@5.3.11 - task: reportgenerator@5.3.11
+10 -5
View File
@@ -1,13 +1,18 @@
#!/bin/bash
set -e
FRAMEWORK="net6.0"
PLATFORM=$1 PLATFORM=$1
ARCHITECTURE="${2:-x64}"
if [ "$PLATFORM" = "Windows" ]; then if [ "$PLATFORM" = "Windows" ]; then
RUNTIME="win-x64" RUNTIME="win-$ARCHITECTURE"
elif [ "$PLATFORM" = "Linux" ]; then elif [ "$PLATFORM" = "Linux" ]; then
RUNTIME="linux-x64" RUNTIME="linux-$ARCHITECTURE"
elif [ "$PLATFORM" = "Mac" ]; then elif [ "$PLATFORM" = "Mac" ]; then
RUNTIME="osx-x64" RUNTIME="osx-$ARCHITECTURE"
else 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 exit 1
fi fi
@@ -35,7 +40,7 @@ dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p
dotnet new tool-manifest dotnet new tool-manifest
dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli 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 sleep 45
+1 -1
View File
@@ -187,7 +187,7 @@ module.exports = (env) => {
loose: true, loose: true,
debug: false, debug: false,
useBuiltIns: 'entry', useBuiltIns: 'entry',
corejs: 3 corejs: '3.39'
} }
] ]
] ]
@@ -41,6 +41,7 @@ function HistoryDetails(props: HistoryDetailsProps) {
indexer, indexer,
releaseGroup, releaseGroup,
movieMatchType, movieMatchType,
releaseSource,
customFormatScore, customFormatScore,
nzbInfoUrl, nzbInfoUrl,
downloadClient, downloadClient,
@@ -53,6 +54,31 @@ function HistoryDetails(props: HistoryDetailsProps) {
const downloadClientNameInfo = downloadClientName ?? downloadClient; 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 ( return (
<DescriptionList> <DescriptionList>
<DescriptionListItem <DescriptionListItem
@@ -88,6 +114,14 @@ function HistoryDetails(props: HistoryDetailsProps) {
/> />
) : null} ) : null}
{releaseSource ? (
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('ReleaseSource')}
data={releaseSourceMessage}
/>
) : null}
{nzbInfoUrl ? ( {nzbInfoUrl ? (
<span> <span>
<DescriptionListItemTitle> <DescriptionListItemTitle>
@@ -81,7 +81,6 @@ ImportMovieRow.propTypes = {
selectedMovie: PropTypes.object, selectedMovie: PropTypes.object,
isExistingMovie: PropTypes.bool.isRequired, isExistingMovie: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
queued: PropTypes.bool.isRequired,
isSelected: PropTypes.bool, isSelected: PropTypes.bool,
onSelectedChange: PropTypes.func.isRequired, onSelectedChange: PropTypes.func.isRequired,
onInputChange: PropTypes.func.isRequired onInputChange: PropTypes.func.isRequired
@@ -131,7 +131,7 @@ class ImportMovieSelectMovie extends Component {
id={this._buttonId} id={this._buttonId}
> >
<Link <Link
ref={ref} // ref={ref}
className={styles.button} className={styles.button}
component="div" component="div"
onPress={this.onPress} onPress={this.onPress}
@@ -255,7 +255,7 @@ class ImportMovieSelectMovie extends Component {
items.map((item) => { items.map((item) => {
return ( return (
<ImportMovieSearchResultConnector <ImportMovieSearchResultConnector
key={item.tvdbId} key={item.tmdbId}
tmdbId={item.tmdbId} tmdbId={item.tmdbId}
title={item.title} title={item.title}
year={item.year} year={item.year}
+1 -1
View File
@@ -1,6 +1,6 @@
import { AppSectionProviderState } from 'App/State/AppSectionState'; import { AppSectionProviderState } from 'App/State/AppSectionState';
import Metadata from 'typings/Metadata'; import Metadata from 'typings/Metadata';
interface MetadataAppState extends AppSectionProviderState<Metadata> {} type MetadataAppState = AppSectionProviderState<Metadata>;
export default MetadataAppState; export default MetadataAppState;
@@ -1,6 +1,6 @@
import AppSectionState from 'App/State/AppSectionState'; import AppSectionState from 'App/State/AppSectionState';
import MovieCredit from 'typings/MovieCredit'; import MovieCredit from 'typings/MovieCredit';
interface MovieCreditAppState extends AppSectionState<MovieCredit> {} type MovieCreditAppState = AppSectionState<MovieCredit>;
export default MovieCreditAppState; export default MovieCreditAppState;
+1 -2
View File
@@ -37,8 +37,7 @@ export interface NamingAppState
extends AppSectionItemState<NamingConfig>, extends AppSectionItemState<NamingConfig>,
AppSectionSaveState {} AppSectionSaveState {}
export interface NamingExamplesAppState export type NamingExamplesAppState = AppSectionItemState<NamingExample>;
extends AppSectionItemState<NamingExample> {}
export interface ImportListAppState export interface ImportListAppState
extends AppSectionState<ImportList>, extends AppSectionState<ImportList>,
@@ -1,7 +1,7 @@
import React, { ComponentPropsWithoutRef } from 'react'; import React, { ComponentPropsWithoutRef } from 'react';
import styles from './TableRowCell.css'; import styles from './TableRowCell.css';
export interface TableRowCellProps extends ComponentPropsWithoutRef<'td'> {} export type TableRowCellProps = ComponentPropsWithoutRef<'td'>;
export default function TableRowCell({ export default function TableRowCell({
className = styles.cell, className = styles.cell,
+1 -1
View File
@@ -15,7 +15,7 @@ function TraktRating(props: TraktRatingProps) {
const { ratings, iconSize = 14, hideIcon = false } = props; const { ratings, iconSize = 14, hideIcon = false } = props;
const traktImage = 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; const { value = 0, votes = 0 } = ratings.trakt;
@@ -85,10 +85,16 @@ $hoverScale: 1.05;
flex: 1 0 auto; flex: 1 0 auto;
} }
.overviewContainer {
display: flex;
justify-content: space-between;
flex: 0 1 1000px;
flex-direction: column;
}
.overview { .overview {
composes: link; composes: link;
flex: 0 1 1000px;
overflow: hidden; overflow: hidden;
min-height: 0; min-height: 0;
} }
@@ -11,6 +11,7 @@ interface CssExports {
'link': string; 'link': string;
'lists': string; 'lists': string;
'overview': string; 'overview': string;
'overviewContainer': string;
'poster': string; 'poster': string;
'posterContainer': string; 'posterContainer': string;
'title': string; 'title': string;
@@ -133,14 +133,20 @@ class DiscoverMovieOverview extends Component {
/> />
</div> </div>
<MoviePoster <Link
className={styles.poster} className={styles.link}
style={elementStyle} style={elementStyle}
images={images} {...linkProps}
size={250} >
lazy={false} <MoviePoster
overflow={true} className={styles.poster}
/> style={elementStyle}
images={images}
size={250}
lazy={false}
overflow={true}
/>
</Link>
</div> </div>
</div> </div>
@@ -242,11 +248,13 @@ class DiscoverMovieOverview extends Component {
</div> </div>
<div className={styles.details}> <div className={styles.details}>
<div className={styles.overview}> <div className={styles.overviewContainer}>
<TextTruncate <Link className={styles.overview} {...linkProps}>
line={Math.floor(overviewHeight / (defaultFontSize * lineHeight))} <TextTruncate
text={overview} line={Math.floor(overviewHeight / (defaultFontSize * lineHeight))}
/> text={overview}
/>
</Link>
</div> </div>
<DiscoverMovieOverviewInfo <DiscoverMovieOverviewInfo
@@ -255,7 +263,6 @@ class DiscoverMovieOverview extends Component {
{...overviewOptions} {...overviewOptions}
{...otherProps} {...otherProps}
/> />
</div> </div>
</div> </div>
</div> </div>
+7 -3
View File
@@ -21,7 +21,7 @@ import TmdbRating from 'Components/TmdbRating';
import Popover from 'Components/Tooltip/Popover'; import Popover from 'Components/Tooltip/Popover';
import Tooltip from 'Components/Tooltip/Tooltip'; import Tooltip from 'Components/Tooltip/Tooltip';
import TraktRating from 'Components/TraktRating'; 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 InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal'; import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector'; import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
@@ -753,11 +753,15 @@ class MovieDetails extends Component {
<InteractiveImportModal <InteractiveImportModal
isOpen={isInteractiveImportModalOpen} isOpen={isInteractiveImportModalOpen}
movieId={id} movieId={id}
modalTitle={translate('ManageFiles')} title={title}
folder={path} folder={path}
initialSortKey="relativePath"
initialSortDirection={sortDirections.ASCENDING}
showMovie={false}
allowMovieChange={false} allowMovieChange={false}
showFilterExistingFiles={true} showDelete={true}
showImportMode={false} showImportMode={false}
modalTitle={translate('ManageFiles')}
onModalClose={this.onInteractiveImportModalClose} onModalClose={this.onInteractiveImportModalClose}
/> />
@@ -92,6 +92,19 @@ function MovieDetailsLinks(props: MovieDetailsLinksProps) {
MDBList MDBList
</Label> </Label>
</Link> </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} ) : null}
@@ -0,0 +1,38 @@
import React from 'react';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import MediaInfoProps from 'typings/MediaInfo';
import formatBitrate from 'Utilities/Number/formatBitrate';
import getEntries from 'Utilities/Object/getEntries';
function MediaInfo(props: MediaInfoProps) {
return (
<DescriptionList>
{getEntries(props).map(([key, value]) => {
const title = key
.replace(/([A-Z])/g, ' $1')
.replace(/^./, (str) => str.toUpperCase());
if (!value) {
return null;
}
if (key === 'audioBitrate' || key === 'videoBitrate') {
return (
<DescriptionListItem
key={key}
title={title}
data={
<span title={value.toString()}>{formatBitrate(value)}</span>
}
/>
);
}
return <DescriptionListItem key={key} title={title} data={value} />;
})}
</DescriptionList>
);
}
export default MediaInfo;
@@ -1,33 +0,0 @@
import React from 'react';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
function MediaInfoPopover(props) {
return (
<DescriptionList>
{
Object.keys(props).map((key) => {
const title = key
.replace(/([A-Z])/g, ' $1')
.replace(/^./, (str) => str.toUpperCase());
const value = props[key];
if (!value) {
return null;
}
return (
<DescriptionListItem
key={key}
title={title}
data={props[key]}
/>
);
})
}
</DescriptionList>
);
}
export default MediaInfoPopover;
@@ -14,7 +14,7 @@ import MovieFormats from 'Movie/MovieFormats';
import MovieLanguages from 'Movie/MovieLanguages'; import MovieLanguages from 'Movie/MovieLanguages';
import MovieQuality from 'Movie/MovieQuality'; import MovieQuality from 'Movie/MovieQuality';
import FileEditModal from 'MovieFile/Edit/FileEditModal'; import FileEditModal from 'MovieFile/Edit/FileEditModal';
import MediaInfoConnector from 'MovieFile/MediaInfoConnector'; import MediaInfo from 'MovieFile/MediaInfo';
import * as mediaInfoTypes from 'MovieFile/mediaInfoTypes'; import * as mediaInfoTypes from 'MovieFile/mediaInfoTypes';
import formatBytes from 'Utilities/Number/formatBytes'; import formatBytes from 'Utilities/Number/formatBytes';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore'; import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
@@ -224,7 +224,7 @@ class MovieFileEditorRow extends Component {
key={name} key={name}
className={styles.audio} className={styles.audio}
> >
<MediaInfoConnector <MediaInfo
type={mediaInfoTypes.AUDIO} type={mediaInfoTypes.AUDIO}
movieFileId={id} movieFileId={id}
/> />
@@ -238,7 +238,7 @@ class MovieFileEditorRow extends Component {
key={name} key={name}
className={styles.audioLanguages} className={styles.audioLanguages}
> >
<MediaInfoConnector <MediaInfo
type={mediaInfoTypes.AUDIO_LANGUAGES} type={mediaInfoTypes.AUDIO_LANGUAGES}
movieFileId={id} movieFileId={id}
/> />
@@ -252,7 +252,7 @@ class MovieFileEditorRow extends Component {
key={name} key={name}
className={styles.subtitles} className={styles.subtitles}
> >
<MediaInfoConnector <MediaInfo
type={mediaInfoTypes.SUBTITLES} type={mediaInfoTypes.SUBTITLES}
movieFileId={id} movieFileId={id}
/> />
@@ -266,7 +266,7 @@ class MovieFileEditorRow extends Component {
key={name} key={name}
className={styles.video} className={styles.video}
> >
<MediaInfoConnector <MediaInfo
type={mediaInfoTypes.VIDEO} type={mediaInfoTypes.VIDEO}
movieFileId={id} movieFileId={id}
/> />
@@ -280,7 +280,7 @@ class MovieFileEditorRow extends Component {
key={name} key={name}
className={styles.videoDynamicRangeType} className={styles.videoDynamicRangeType}
> >
<MediaInfoConnector <MediaInfo
type={mediaInfoTypes.VIDEO_DYNAMIC_RANGE_TYPE} type={mediaInfoTypes.VIDEO_DYNAMIC_RANGE_TYPE}
movieFileId={id} movieFileId={id}
/> />
+2 -2
View File
@@ -8,7 +8,7 @@ import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import { sizes } from 'Helpers/Props'; import { sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import MediaInfoPopover from './Editor/MediaInfoPopover'; import MediaInfo from './Editor/MediaInfo';
function FileDetailsModal(props) { function FileDetailsModal(props) {
const { const {
@@ -31,7 +31,7 @@ function FileDetailsModal(props) {
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
<MediaInfoPopover {...mediaInfo} /> <MediaInfo {...mediaInfo} />
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
-104
View File
@@ -1,104 +0,0 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React from 'react';
import getLanguageName from 'Utilities/String/getLanguageName';
import translate from 'Utilities/String/translate';
import * as mediaInfoTypes from './mediaInfoTypes';
function formatLanguages(languages) {
if (!languages) {
return null;
}
const splitLanguages = _.uniq(languages.split('/')).map((l) => {
const simpleLanguage = l.split('_')[0];
if (simpleLanguage === 'und') {
return translate('Unknown');
}
return getLanguageName(simpleLanguage);
});
if (splitLanguages.length > 3) {
return (
<span title={splitLanguages.join(', ')}>
{splitLanguages.slice(0, 2).join(', ')}, {splitLanguages.length - 2} more
</span>
);
}
return (
<span>
{splitLanguages.join(', ')}
</span>
);
}
function MediaInfo(props) {
const {
type,
audioChannels,
audioCodec,
audioLanguages,
subtitles,
videoCodec,
videoDynamicRangeType
} = props;
if (type === mediaInfoTypes.AUDIO) {
return (
<span>
{
audioCodec ? audioCodec : ''
}
{
audioCodec && audioChannels ? ' - ' : ''
}
{
audioChannels ? audioChannels.toFixed(1) : ''
}
</span>
);
}
if (type === mediaInfoTypes.AUDIO_LANGUAGES) {
return formatLanguages(audioLanguages);
}
if (type === mediaInfoTypes.SUBTITLES) {
return formatLanguages(subtitles);
}
if (type === mediaInfoTypes.VIDEO) {
return (
<span>
{videoCodec}
</span>
);
}
if (type === mediaInfoTypes.VIDEO_DYNAMIC_RANGE_TYPE) {
return (
<span>
{videoDynamicRangeType}
</span>
);
}
return null;
}
MediaInfo.propTypes = {
type: PropTypes.string.isRequired,
audioChannels: PropTypes.number,
audioCodec: PropTypes.string,
audioLanguages: PropTypes.string,
subtitles: PropTypes.string,
videoCodec: PropTypes.string,
videoDynamicRangeType: PropTypes.string
};
export default MediaInfo;
+92
View File
@@ -0,0 +1,92 @@
import React from 'react';
import getLanguageName from 'Utilities/String/getLanguageName';
import translate from 'Utilities/String/translate';
import useMovieFile from './useMovieFile';
function formatLanguages(languages: string | undefined) {
if (!languages) {
return null;
}
const splitLanguages = [...new Set(languages.split('/'))].map((l) => {
const simpleLanguage = l.split('_')[0];
if (simpleLanguage === 'und') {
return translate('Unknown');
}
return getLanguageName(simpleLanguage);
});
if (splitLanguages.length > 3) {
return (
<span title={splitLanguages.join(', ')}>
{splitLanguages.slice(0, 2).join(', ')}, {splitLanguages.length - 2}{' '}
more
</span>
);
}
return <span>{splitLanguages.join(', ')}</span>;
}
export type MediaInfoType =
| 'audio'
| 'audioLanguages'
| 'subtitles'
| 'video'
| 'videoDynamicRangeType';
interface MediaInfoProps {
movieFileId?: number;
type: MediaInfoType;
}
function MediaInfo({ movieFileId, type }: MediaInfoProps) {
const movieFile = useMovieFile(movieFileId);
if (!movieFile?.mediaInfo) {
return null;
}
const {
audioChannels,
audioCodec,
audioLanguages,
subtitles,
videoCodec,
videoDynamicRangeType,
} = movieFile.mediaInfo;
if (type === 'audio') {
return (
<span>
{audioCodec ? audioCodec : ''}
{audioCodec && audioChannels ? ' - ' : ''}
{audioChannels ? audioChannels.toFixed(1) : ''}
</span>
);
}
if (type === 'audioLanguages') {
return formatLanguages(audioLanguages);
}
if (type === 'subtitles') {
return formatLanguages(subtitles);
}
if (type === 'video') {
return <span>{videoCodec}</span>;
}
if (type === 'videoDynamicRangeType') {
return <span>{videoDynamicRangeType}</span>;
}
return null;
}
export default MediaInfo;
@@ -1,21 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createMovieFileSelector from 'Store/Selectors/createMovieFileSelector';
import MediaInfo from './MediaInfo';
function createMapStateToProps() {
return createSelector(
createMovieFileSelector(),
(movieFile) => {
if (movieFile) {
return {
...movieFile.mediaInfo
};
}
return {};
}
);
}
export default connect(createMapStateToProps)(MediaInfo);
@@ -1,17 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import MovieLanguages from 'Movie/MovieLanguages';
import createMovieFileSelector from 'Store/Selectors/createMovieFileSelector';
function createMapStateToProps() {
return createSelector(
createMovieFileSelector(),
(movieFile) => {
return {
languages: movieFile ? movieFile.languages : undefined
};
}
);
}
export default connect(createMapStateToProps)(MovieLanguages);
@@ -0,0 +1,15 @@
import React from 'react';
import MovieLanguages from 'Movie/MovieLanguages';
import useMovieFile from './useMovieFile';
interface MovieFileLanguagesProps {
movieFileId: number;
}
function MovieFileLanguages({ movieFileId }: MovieFileLanguagesProps) {
const movieFile = useMovieFile(movieFileId);
return <MovieLanguages languages={movieFile?.languages ?? []} />;
}
export default MovieFileLanguages;
+18
View File
@@ -0,0 +1,18 @@
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
function createMovieFileSelector(movieFileId?: number) {
return createSelector(
(state: AppState) => state.movieFiles.items,
(movieFiles) => {
return movieFiles.find(({ id }) => id === movieFileId);
}
);
}
function useMovieFile(movieFileId: number | undefined) {
return useSelector(createMovieFileSelector(movieFileId));
}
export default useMovieFile;
@@ -55,10 +55,10 @@ function EditSpecificationModalContent(props) {
<InlineMarkdown data={translate('ConditionUsingRegularExpressions')} /> <InlineMarkdown data={translate('ConditionUsingRegularExpressions')} />
</div> </div>
<div> <div>
<InlineMarkdown data={translate('RegularExpressionsTutorialLink')} /> <InlineMarkdown data={translate('RegularExpressionsTutorialLink', { url: 'https://www.regular-expressions.info/tutorial.html' })} />
</div> </div>
<div> <div>
<InlineMarkdown data={translate('RegularExpressionsCanBeTested')} /> <InlineMarkdown data={translate('RegularExpressionsCanBeTested', { url: 'http://regexstorm.net/tester' })} />
</div> </div>
</Alert> </Alert>
} }
@@ -21,8 +21,8 @@
display: flex; display: flex;
color: var(--helpTextColor); color: var(--helpTextColor);
.icon { .identifier {
margin-top: 3px; margin-top: 8px;
margin-right: 5px; margin-right: 5px;
padding: 2px; padding: 2px;
} }
@@ -3,7 +3,7 @@
interface CssExports { interface CssExports {
'footNote': string; 'footNote': string;
'groups': string; 'groups': string;
'icon': string; 'identifier': string;
'namingSelect': string; 'namingSelect': string;
'namingSelectContainer': string; 'namingSelectContainer': string;
} }
@@ -2,7 +2,6 @@ import React, { useCallback, useState } from 'react';
import FieldSet from 'Components/FieldSet'; import FieldSet from 'Components/FieldSet';
import SelectInput from 'Components/Form/SelectInput'; import SelectInput from 'Components/Form/SelectInput';
import TextInput from 'Components/Form/TextInput'; import TextInput from 'Components/Form/TextInput';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button'; import Button from 'Components/Link/Button';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
import Modal from 'Components/Modal/Modal'; import Modal from 'Components/Modal/Modal';
@@ -10,7 +9,7 @@ import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent'; import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import { icons, sizes } from 'Helpers/Props'; import { sizes } from 'Helpers/Props';
import NamingConfig from 'typings/Settings/NamingConfig'; import NamingConfig from 'typings/Settings/NamingConfig';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import NamingOption from './NamingOption'; import NamingOption from './NamingOption';
@@ -88,32 +87,32 @@ const fileNameTokens = [
]; ];
const movieTokens = [ const movieTokens = [
{ token: '{Movie Title}', example: "Movie's Title", footNote: true }, { token: '{Movie Title}', example: "Movie's Title", footNotes: '1' },
{ token: '{Movie Title:DE}', example: 'Titel des Films', footNote: true }, { token: '{Movie Title:DE}', example: 'Titel des Films', footNotes: '1' },
{ token: '{Movie CleanTitle}', example: 'Movies Title', footNote: true }, { token: '{Movie CleanTitle}', example: 'Movies Title', footNotes: '1' },
{ {
token: '{Movie CleanTitle:DE}', token: '{Movie CleanTitle:DE}',
example: 'Titel des Films', example: 'Titel des Films',
footNote: true, footNotes: '1',
}, },
{ token: '{Movie TitleThe}', example: "Movie's Title, The", footNote: true }, { token: '{Movie TitleThe}', example: "Movie's Title, The", footNotes: '1' },
{ {
token: '{Movie CleanTitleThe}', token: '{Movie CleanTitleThe}',
example: 'Movies Title, The', example: 'Movies Title, The',
footNote: true, footNotes: '1',
}, },
{ token: '{Movie OriginalTitle}', example: 'Τίτλος ταινίας', footNote: true }, { token: '{Movie OriginalTitle}', example: 'Τίτλος ταινίας', footNotes: '1' },
{ {
token: '{Movie CleanOriginalTitle}', token: '{Movie CleanOriginalTitle}',
example: 'Τίτλος ταινίας', example: 'Τίτλος ταινίας',
footNote: true, footNotes: '1',
}, },
{ token: '{Movie TitleFirstCharacter}', example: 'M' }, { token: '{Movie TitleFirstCharacter}', example: 'M' },
{ token: '{Movie TitleFirstCharacter:DE}', example: 'T' }, { token: '{Movie TitleFirstCharacter:DE}', example: 'T' },
{ {
token: '{Movie Collection}', token: '{Movie Collection}',
example: 'The Movie Collection', example: 'The Movie Collection',
footNote: true, footNotes: '1',
}, },
{ token: '{Movie Certification}', example: 'R' }, { token: '{Movie Certification}', example: 'R' },
{ token: '{Release Year}', example: '2009' }, { token: '{Release Year}', example: '2009' },
@@ -131,12 +130,21 @@ const qualityTokens = [
const mediaInfoTokens = [ const mediaInfoTokens = [
{ token: '{MediaInfo Simple}', example: 'x264 DTS' }, { token: '{MediaInfo Simple}', example: 'x264 DTS' },
{ token: '{MediaInfo Full}', example: 'x264 DTS [EN+DE]', footNote: true }, { token: '{MediaInfo Full}', example: 'x264 DTS [EN+DE]', footNotes: '1' },
{ token: '{MediaInfo AudioCodec}', example: 'DTS' }, { token: '{MediaInfo AudioCodec}', example: 'DTS' },
{ token: '{MediaInfo AudioChannels}', example: '5.1' }, { token: '{MediaInfo AudioChannels}', example: '5.1' },
{ token: '{MediaInfo AudioLanguages}', example: '[EN+DE]', footNote: true }, {
{ token: '{MediaInfo SubtitleLanguages}', example: '[DE]', footNote: true }, token: '{MediaInfo AudioLanguages}',
example: '[EN+DE]',
footNotes: '1,2',
},
{
token: '{MediaInfo AudioLanguagesAll}',
example: '[EN]',
footNotes: '1',
},
{ token: '{MediaInfo SubtitleLanguages}', example: '[DE]', footNotes: '1' },
{ token: '{MediaInfo VideoCodec}', example: 'x264' }, { token: '{MediaInfo VideoCodec}', example: 'x264' },
{ token: '{MediaInfo VideoBitDepth}', example: '10' }, { token: '{MediaInfo VideoBitDepth}', example: '10' },
@@ -146,11 +154,11 @@ const mediaInfoTokens = [
]; ];
const releaseGroupTokens = [ const releaseGroupTokens = [
{ token: '{Release Group}', example: 'Rls Grp', footNote: true }, { token: '{Release Group}', example: 'Rls Grp', footNotes: '1' },
]; ];
const editionTokens = [ const editionTokens = [
{ token: '{Edition Tags}', example: 'IMAX', footNote: true }, { token: '{Edition Tags}', example: 'IMAX', footNotes: '1' },
]; ];
const customFormatTokens = [ const customFormatTokens = [
@@ -287,13 +295,13 @@ function NamingModal(props: NamingModalProps) {
<FieldSet legend={translate('Movie')}> <FieldSet legend={translate('Movie')}>
<div className={styles.groups}> <div className={styles.groups}>
{movieTokens.map(({ token, example, footNote }) => { {movieTokens.map(({ token, example, footNotes }) => {
return ( return (
<NamingOption <NamingOption
key={token} key={token}
token={token} token={token}
example={example} example={example}
footNote={footNote} footNotes={footNotes}
tokenSeparator={tokenSeparator} tokenSeparator={tokenSeparator}
tokenCase={tokenCase} tokenCase={tokenCase}
onPress={handleOptionPress} onPress={handleOptionPress}
@@ -303,7 +311,7 @@ function NamingModal(props: NamingModalProps) {
</div> </div>
<div className={styles.footNote}> <div className={styles.footNote}>
<Icon className={styles.icon} name={icons.FOOTNOTE} /> <sup className={styles.identifier}>1</sup>
<InlineMarkdown data={translate('MovieFootNote')} /> <InlineMarkdown data={translate('MovieFootNote')} />
</div> </div>
</FieldSet> </FieldSet>
@@ -346,13 +354,13 @@ function NamingModal(props: NamingModalProps) {
<FieldSet legend={translate('MediaInfo')}> <FieldSet legend={translate('MediaInfo')}>
<div className={styles.groups}> <div className={styles.groups}>
{mediaInfoTokens.map(({ token, example, footNote }) => { {mediaInfoTokens.map(({ token, example, footNotes }) => {
return ( return (
<NamingOption <NamingOption
key={token} key={token}
token={token} token={token}
example={example} example={example}
footNote={footNote} footNotes={footNotes}
tokenSeparator={tokenSeparator} tokenSeparator={tokenSeparator}
tokenCase={tokenCase} tokenCase={tokenCase}
onPress={handleOptionPress} onPress={handleOptionPress}
@@ -362,20 +370,25 @@ function NamingModal(props: NamingModalProps) {
</div> </div>
<div className={styles.footNote}> <div className={styles.footNote}>
<Icon className={styles.icon} name={icons.FOOTNOTE} /> <sup className={styles.identifier}>1</sup>
<InlineMarkdown data={translate('MediaInfoFootNote')} /> <InlineMarkdown data={translate('MediaInfoFootNote')} />
</div> </div>
<div className={styles.footNote}>
<sup className={styles.identifier}>2</sup>
<InlineMarkdown data={translate('MediaInfoFootNote2')} />
</div>
</FieldSet> </FieldSet>
<FieldSet legend={translate('ReleaseGroup')}> <FieldSet legend={translate('ReleaseGroup')}>
<div className={styles.groups}> <div className={styles.groups}>
{releaseGroupTokens.map(({ token, example, footNote }) => { {releaseGroupTokens.map(({ token, example, footNotes }) => {
return ( return (
<NamingOption <NamingOption
key={token} key={token}
token={token} token={token}
example={example} example={example}
footNote={footNote} footNotes={footNotes}
tokenSeparator={tokenSeparator} tokenSeparator={tokenSeparator}
tokenCase={tokenCase} tokenCase={tokenCase}
onPress={handleOptionPress} onPress={handleOptionPress}
@@ -385,20 +398,20 @@ function NamingModal(props: NamingModalProps) {
</div> </div>
<div className={styles.footNote}> <div className={styles.footNote}>
<Icon className={styles.icon} name={icons.FOOTNOTE} /> <sup className={styles.identifier}>1</sup>
<InlineMarkdown data={translate('ReleaseGroupFootNote')} /> <InlineMarkdown data={translate('ReleaseGroupFootNote')} />
</div> </div>
</FieldSet> </FieldSet>
<FieldSet legend={translate('Edition')}> <FieldSet legend={translate('Edition')}>
<div className={styles.groups}> <div className={styles.groups}>
{editionTokens.map(({ token, example, footNote }) => { {editionTokens.map(({ token, example, footNotes }) => {
return ( return (
<NamingOption <NamingOption
key={token} key={token}
token={token} token={token}
example={example} example={example}
footNote={footNote} footNotes={footNotes}
tokenSeparator={tokenSeparator} tokenSeparator={tokenSeparator}
tokenCase={tokenCase} tokenCase={tokenCase}
onPress={handleOptionPress} onPress={handleOptionPress}
@@ -408,7 +421,7 @@ function NamingModal(props: NamingModalProps) {
</div> </div>
<div className={styles.footNote}> <div className={styles.footNote}>
<Icon className={styles.icon} name={icons.FOOTNOTE} /> <sup className={styles.identifier}>1</sup>
<InlineMarkdown data={translate('EditionFootNote')} /> <InlineMarkdown data={translate('EditionFootNote')} />
</div> </div>
</FieldSet> </FieldSet>
@@ -40,7 +40,7 @@
padding: 6px; padding: 6px;
background-color: var(--popoverBodyBackgroundColor); background-color: var(--popoverBodyBackgroundColor);
.footNote { .footNotes {
padding: 2px; padding: 2px;
color: #aaa; color: #aaa;
} }
@@ -2,7 +2,7 @@
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'example': string; 'example': string;
'footNote': string; 'footNotes': string;
'isFullFilename': string; 'isFullFilename': string;
'large': string; 'large': string;
'lower': string; 'lower': string;
@@ -1,8 +1,6 @@
import classNames from 'classnames'; import classNames from 'classnames';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
import { icons } from 'Helpers/Props';
import { Size } from 'Helpers/Props/sizes'; import { Size } from 'Helpers/Props/sizes';
import TokenCase from './TokenCase'; import TokenCase from './TokenCase';
import TokenSeparator from './TokenSeparator'; import TokenSeparator from './TokenSeparator';
@@ -14,7 +12,7 @@ interface NamingOptionProps {
example: string; example: string;
tokenCase: TokenCase; tokenCase: TokenCase;
isFullFilename?: boolean; isFullFilename?: boolean;
footNote?: boolean; footNotes?: string;
size?: Extract<Size, keyof typeof styles>; size?: Extract<Size, keyof typeof styles>;
onPress: ({ onPress: ({
isFullFilename, isFullFilename,
@@ -32,7 +30,7 @@ function NamingOption(props: NamingOptionProps) {
example, example,
tokenCase, tokenCase,
isFullFilename = false, isFullFilename = false,
footNote = false, footNotes,
size = 'small', size = 'small',
onPress, onPress,
} = props; } = props;
@@ -66,8 +64,10 @@ function NamingOption(props: NamingOptionProps) {
<div className={styles.example}> <div className={styles.example}>
{example.replace(/ /g, tokenSeparator)} {example.replace(/ /g, tokenSeparator)}
{footNote ? ( {footNotes ? (
<Icon className={styles.footNote} name={icons.FOOTNOTE} /> <div className={styles.footNotes}>
<sup>{footNotes}</sup>
</div>
) : null} ) : null}
</div> </div>
</Link> </Link>
@@ -1,5 +1,6 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import Modal from 'Components/Modal/Modal'; import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props'; import { sizes } from 'Helpers/Props';
import { clearPendingChanges } from 'Store/Actions/baseActions'; import { clearPendingChanges } from 'Store/Actions/baseActions';
@@ -7,7 +8,8 @@ import EditMetadataModalContent, {
EditMetadataModalContentProps, EditMetadataModalContentProps,
} from './EditMetadataModalContent'; } from './EditMetadataModalContent';
interface EditMetadataModalProps extends EditMetadataModalContentProps { interface EditMetadataModalProps
extends Omit<EditMetadataModalContentProps, 'advancedSettings'> {
isOpen: boolean; isOpen: boolean;
} }
@@ -18,6 +20,10 @@ function EditMetadataModal({
}: EditMetadataModalProps) { }: EditMetadataModalProps) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const advancedSettings = useSelector(
(state: AppState) => state.settings.advancedSettings
);
const handleModalClose = useCallback(() => { const handleModalClose = useCallback(() => {
dispatch(clearPendingChanges({ section: 'metadata' })); dispatch(clearPendingChanges({ section: 'metadata' }));
onModalClose(); onModalClose();
@@ -27,6 +33,7 @@ function EditMetadataModal({
<Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={handleModalClose}> <Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={handleModalClose}>
<EditMetadataModalContent <EditMetadataModalContent
{...otherProps} {...otherProps}
advancedSettings={advancedSettings}
onModalClose={handleModalClose} onModalClose={handleModalClose}
/> />
</Modal> </Modal>
@@ -1,4 +1,4 @@
import React, { useCallback, useMemo } from 'react'; import React, { useCallback, useEffect, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState'; import AppState from 'App/State/AppState';
import Alert from 'Components/Alert'; import Alert from 'Components/Alert';
@@ -13,6 +13,7 @@ import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent'; import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import usePrevious from 'Helpers/Hooks/usePrevious';
import { inputTypes } from 'Helpers/Props'; import { inputTypes } from 'Helpers/Props';
import { import {
saveMetadata, saveMetadata,
@@ -41,6 +42,8 @@ function EditMetadataModalContent({
(state: AppState) => state.settings.metadata (state: AppState) => state.settings.metadata
); );
const wasSaving = usePrevious(isSaving);
const { settings, ...otherSettings } = useMemo(() => { const { settings, ...otherSettings } = useMemo(() => {
const item = items.find((item) => item.id === id)!; const item = items.find((item) => item.id === id)!;
@@ -69,6 +72,12 @@ function EditMetadataModalContent({
dispatch(saveMetadata({ id })); dispatch(saveMetadata({ id }));
}, [id, dispatch]); }, [id, dispatch]);
useEffect(() => {
if (wasSaving && !isSaving && !saveError) {
onModalClose();
}
}, [isSaving, wasSaving, saveError, onModalClose]);
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
@@ -95,7 +95,6 @@ function Metadata({ id, name, enable, fields }: MetadataProps) {
) : null} ) : null}
<EditMetadataModal <EditMetadataModal
advancedSettings={false}
id={id} id={id}
isOpen={isEditMetadataModalOpen} isOpen={isEditMetadataModalOpen}
onModalClose={handleModalClose} onModalClose={handleModalClose}
@@ -86,10 +86,10 @@ function EditSpecificationModalContent(props) {
<InlineMarkdown data={translate('ConditionUsingRegularExpressions')} /> <InlineMarkdown data={translate('ConditionUsingRegularExpressions')} />
</div> </div>
<div> <div>
<InlineMarkdown data={translate('RegularExpressionsTutorialLink')} /> <InlineMarkdown data={translate('RegularExpressionsTutorialLink', { url: 'https://www.regular-expressions.info/tutorial.html' })} />
</div> </div>
<div> <div>
<InlineMarkdown data={translate('RegularExpressionsCanBeTested')} /> <InlineMarkdown data={translate('RegularExpressionsCanBeTested', { url: 'http://regexstorm.net/tester' })} />
</div> </div>
</Alert> </Alert>
} }
@@ -86,7 +86,7 @@ export const actionHandlers = handleThunks({
section, section,
...item, ...item,
term, term,
queued: true, isQueued: true,
items: [] items: []
})); }));
@@ -151,6 +151,8 @@ export const actionHandlers = handleThunks({
abortCurrentLookup = abortRequest; abortCurrentLookup = abortRequest;
request.done((data) => { request.done((data) => {
const selectedMovie = queued.selectedMovie || data[0];
dispatch(updateItem({ dispatch(updateItem({
section, section,
id: queued.id, id: queued.id,
@@ -158,8 +160,8 @@ export const actionHandlers = handleThunks({
isPopulated: true, isPopulated: true,
error: null, error: null,
items: data, items: data,
queued: false, isQueued: false,
selectedMovie: queued.selectedMovie || data[0], selectedMovie,
updateOnly: true updateOnly: true
})); }));
}); });
@@ -171,7 +173,7 @@ export const actionHandlers = handleThunks({
isFetching: false, isFetching: false,
isPopulated: false, isPopulated: false,
error: xhr, error: xhr,
queued: false, isQueued: false,
updateOnly: true updateOnly: true
})); }));
}); });
@@ -278,7 +280,23 @@ export const actionHandlers = handleThunks({
export const reducers = createHandleActions({ export const reducers = createHandleActions({
[CANCEL_LOOKUP_MOVIE]: function(state) { [CANCEL_LOOKUP_MOVIE]: function(state) {
return Object.assign({}, state, { isLookingUpMovie: false }); queue.splice(0, queue.length);
const items = state.items.map((item) => {
if (item.isQueued) {
return {
...item,
isQueued: false
};
}
return item;
});
return Object.assign({}, state, {
isLookingUpMovie: false,
items
});
}, },
[CLEAR_IMPORT_MOVIE]: function(state) { [CLEAR_IMPORT_MOVIE]: function(state) {
@@ -210,6 +210,12 @@ export const defaultState = {
name: 'rejectionCount', name: 'rejectionCount',
label: () => translate('RejectionCount'), label: () => translate('RejectionCount'),
type: filterBuilderTypes.NUMBER type: filterBuilderTypes.NUMBER
},
{
name: 'movieRequested',
label: () => translate('MovieRequested'),
type: filterBuilderTypes.EXACT,
valueType: filterBuilderValueTypes.BOOL
} }
], ],
selectedFilterKey: 'all' selectedFilterKey: 'all'
@@ -1,13 +1,14 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import AppState from 'App/State/AppState'; import AppState from 'App/State/AppState';
import Movie from 'Movie/Movie'; import Movie from 'Movie/Movie';
import QualityProfile from 'typings/QualityProfile';
import { createMovieSelectorForHook } from './createMovieSelector'; import { createMovieSelectorForHook } from './createMovieSelector';
function createMovieQualityProfileSelector(movieId: number) { function createMovieQualityProfileSelector(movieId: number) {
return createSelector( return createSelector(
(state: AppState) => state.settings.qualityProfiles.items, (state: AppState) => state.settings.qualityProfiles.items,
createMovieSelectorForHook(movieId), createMovieSelectorForHook(movieId),
(qualityProfiles, movie = {} as Movie) => { (qualityProfiles: QualityProfile[], movie = {} as Movie) => {
return qualityProfiles.find( return qualityProfiles.find(
(profile) => profile.id === movie.qualityProfileId (profile) => profile.id === movie.qualityProfileId
); );
@@ -0,0 +1,19 @@
import { filesize } from 'filesize';
function formatBitrate(input: string | number) {
const size = Number(input);
if (isNaN(size)) {
return '';
}
const { value, symbol } = filesize(size, {
base: 10,
round: 1,
output: 'object',
});
return `${value} ${symbol}/s`;
}
export default formatBitrate;
@@ -0,0 +1,9 @@
export type Entries<T> = {
[K in keyof T]: [K, T[K]];
}[keyof T][];
function getEntries<T extends object>(obj: T): Entries<T> {
return Object.entries(obj) as Entries<T>;
}
export default getEntries;
@@ -35,7 +35,7 @@ export default function getLanguageName(code: string) {
try { try {
return languageNames.of(code) ?? code; return languageNames.of(code) ?? code;
} catch (error) { } catch {
return code; return code;
} }
} }
+1 -1
View File
@@ -17,7 +17,7 @@ export async function fetchTranslations(): Promise<boolean> {
translations = data.Strings; translations = data.Strings;
resolve(true); resolve(true);
} catch (error) { } catch {
resolve(false); resolve(false);
} }
}); });
@@ -8,7 +8,7 @@ import movieEntities from 'Movie/movieEntities';
import MovieSearchCell from 'Movie/MovieSearchCell'; import MovieSearchCell from 'Movie/MovieSearchCell';
import MovieStatusConnector from 'Movie/MovieStatusConnector'; import MovieStatusConnector from 'Movie/MovieStatusConnector';
import MovieTitleLink from 'Movie/MovieTitleLink'; import MovieTitleLink from 'Movie/MovieTitleLink';
import MovieFileLanguageConnector from 'MovieFile/MovieFileLanguageConnector'; import MovieFileLanguages from 'MovieFile/MovieFileLanguages';
import styles from './CutoffUnmetRow.css'; import styles from './CutoffUnmetRow.css';
function CutoffUnmetRow(props) { function CutoffUnmetRow(props) {
@@ -104,7 +104,7 @@ function CutoffUnmetRow(props) {
key={name} key={name}
className={styles.languages} className={styles.languages}
> >
<MovieFileLanguageConnector <MovieFileLanguages
movieFileId={movieFileId} movieFileId={movieFileId}
/> />
</TableRowCell> </TableRowCell>
+5 -4
View File
@@ -23,12 +23,13 @@ const error = console.error;
function logError(...parameters: any[]) { function logError(...parameters: any[]) {
const filter = parameters.find((parameter) => { const filter = parameters.find((parameter) => {
return ( return (
parameter.includes( typeof parameter === 'string' &&
(parameter.includes(
'Support for defaultProps will be removed from function components in a future major release' 'Support for defaultProps will be removed from function components in a future major release'
) || ) ||
parameter.includes( parameter.includes(
'findDOMNode is deprecated and will be removed in the next major release' 'findDOMNode is deprecated and will be removed in the next major release'
) ))
); );
}); });
+15 -15
View File
@@ -22,11 +22,11 @@
"defaults" "defaults"
], ],
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "6.6.0", "@fortawesome/fontawesome-free": "6.7.1",
"@fortawesome/fontawesome-svg-core": "6.6.0", "@fortawesome/fontawesome-svg-core": "6.7.1",
"@fortawesome/free-brands-svg-icons": "6.6.0", "@fortawesome/free-brands-svg-icons": "6.7.1",
"@fortawesome/free-regular-svg-icons": "6.6.0", "@fortawesome/free-regular-svg-icons": "6.7.1",
"@fortawesome/free-solid-svg-icons": "6.6.0", "@fortawesome/free-solid-svg-icons": "6.7.1",
"@fortawesome/react-fontawesome": "0.2.2", "@fortawesome/react-fontawesome": "0.2.2",
"@juggle/resize-observer": "3.4.0", "@juggle/resize-observer": "3.4.0",
"@microsoft/signalr": "6.0.25", "@microsoft/signalr": "6.0.25",
@@ -84,16 +84,16 @@
"reselect": "4.1.8", "reselect": "4.1.8",
"stacktrace-js": "2.0.2", "stacktrace-js": "2.0.2",
"swiper": "8.3.2", "swiper": "8.3.2",
"typescript": "5.1.6" "typescript": "5.7.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.25.8", "@babel/core": "7.26.0",
"@babel/eslint-parser": "7.25.8", "@babel/eslint-parser": "7.25.9",
"@babel/plugin-proposal-export-default-from": "7.25.8", "@babel/plugin-proposal-export-default-from": "7.25.9",
"@babel/plugin-syntax-dynamic-import": "7.8.3", "@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/preset-env": "7.25.8", "@babel/preset-env": "7.26.0",
"@babel/preset-react": "7.25.7", "@babel/preset-react": "7.26.3",
"@babel/preset-typescript": "7.25.7", "@babel/preset-typescript": "7.26.0",
"@types/lodash": "4.14.195", "@types/lodash": "4.14.195",
"@types/react-document-title": "2.0.10", "@types/react-document-title": "2.0.10",
"@types/react-lazyload": "3.2.3", "@types/react-lazyload": "3.2.3",
@@ -102,13 +102,13 @@
"@types/react-window": "1.8.8", "@types/react-window": "1.8.8",
"@types/redux-actions": "2.6.5", "@types/redux-actions": "2.6.5",
"@types/webpack-livereload-plugin": "2.3.6", "@types/webpack-livereload-plugin": "2.3.6",
"@typescript-eslint/eslint-plugin": "6.21.0", "@typescript-eslint/eslint-plugin": "8.18.1",
"@typescript-eslint/parser": "6.21.0", "@typescript-eslint/parser": "8.18.1",
"autoprefixer": "10.4.20", "autoprefixer": "10.4.20",
"babel-loader": "9.2.1", "babel-loader": "9.2.1",
"babel-plugin-inline-classnames": "2.0.1", "babel-plugin-inline-classnames": "2.0.1",
"babel-plugin-transform-react-remove-prop-types": "0.4.24", "babel-plugin-transform-react-remove-prop-types": "0.4.24",
"core-js": "3.38.1", "core-js": "3.39.0",
"css-loader": "6.7.3", "css-loader": "6.7.3",
"css-modules-typescript-loader": "4.0.1", "css-modules-typescript-loader": "4.0.1",
"eslint": "8.57.1", "eslint": "8.57.1",
+10 -9
View File
@@ -42,17 +42,18 @@ namespace NzbDrone.Common
public void CreateZip(string path, IEnumerable<string> files) public void CreateZip(string path, IEnumerable<string> files)
{ {
using (var zipFile = ZipFile.Create(path)) _logger.Debug("Creating archive {0}", path);
using var zipFile = ZipFile.Create(path);
zipFile.BeginUpdate();
foreach (var file in files)
{ {
zipFile.BeginUpdate(); zipFile.Add(file, Path.GetFileName(file));
foreach (var file in files)
{
zipFile.Add(file, Path.GetFileName(file));
}
zipFile.CommitUpdate();
} }
zipFile.CommitUpdate();
} }
private void ExtractZip(string compressedFile, string destination) private void ExtractZip(string compressedFile, string destination)
@@ -341,10 +341,11 @@ namespace NzbDrone.Common.Disk
var isCifs = targetDriveFormat == "cifs"; var isCifs = targetDriveFormat == "cifs";
var isBtrfs = sourceDriveFormat == "btrfs" && targetDriveFormat == "btrfs"; var isBtrfs = sourceDriveFormat == "btrfs" && targetDriveFormat == "btrfs";
var isZfs = sourceDriveFormat == "zfs" && targetDriveFormat == "zfs";
if (mode.HasFlag(TransferMode.Copy)) if (mode.HasFlag(TransferMode.Copy))
{ {
if (isBtrfs) if (isBtrfs || isZfs)
{ {
if (_diskProvider.TryCreateRefLink(sourcePath, targetPath)) if (_diskProvider.TryCreateRefLink(sourcePath, targetPath))
{ {
@@ -358,7 +359,7 @@ namespace NzbDrone.Common.Disk
if (mode.HasFlag(TransferMode.Move)) if (mode.HasFlag(TransferMode.Move))
{ {
if (isBtrfs) if (isBtrfs || isZfs)
{ {
if (isSameMount && _diskProvider.TryRenameFile(sourcePath, targetPath)) if (isSameMount && _diskProvider.TryRenameFile(sourcePath, targetPath))
{ {
@@ -17,37 +17,6 @@ namespace NzbDrone.Common.Disk
private readonly IDiskProvider _diskProvider; private readonly IDiskProvider _diskProvider;
private readonly IRuntimeInfo _runtimeInfo; private readonly IRuntimeInfo _runtimeInfo;
private readonly HashSet<string> _setToRemove = new HashSet<string>
{
// Windows
"boot",
"bootmgr",
"cache",
"msocache",
"recovery",
"$recycle.bin",
"recycler",
"system volume information",
"temporary internet files",
"windows",
// OS X
".fseventd",
".spotlight",
".trashes",
".vol",
"cachedmessages",
"caches",
"trash",
// QNAP
".@__thumb",
// Synology
"@eadir",
"#recycle"
};
public FileSystemLookupService(IDiskProvider diskProvider, IRuntimeInfo runtimeInfo) public FileSystemLookupService(IDiskProvider diskProvider, IRuntimeInfo runtimeInfo)
{ {
_diskProvider = diskProvider; _diskProvider = diskProvider;
@@ -158,7 +127,7 @@ namespace NzbDrone.Common.Disk
}) })
.ToList(); .ToList();
directories.RemoveAll(d => _setToRemove.Contains(d.Name.ToLowerInvariant())); directories.RemoveAll(d => SpecialFolders.IsSpecialFolder(d.Name));
return directories; return directories;
} }
@@ -0,0 +1,47 @@
using System.Collections.Generic;
namespace NzbDrone.Common.Disk;
public static class SpecialFolders
{
private static readonly HashSet<string> _specialFolders = new HashSet<string>
{
// Windows
"boot",
"bootmgr",
"cache",
"msocache",
"recovery",
"$recycle.bin",
"recycler",
"system volume information",
"temporary internet files",
"windows",
// OS X
".fseventd",
".spotlight",
".trashes",
".vol",
"cachedmessages",
"caches",
"trash",
// QNAP
".@__thumb",
// Synology
"@eadir",
"#recycle"
};
public static bool IsSpecialFolder(string folder)
{
if (folder == null)
{
return false;
}
return _specialFolders.Contains(folder.ToLowerInvariant());
}
}
@@ -54,10 +54,8 @@ namespace NzbDrone.Common.Extensions
foreach (var item in src) foreach (var item in src)
{ {
var key = keySelector(item); var key = keySelector(item);
if (!result.ContainsKey(key))
{ result.TryAdd(key, item);
result[key] = item;
}
} }
return result; return result;
@@ -69,10 +67,9 @@ namespace NzbDrone.Common.Extensions
foreach (var item in src) foreach (var item in src)
{ {
var key = keySelector(item); var key = keySelector(item);
if (!result.ContainsKey(key)) var value = valueSelector(item);
{
result[key] = valueSelector(item); result.TryAdd(key, value);
}
} }
return result; return result;
@@ -0,0 +1,21 @@
using System.Text;
using NLog;
using NLog.Layouts.ClefJsonLayout;
using NzbDrone.Common.EnvironmentInfo;
namespace NzbDrone.Common.Instrumentation;
public class CleansingClefLogLayout : CompactJsonLayout
{
protected override void RenderFormattedMessage(LogEventInfo logEvent, StringBuilder target)
{
base.RenderFormattedMessage(logEvent, target);
if (RuntimeInfo.IsProduction)
{
var result = CleanseLogMessage.Cleanse(target.ToString());
target.Clear();
target.Append(result);
}
}
}
@@ -0,0 +1,26 @@
using System.Text;
using NLog;
using NLog.Layouts;
using NzbDrone.Common.EnvironmentInfo;
namespace NzbDrone.Common.Instrumentation;
public class CleansingConsoleLogLayout : SimpleLayout
{
public CleansingConsoleLogLayout(string format)
: base(format)
{
}
protected override void RenderFormattedMessage(LogEventInfo logEvent, StringBuilder target)
{
base.RenderFormattedMessage(logEvent, target);
if (RuntimeInfo.IsProduction)
{
var result = CleanseLogMessage.Cleanse(target.ToString());
target.Clear();
target.Append(result);
}
}
}
@@ -4,7 +4,7 @@ using NLog.Targets;
namespace NzbDrone.Common.Instrumentation namespace NzbDrone.Common.Instrumentation
{ {
public class NzbDroneFileTarget : FileTarget public class CleansingFileTarget : FileTarget
{ {
protected override void RenderFormattedMessage(LogEventInfo logEvent, StringBuilder target) protected override void RenderFormattedMessage(LogEventInfo logEvent, StringBuilder target)
{ {
@@ -3,7 +3,6 @@ using System.Diagnostics;
using System.IO; using System.IO;
using NLog; using NLog;
using NLog.Config; using NLog.Config;
using NLog.Layouts.ClefJsonLayout;
using NLog.Targets; using NLog.Targets;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
@@ -13,9 +12,11 @@ namespace NzbDrone.Common.Instrumentation
{ {
public static class NzbDroneLogger public static class NzbDroneLogger
{ {
private const string FILE_LOG_LAYOUT = @"${date:format=yyyy-MM-dd HH\:mm\:ss.f}|${level}|${logger}|${message}${onexception:inner=${newline}${newline}[v${assembly-version}] ${exception:format=ToString}${newline}${exception:format=Data}${newline}}"; private const string FileLogLayout = @"${date:format=yyyy-MM-dd HH\:mm\:ss.f}|${level}|${logger}|${message}${onexception:inner=${newline}${newline}[v${assembly-version}] ${exception:format=ToString}${newline}${exception:format=Data}${newline}}";
public const string ConsoleLogLayout = "[${level}] ${logger}: ${message} ${onexception:inner=${newline}${newline}[v${assembly-version}] ${exception:format=ToString}${newline}${exception:format=Data}${newline}}"; private const string ConsoleFormat = "[${level}] ${logger}: ${message} ${onexception:inner=${newline}${newline}[v${assembly-version}] ${exception:format=ToString}${newline}${exception:format=Data}${newline}}";
public static CompactJsonLayout ClefLogLayout = new CompactJsonLayout();
private static readonly CleansingConsoleLogLayout CleansingConsoleLayout = new (ConsoleFormat);
private static readonly CleansingClefLogLayout ClefLogLayout = new ();
private static bool _isConfigured; private static bool _isConfigured;
@@ -119,11 +120,7 @@ namespace NzbDrone.Common.Instrumentation
? formatEnumValue ? formatEnumValue
: ConsoleLogFormat.Standard; : ConsoleLogFormat.Standard;
coloredConsoleTarget.Layout = logFormat switch ConfigureConsoleLayout(coloredConsoleTarget, logFormat);
{
ConsoleLogFormat.Clef => ClefLogLayout,
_ => ConsoleLogLayout
};
var loggingRule = new LoggingRule("*", level, coloredConsoleTarget); var loggingRule = new LoggingRule("*", level, coloredConsoleTarget);
@@ -140,7 +137,7 @@ namespace NzbDrone.Common.Instrumentation
private static void RegisterAppFile(IAppFolderInfo appFolderInfo, string name, string fileName, int maxArchiveFiles, LogLevel minLogLevel) private static void RegisterAppFile(IAppFolderInfo appFolderInfo, string name, string fileName, int maxArchiveFiles, LogLevel minLogLevel)
{ {
var fileTarget = new NzbDroneFileTarget(); var fileTarget = new CleansingFileTarget();
fileTarget.Name = name; fileTarget.Name = name;
fileTarget.FileName = Path.Combine(appFolderInfo.GetLogFolder(), fileName); fileTarget.FileName = Path.Combine(appFolderInfo.GetLogFolder(), fileName);
@@ -153,7 +150,7 @@ namespace NzbDrone.Common.Instrumentation
fileTarget.MaxArchiveFiles = maxArchiveFiles; fileTarget.MaxArchiveFiles = maxArchiveFiles;
fileTarget.EnableFileDelete = true; fileTarget.EnableFileDelete = true;
fileTarget.ArchiveNumbering = ArchiveNumberingMode.Rolling; fileTarget.ArchiveNumbering = ArchiveNumberingMode.Rolling;
fileTarget.Layout = FILE_LOG_LAYOUT; fileTarget.Layout = FileLogLayout;
var loggingRule = new LoggingRule("*", minLogLevel, fileTarget); var loggingRule = new LoggingRule("*", minLogLevel, fileTarget);
@@ -172,7 +169,7 @@ namespace NzbDrone.Common.Instrumentation
fileTarget.ConcurrentWrites = false; fileTarget.ConcurrentWrites = false;
fileTarget.ConcurrentWriteAttemptDelay = 50; fileTarget.ConcurrentWriteAttemptDelay = 50;
fileTarget.ConcurrentWriteAttempts = 100; fileTarget.ConcurrentWriteAttempts = 100;
fileTarget.Layout = FILE_LOG_LAYOUT; fileTarget.Layout = FileLogLayout;
var loggingRule = new LoggingRule("*", LogLevel.Trace, fileTarget); var loggingRule = new LoggingRule("*", LogLevel.Trace, fileTarget);
@@ -217,6 +214,15 @@ namespace NzbDrone.Common.Instrumentation
{ {
return GetLogger(obj.GetType()); return GetLogger(obj.GetType());
} }
public static void ConfigureConsoleLayout(ColoredConsoleTarget target, ConsoleLogFormat format)
{
target.Layout = format switch
{
ConsoleLogFormat.Clef => NzbDroneLogger.ClefLogLayout,
_ => NzbDroneLogger.CleansingConsoleLayout
};
}
} }
public enum ConsoleLogFormat public enum ConsoleLogFormat
@@ -119,7 +119,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
o.Environment = BuildInfo.Branch; o.Environment = BuildInfo.Branch;
// Crash free run statistics (sends a ping for healthy and for crashes sessions) // Crash free run statistics (sends a ping for healthy and for crashes sessions)
o.AutoSessionTracking = true; o.AutoSessionTracking = false;
// Caches files in the event device is offline // Caches files in the event device is offline
// Sentry creates a 'sentry' sub directory, no need to concat here // Sentry creates a 'sentry' sub directory, no need to concat here
+1 -1
View File
@@ -10,7 +10,7 @@
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" /> <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NLog" Version="5.3.4" /> <PackageReference Include="NLog" Version="5.3.4" />
<PackageReference Include="NLog.Layouts.ClefJsonLayout" Version="1.0.2" /> <PackageReference Include="NLog.Layouts.ClefJsonLayout" Version="1.0.3" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.15" /> <PackageReference Include="NLog.Extensions.Logging" Version="5.3.15" />
<PackageReference Include="Npgsql" Version="7.0.9" /> <PackageReference Include="Npgsql" Version="7.0.9" />
<PackageReference Include="Sentry" Version="4.0.2" /> <PackageReference Include="Sentry" Version="4.0.2" />
@@ -0,0 +1,55 @@
using System.Linq;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Datastore.Migration;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Datastore.Migration
{
[TestFixture]
public class stevenlu_update_urlFixture : MigrationTest<stevenlu_update_url>
{
[Test]
public void should_update_stevenlu_url()
{
var db = WithMigrationTestDb(c =>
{
c.Insert.IntoTable("ImportLists").Row(new
{
Enabled = true,
EnableAuto = true,
Name = "StevenLu List",
QualityProfileId = 1,
MinimumAvailability = 1,
RootFolderPath = "/movies",
Monitor = 0,
SearchOnAdd = true,
Tags = "[]",
Implementation = "StevenLuImport",
ConfigContract = "StevenLuSettings",
Settings = new StevenLuSettings241
{
Link = "https://s3.amazonaws.com/popular-movies/movies.json"
}.ToJson()
});
});
var items = db.Query<ImportListDefinition241>("SELECT \"Id\", \"Settings\" FROM \"ImportLists\"");
items.Should().HaveCount(1);
items.First().Settings.Link.Should().Be("https://popular-movies-data.stevenlu.com/movies.json");
}
}
public class ImportListDefinition241 : ModelBase
{
public StevenLuSettings241 Settings { get; set; }
}
public class StevenLuSettings241
{
public string Link { get; set; }
}
}
@@ -20,32 +20,32 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
private List<ReleaseInfo> _reports; private List<ReleaseInfo> _reports;
private RemoteMovie _remoteEpisode; private RemoteMovie _remoteEpisode;
private Mock<IDecisionEngineSpecification> _pass1; private Mock<IDownloadDecisionEngineSpecification> _pass1;
private Mock<IDecisionEngineSpecification> _pass2; private Mock<IDownloadDecisionEngineSpecification> _pass2;
private Mock<IDecisionEngineSpecification> _pass3; private Mock<IDownloadDecisionEngineSpecification> _pass3;
private Mock<IDecisionEngineSpecification> _fail1; private Mock<IDownloadDecisionEngineSpecification> _fail1;
private Mock<IDecisionEngineSpecification> _fail2; private Mock<IDownloadDecisionEngineSpecification> _fail2;
private Mock<IDecisionEngineSpecification> _fail3; private Mock<IDownloadDecisionEngineSpecification> _fail3;
[SetUp] [SetUp]
public void Setup() public void Setup()
{ {
_pass1 = new Mock<IDecisionEngineSpecification>(); _pass1 = new Mock<IDownloadDecisionEngineSpecification>();
_pass2 = new Mock<IDecisionEngineSpecification>(); _pass2 = new Mock<IDownloadDecisionEngineSpecification>();
_pass3 = new Mock<IDecisionEngineSpecification>(); _pass3 = new Mock<IDownloadDecisionEngineSpecification>();
_fail1 = new Mock<IDecisionEngineSpecification>(); _fail1 = new Mock<IDownloadDecisionEngineSpecification>();
_fail2 = new Mock<IDecisionEngineSpecification>(); _fail2 = new Mock<IDownloadDecisionEngineSpecification>();
_fail3 = new Mock<IDecisionEngineSpecification>(); _fail3 = new Mock<IDownloadDecisionEngineSpecification>();
_pass1.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteMovie>(), null)).Returns(Decision.Accept); _pass1.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteMovie>(), null)).Returns(DownloadSpecDecision.Accept);
_pass2.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteMovie>(), null)).Returns(Decision.Accept); _pass2.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteMovie>(), null)).Returns(DownloadSpecDecision.Accept);
_pass3.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteMovie>(), null)).Returns(Decision.Accept); _pass3.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteMovie>(), null)).Returns(DownloadSpecDecision.Accept);
_fail1.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteMovie>(), null)).Returns(Decision.Reject("fail1")); _fail1.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteMovie>(), null)).Returns(DownloadSpecDecision.Reject(DownloadRejectionReason.Unknown, "fail1"));
_fail2.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteMovie>(), null)).Returns(Decision.Reject("fail2")); _fail2.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteMovie>(), null)).Returns(DownloadSpecDecision.Reject(DownloadRejectionReason.Unknown, "fail2"));
_fail3.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteMovie>(), null)).Returns(Decision.Reject("fail3")); _fail3.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteMovie>(), null)).Returns(DownloadSpecDecision.Reject(DownloadRejectionReason.Unknown, "fail3"));
_reports = new List<ReleaseInfo> { new ReleaseInfo { Title = "Trolls.2016.720p.WEB-DL.DD5.1.H264-FGT" } }; _reports = new List<ReleaseInfo> { new ReleaseInfo { Title = "Trolls.2016.720p.WEB-DL.DD5.1.H264-FGT" } };
_remoteEpisode = new RemoteMovie _remoteEpisode = new RemoteMovie
@@ -58,9 +58,9 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
.Setup(c => c.Map(It.IsAny<ParsedMovieInfo>(), It.IsAny<string>(), It.IsAny<int>(), It.IsAny<SearchCriteriaBase>())).Returns(_remoteEpisode); .Setup(c => c.Map(It.IsAny<ParsedMovieInfo>(), It.IsAny<string>(), It.IsAny<int>(), It.IsAny<SearchCriteriaBase>())).Returns(_remoteEpisode);
} }
private void GivenSpecifications(params Mock<IDecisionEngineSpecification>[] mocks) private void GivenSpecifications(params Mock<IDownloadDecisionEngineSpecification>[] mocks)
{ {
Mocker.SetConstant<IEnumerable<IDecisionEngineSpecification>>(mocks.Select(c => c.Object)); Mocker.SetConstant<IEnumerable<IDownloadDecisionEngineSpecification>>(mocks.Select(c => c.Object));
} }
[Test] [Test]
@@ -5,6 +5,7 @@ using FluentAssertions;
using Moq; using Moq;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common.Serializer; using NzbDrone.Common.Serializer;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.CustomFormats; using NzbDrone.Core.CustomFormats;
using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
@@ -337,5 +338,42 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeFalse(); Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeFalse();
} }
[Test]
public void should_return_false_if_quality_profile_does_not_allow_upgrades_but_format_cutoff_is_above_current_score_and_is_revision_upgrade()
{
var customFormat = new CustomFormat("My Format", new ResolutionSpecification { Value = (int)Resolution.R1080p }) { Id = 1 };
Mocker.GetMock<IConfigService>()
.SetupGet(s => s.DownloadPropersAndRepacks)
.Returns(ProperDownloadTypes.DoNotPrefer);
GivenProfile(new QualityProfile
{
Cutoff = Quality.SDTV.Id,
MinFormatScore = 0,
CutoffFormatScore = 10000,
Items = Qualities.QualityFixture.GetDefaultQualities(),
FormatItems = CustomFormatsTestHelpers.GetSampleFormatItems("My Format"),
UpgradeAllowed = false
});
_parseResultSingle.Movie.QualityProfile.FormatItems = new List<ProfileFormatItem>
{
new ProfileFormatItem
{
Format = customFormat,
Score = 50
}
};
GivenFileQuality(new QualityModel(Quality.WEBDL1080p, new Revision(version: 1)));
GivenNewQuality(new QualityModel(Quality.WEBDL1080p, new Revision(version: 2)));
GivenOldCustomFormats(new List<CustomFormat>());
GivenNewCustomFormats(new List<CustomFormat> { customFormat });
Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeFalse();
}
} }
} }
@@ -107,6 +107,25 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
.Should().Be(UpgradeableRejectReason.None); .Should().Be(UpgradeableRejectReason.None);
} }
[Test]
public void should_return_false_if_proper_and_autoDownloadPropers_is_do_not_prefer()
{
GivenAutoDownloadPropers(ProperDownloadTypes.DoNotPrefer);
var profile = new QualityProfile
{
Items = Qualities.QualityFixture.GetDefaultQualities(),
};
Subject.IsUpgradable(
profile,
new QualityModel(Quality.DVD, new Revision(version: 1)),
new List<CustomFormat>(),
new QualityModel(Quality.DVD, new Revision(version: 2)),
new List<CustomFormat>())
.Should().Be(UpgradeableRejectReason.UpgradesNotAllowed);
}
[Test] [Test]
public void should_return_false_if_release_and_existing_file_are_the_same() public void should_return_false_if_release_and_existing_file_are_the_same()
{ {
@@ -121,7 +140,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
new List<CustomFormat>(), new List<CustomFormat>(),
new QualityModel(Quality.HDTV720p, new Revision(version: 1)), new QualityModel(Quality.HDTV720p, new Revision(version: 1)),
new List<CustomFormat>()) new List<CustomFormat>())
.Should().Be(UpgradeableRejectReason.CustomFormatScore); .Should().Be(UpgradeableRejectReason.UpgradesNotAllowed);
} }
[Test] [Test]
@@ -74,7 +74,7 @@ namespace NzbDrone.Core.Test.Download.Aggregation.Aggregators
Settings = new TorrentRssIndexerSettings { MultiLanguages = new List<int> { Language.Original.Id, Language.French.Id } } Settings = new TorrentRssIndexerSettings { MultiLanguages = new List<int> { Language.Original.Id, Language.French.Id } }
}; };
Mocker.GetMock<IIndexerFactory>() Mocker.GetMock<IIndexerFactory>()
.Setup(v => v.Get(1)) .Setup(v => v.Find(1))
.Returns(indexerDefinition); .Returns(indexerDefinition);
_remoteMovie.ParsedMovieInfo = GetParsedMovieInfo(new List<Language> { }, releaseTitle); _remoteMovie.ParsedMovieInfo = GetParsedMovieInfo(new List<Language> { }, releaseTitle);
@@ -82,7 +82,7 @@ namespace NzbDrone.Core.Test.Download.Aggregation.Aggregators
_remoteMovie.Release.Title = releaseTitle; _remoteMovie.Release.Title = releaseTitle;
Subject.Aggregate(_remoteMovie).Languages.Should().BeEquivalentTo(new List<Language> { _movie.MovieMetadata.Value.OriginalLanguage, Language.French }); Subject.Aggregate(_remoteMovie).Languages.Should().BeEquivalentTo(new List<Language> { _movie.MovieMetadata.Value.OriginalLanguage, Language.French });
Mocker.GetMock<IIndexerFactory>().Verify(c => c.Get(1), Times.Once()); Mocker.GetMock<IIndexerFactory>().Verify(c => c.Find(1), Times.Once());
Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls(); Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls();
} }
@@ -104,7 +104,7 @@ namespace NzbDrone.Core.Test.Download.Aggregation.Aggregators
}; };
Mocker.GetMock<IIndexerFactory>() Mocker.GetMock<IIndexerFactory>()
.Setup(v => v.Get(1)) .Setup(v => v.Find(1))
.Returns(indexerDefinition1); .Returns(indexerDefinition1);
Mocker.GetMock<IIndexerFactory>() Mocker.GetMock<IIndexerFactory>()
@@ -117,7 +117,7 @@ namespace NzbDrone.Core.Test.Download.Aggregation.Aggregators
_remoteMovie.Release.Title = releaseTitle; _remoteMovie.Release.Title = releaseTitle;
Subject.Aggregate(_remoteMovie).Languages.Should().BeEquivalentTo(new List<Language> { _movie.MovieMetadata.Value.OriginalLanguage, Language.French }); Subject.Aggregate(_remoteMovie).Languages.Should().BeEquivalentTo(new List<Language> { _movie.MovieMetadata.Value.OriginalLanguage, Language.French });
Mocker.GetMock<IIndexerFactory>().Verify(c => c.Get(1), Times.Once()); Mocker.GetMock<IIndexerFactory>().Verify(c => c.Find(1), Times.Once());
Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls(); Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls();
} }
@@ -155,7 +155,7 @@ namespace NzbDrone.Core.Test.Download.Aggregation.Aggregators
Settings = new TorrentRssIndexerSettings { MultiLanguages = new List<int> { Language.Original.Id, Language.French.Id } } Settings = new TorrentRssIndexerSettings { MultiLanguages = new List<int> { Language.Original.Id, Language.French.Id } }
}; };
Mocker.GetMock<IIndexerFactory>() Mocker.GetMock<IIndexerFactory>()
.Setup(v => v.Get(1)) .Setup(v => v.Find(1))
.Returns(indexerDefinition); .Returns(indexerDefinition);
_remoteMovie.ParsedMovieInfo = GetParsedMovieInfo(new List<Language> { Language.Unknown }, releaseTitle); _remoteMovie.ParsedMovieInfo = GetParsedMovieInfo(new List<Language> { Language.Unknown }, releaseTitle);
@@ -163,7 +163,51 @@ namespace NzbDrone.Core.Test.Download.Aggregation.Aggregators
_remoteMovie.Release.Title = releaseTitle; _remoteMovie.Release.Title = releaseTitle;
Subject.Aggregate(_remoteMovie).Languages.Should().BeEquivalentTo(new List<Language> { _movie.MovieMetadata.Value.OriginalLanguage, Language.French }); Subject.Aggregate(_remoteMovie).Languages.Should().BeEquivalentTo(new List<Language> { _movie.MovieMetadata.Value.OriginalLanguage, Language.French });
Mocker.GetMock<IIndexerFactory>().Verify(c => c.Get(1), Times.Once()); Mocker.GetMock<IIndexerFactory>().Verify(c => c.Find(1), Times.Once());
Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls();
}
[Test]
public void should_return_multi_languages_when_release_as_specified_language_and_indexer_has_multi_languages_configuration()
{
var releaseTitle = "Some.Movie.2024.MULTi.VFF.VFQ.1080p.BluRay.DTS.HDMA.x264-RlsGroup";
var indexerDefinition = new IndexerDefinition
{
Id = 1,
Settings = new TorrentRssIndexerSettings { MultiLanguages = new List<int> { Language.Original.Id, Language.French.Id } }
};
Mocker.GetMock<IIndexerFactory>()
.Setup(v => v.Find(1))
.Returns(indexerDefinition);
_remoteMovie.ParsedMovieInfo = GetParsedMovieInfo(new List<Language> { Language.French }, releaseTitle);
_remoteMovie.Release.IndexerId = 1;
_remoteMovie.Release.Title = releaseTitle;
Subject.Aggregate(_remoteMovie).Languages.Should().BeEquivalentTo(new List<Language> { _movie.MovieMetadata.Value.OriginalLanguage, Language.French });
Mocker.GetMock<IIndexerFactory>().Verify(c => c.Find(1), Times.Once());
Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls();
}
[Test]
public void should_return_multi_languages_when_release_as_other_language_and_indexer_has_multi_languages_configuration()
{
var releaseTitle = "Some.Movie.2024.MULTi.GERMAN.1080p.BluRay.DTS.HDMA.x264-RlsGroup";
var indexerDefinition = new IndexerDefinition
{
Id = 1,
Settings = new TorrentRssIndexerSettings { MultiLanguages = new List<int> { Language.Original.Id, Language.French.Id } }
};
Mocker.GetMock<IIndexerFactory>()
.Setup(v => v.Find(1))
.Returns(indexerDefinition);
_remoteMovie.ParsedMovieInfo = GetParsedMovieInfo(new List<Language> { Language.German }, releaseTitle);
_remoteMovie.Release.IndexerId = 1;
_remoteMovie.Release.Title = releaseTitle;
Subject.Aggregate(_remoteMovie).Languages.Should().BeEquivalentTo(new List<Language> { _movie.MovieMetadata.Value.OriginalLanguage, Language.French, Language.German });
Mocker.GetMock<IIndexerFactory>().Verify(c => c.Find(1), Times.Once());
Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls(); Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls();
} }
@@ -177,7 +221,7 @@ namespace NzbDrone.Core.Test.Download.Aggregation.Aggregators
Settings = new TorrentRssIndexerSettings { } Settings = new TorrentRssIndexerSettings { }
}; };
Mocker.GetMock<IIndexerFactory>() Mocker.GetMock<IIndexerFactory>()
.Setup(v => v.Get(1)) .Setup(v => v.Find(1))
.Returns(indexerDefinition); .Returns(indexerDefinition);
_remoteMovie.ParsedMovieInfo = GetParsedMovieInfo(new List<Language> { }, releaseTitle); _remoteMovie.ParsedMovieInfo = GetParsedMovieInfo(new List<Language> { }, releaseTitle);
@@ -185,7 +229,7 @@ namespace NzbDrone.Core.Test.Download.Aggregation.Aggregators
_remoteMovie.Release.Title = releaseTitle; _remoteMovie.Release.Title = releaseTitle;
Subject.Aggregate(_remoteMovie).Languages.Should().BeEquivalentTo(new List<Language> { _movie.MovieMetadata.Value.OriginalLanguage }); Subject.Aggregate(_remoteMovie).Languages.Should().BeEquivalentTo(new List<Language> { _movie.MovieMetadata.Value.OriginalLanguage });
Mocker.GetMock<IIndexerFactory>().Verify(c => c.Get(1), Times.Once()); Mocker.GetMock<IIndexerFactory>().Verify(c => c.Find(1), Times.Once());
Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls(); Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls();
} }
@@ -248,5 +292,85 @@ namespace NzbDrone.Core.Test.Download.Aggregation.Aggregators
Subject.Aggregate(_remoteMovie).Languages.Should().Equal(Language.Greek); Subject.Aggregate(_remoteMovie).Languages.Should().Equal(Language.Greek);
} }
[Test]
public void should_return_multi_languages_from_indexer_with_name_when_indexer_id_does_not_exist()
{
var releaseTitle = "Series.Title.S01E01.MULTi.1080p.WEB.H265-RlsGroup";
var indexerDefinition1 = new IndexerDefinition
{
Id = 1,
Name = "MyIndexer1",
Settings = new TorrentRssIndexerSettings { MultiLanguages = new List<int> { Language.Original.Id, Language.French.Id } }
};
var indexerDefinition2 = new IndexerDefinition
{
Id = 2,
Name = "MyIndexer2",
Settings = new TorrentRssIndexerSettings { MultiLanguages = new List<int> { Language.Original.Id, Language.German.Id } }
};
Mocker.GetMock<IIndexerFactory>()
.Setup(v => v.Find(1))
.Returns(null as IndexerDefinition);
Mocker.GetMock<IIndexerFactory>()
.Setup(v => v.FindByName("MyIndexer1"))
.Returns(indexerDefinition1);
Mocker.GetMock<IIndexerFactory>()
.Setup(v => v.All())
.Returns(new List<IndexerDefinition>() { indexerDefinition1, indexerDefinition2 });
_remoteMovie.ParsedMovieInfo = GetParsedMovieInfo(new List<Language> { }, releaseTitle);
_remoteMovie.Release.IndexerId = 10;
_remoteMovie.Release.Indexer = "MyIndexer1";
_remoteMovie.Release.Title = releaseTitle;
Subject.Aggregate(_remoteMovie).Languages.Should().BeEquivalentTo(new List<Language> { _movie.MovieMetadata.Value.OriginalLanguage, Language.French });
Mocker.GetMock<IIndexerFactory>().Verify(c => c.Find(10), Times.Once());
Mocker.GetMock<IIndexerFactory>().Verify(c => c.FindByName("MyIndexer1"), Times.Once());
Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls();
}
[Test]
public void should_return_multi_languages_from_indexer_with_name_when_indexer_id_not_available()
{
var releaseTitle = "Series.Title.S01E01.MULTi.1080p.WEB.H265-RlsGroup";
var indexerDefinition1 = new IndexerDefinition
{
Id = 1,
Name = "MyIndexer1",
Settings = new TorrentRssIndexerSettings { MultiLanguages = new List<int> { Language.Original.Id, Language.French.Id } }
};
var indexerDefinition2 = new IndexerDefinition
{
Id = 2,
Name = "MyIndexer2",
Settings = new TorrentRssIndexerSettings { MultiLanguages = new List<int> { Language.Original.Id, Language.German.Id } }
};
Mocker.GetMock<IIndexerFactory>()
.Setup(v => v.Find(1))
.Returns(null as IndexerDefinition);
Mocker.GetMock<IIndexerFactory>()
.Setup(v => v.FindByName("MyIndexer1"))
.Returns(indexerDefinition1);
Mocker.GetMock<IIndexerFactory>()
.Setup(v => v.All())
.Returns(new List<IndexerDefinition>() { indexerDefinition1, indexerDefinition2 });
_remoteMovie.ParsedMovieInfo = GetParsedMovieInfo(new List<Language> { }, releaseTitle);
_remoteMovie.Release.IndexerId = 0;
_remoteMovie.Release.Indexer = "MyIndexer1";
_remoteMovie.Release.Title = releaseTitle;
Subject.Aggregate(_remoteMovie).Languages.Should().BeEquivalentTo(new List<Language> { _movie.MovieMetadata.Value.OriginalLanguage, Language.French });
Mocker.GetMock<IIndexerFactory>().Verify(c => c.Find(10), Times.Never());
Mocker.GetMock<IIndexerFactory>().Verify(c => c.FindByName("MyIndexer1"), Times.Once());
Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls();
}
} }
} }
@@ -4,7 +4,6 @@ using FluentAssertions;
using Moq; using Moq;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download; using NzbDrone.Core.Download;
using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.History; using NzbDrone.Core.History;
@@ -107,11 +106,11 @@ namespace NzbDrone.Core.Test.Download
{ {
new ImportResult( new ImportResult(
new ImportDecision( new ImportDecision(
new LocalMovie { Path = @"C:\TestPath\Droned.1998.mkv" }, new Rejection("Rejected!")), "Test Failure"), new LocalMovie { Path = @"C:\TestPath\Droned.1998.mkv" }, new ImportRejection(ImportRejectionReason.Unknown, "Rejected!")), "Test Failure"),
new ImportResult( new ImportResult(
new ImportDecision( new ImportDecision(
new LocalMovie { Path = @"C:\TestPath\Droned.1999.mkv" }, new Rejection("Rejected!")), "Test Failure") new LocalMovie { Path = @"C:\TestPath\Droned.1999.mkv" }, new ImportRejection(ImportRejectionReason.Unknown, "Rejected!")), "Test Failure")
}); });
Subject.Import(_trackedDownload); Subject.Import(_trackedDownload);
@@ -131,11 +130,11 @@ namespace NzbDrone.Core.Test.Download
{ {
new ImportResult( new ImportResult(
new ImportDecision( new ImportDecision(
new LocalMovie { Path = @"C:\TestPath\Droned.1998.mkv" }, new Rejection("Rejected!")), "Test Failure"), new LocalMovie { Path = @"C:\TestPath\Droned.1998.mkv" }, new ImportRejection(ImportRejectionReason.Unknown, "Rejected!")), "Test Failure"),
new ImportResult( new ImportResult(
new ImportDecision( new ImportDecision(
new LocalMovie { Path = @"C:\TestPath\Droned.1998.mkv" }, new Rejection("Rejected!")), "Test Failure") new LocalMovie { Path = @"C:\TestPath\Droned.1998.mkv" }, new ImportRejection(ImportRejectionReason.Unknown, "Rejected!")), "Test Failure")
}); });
_trackedDownload.RemoteMovie.Movie = new Movie(); _trackedDownload.RemoteMovie.Movie = new Movie();
@@ -189,8 +189,8 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
{ {
var decisions = new List<DownloadDecision>(); var decisions = new List<DownloadDecision>();
RemoteMovie remoteMovie = null; RemoteMovie remoteMovie = null;
decisions.Add(new DownloadDecision(remoteMovie, new Rejection("Failure!"))); decisions.Add(new DownloadDecision(remoteMovie, new DownloadRejection(DownloadRejectionReason.Unknown, "Failure!")));
decisions.Add(new DownloadDecision(remoteMovie, new Rejection("Failure!"))); decisions.Add(new DownloadDecision(remoteMovie, new DownloadRejection(DownloadRejectionReason.Unknown, "Failure!")));
Subject.GetQualifiedReports(decisions).Should().BeEmpty(); Subject.GetQualifiedReports(decisions).Should().BeEmpty();
} }
@@ -201,7 +201,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
var remoteMovie = GetRemoteMovie(new QualityModel(Quality.HDTV720p)); var remoteMovie = GetRemoteMovie(new QualityModel(Quality.HDTV720p));
var decisions = new List<DownloadDecision>(); var decisions = new List<DownloadDecision>();
decisions.Add(new DownloadDecision(remoteMovie, new Rejection("Failure!", RejectionType.Temporary))); decisions.Add(new DownloadDecision(remoteMovie, new DownloadRejection(DownloadRejectionReason.Unknown, "Failure!", RejectionType.Temporary)));
await Subject.ProcessDecisions(decisions); await Subject.ProcessDecisions(decisions);
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteMovie>(), null), Times.Never()); Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteMovie>(), null), Times.Never());
@@ -214,7 +214,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
var decisions = new List<DownloadDecision>(); var decisions = new List<DownloadDecision>();
decisions.Add(new DownloadDecision(removeMovie)); decisions.Add(new DownloadDecision(removeMovie));
decisions.Add(new DownloadDecision(removeMovie, new Rejection("Failure!", RejectionType.Temporary))); decisions.Add(new DownloadDecision(removeMovie, new DownloadRejection(DownloadRejectionReason.Unknown, "Failure!", RejectionType.Temporary)));
await Subject.ProcessDecisions(decisions); await Subject.ProcessDecisions(decisions);
Mocker.GetMock<IPendingReleaseService>().Verify(v => v.AddMany(It.IsAny<List<Tuple<DownloadDecision, PendingReleaseReason>>>()), Times.Never()); Mocker.GetMock<IPendingReleaseService>().Verify(v => v.AddMany(It.IsAny<List<Tuple<DownloadDecision, PendingReleaseReason>>>()), Times.Never());
@@ -226,8 +226,8 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
var remoteEpisode = GetRemoteMovie(new QualityModel(Quality.HDTV720p)); var remoteEpisode = GetRemoteMovie(new QualityModel(Quality.HDTV720p));
var decisions = new List<DownloadDecision>(); var decisions = new List<DownloadDecision>();
decisions.Add(new DownloadDecision(remoteEpisode, new Rejection("Failure!", RejectionType.Temporary))); decisions.Add(new DownloadDecision(remoteEpisode, new DownloadRejection(DownloadRejectionReason.Unknown, "Failure!", RejectionType.Temporary)));
decisions.Add(new DownloadDecision(remoteEpisode, new Rejection("Failure!", RejectionType.Temporary))); decisions.Add(new DownloadDecision(remoteEpisode, new DownloadRejection(DownloadRejectionReason.Unknown, "Failure!", RejectionType.Temporary)));
await Subject.ProcessDecisions(decisions); await Subject.ProcessDecisions(decisions);
Mocker.GetMock<IPendingReleaseService>().Verify(v => v.AddMany(It.IsAny<List<Tuple<DownloadDecision, PendingReleaseReason>>>()), Times.Once()); Mocker.GetMock<IPendingReleaseService>().Verify(v => v.AddMany(It.IsAny<List<Tuple<DownloadDecision, PendingReleaseReason>>>()), Times.Once());
@@ -95,5 +95,22 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole
VerifySingleItem(DownloadItemStatus.Completed); VerifySingleItem(DownloadItemStatus.Completed);
} }
[TestCase("@eaDir")]
[TestCase(".@__thumb")]
public void GetItems_should_not_include_special_subfolders(string folderName)
{
GivenCompletedItem();
var targetDir = Path.Combine(_completedDownloadFolder, folderName);
Mocker.GetMock<IDiskProvider>()
.Setup(c => c.GetDirectories(_completedDownloadFolder))
.Returns(new[] { targetDir });
var items = Subject.GetItems(_completedDownloadFolder, TimeSpan.FromMilliseconds(50)).ToList();
items.Count.Should().Be(0);
}
} }
} }
@@ -711,6 +711,30 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
item.CanMoveFiles.Should().BeTrue(); item.CanMoveFiles.Should().BeTrue();
} }
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_max_ratio_reached_after_rounding_and_paused(string state)
{
GivenGlobalSeedLimits(1.0f);
GivenCompletedTorrent(state, ratio: 1.1006066990976857f);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_just_under_max_ratio_reached_after_rounding_and_paused(string state)
{
GivenGlobalSeedLimits(1.0f);
GivenCompletedTorrent(state, ratio: 0.9999f);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[TestCase("pausedUP")] [TestCase("pausedUP")]
[TestCase("stoppedUP")] [TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_overridden_max_ratio_reached_and_paused(string state) public void should_be_removable_and_should_allow_move_files_if_overridden_max_ratio_reached_and_paused(string state)
@@ -723,6 +747,30 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
item.CanMoveFiles.Should().BeTrue(); item.CanMoveFiles.Should().BeTrue();
} }
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_overridden_max_ratio_reached_after_rounding_and_paused(string state)
{
GivenGlobalSeedLimits(2.0f);
GivenCompletedTorrent(state, ratio: 1.1006066990976857f, ratioLimit: 1.1f);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_just_under_overridden_max_ratio_reached_after_rounding_and_paused(string state)
{
GivenGlobalSeedLimits(2.0f);
GivenCompletedTorrent(state, ratio: 0.9999f, ratioLimit: 1.0f);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[TestCase("pausedUP")] [TestCase("pausedUP")]
[TestCase("stoppedUP")] [TestCase("stoppedUP")]
public void should_not_be_removable_if_overridden_max_ratio_not_reached_and_paused(string state) public void should_not_be_removable_if_overridden_max_ratio_not_reached_and_paused(string state)
@@ -56,7 +56,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
_remoteMovie.ParsedMovieInfo = _parsedMovieInfo; _remoteMovie.ParsedMovieInfo = _parsedMovieInfo;
_remoteMovie.Release = _release; _remoteMovie.Release = _release;
_temporarilyRejected = new DownloadDecision(_remoteMovie, new Rejection("Temp Rejected", RejectionType.Temporary)); _temporarilyRejected = new DownloadDecision(_remoteMovie, new DownloadRejection(DownloadRejectionReason.MinimumAgeDelay, "Temp Rejected", RejectionType.Temporary));
_heldReleases = new List<PendingRelease>(); _heldReleases = new List<PendingRelease>();
@@ -56,7 +56,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
_remoteMovie.ParsedMovieInfo = _parsedMovieInfo; _remoteMovie.ParsedMovieInfo = _parsedMovieInfo;
_remoteMovie.Release = _release; _remoteMovie.Release = _release;
_temporarilyRejected = new DownloadDecision(_remoteMovie, new Rejection("Temp Rejected", RejectionType.Temporary)); _temporarilyRejected = new DownloadDecision(_remoteMovie, new DownloadRejection(DownloadRejectionReason.MinimumAgeDelay, "Temp Rejected", RejectionType.Temporary));
_heldReleases = new List<PendingRelease>(); _heldReleases = new List<PendingRelease>();
@@ -59,7 +59,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
_remoteMovie.ParsedMovieInfo = _parsedMovieInfo; _remoteMovie.ParsedMovieInfo = _parsedMovieInfo;
_remoteMovie.Release = _release; _remoteMovie.Release = _release;
_temporarilyRejected = new DownloadDecision(_remoteMovie, new Rejection("Temp Rejected", RejectionType.Temporary)); _temporarilyRejected = new DownloadDecision(_remoteMovie, new DownloadRejection(DownloadRejectionReason.MinimumAgeDelay, "Temp Rejected", RejectionType.Temporary));
Mocker.GetMock<IPendingReleaseRepository>() Mocker.GetMock<IPendingReleaseRepository>()
.Setup(s => s.All()) .Setup(s => s.All())
@@ -76,6 +76,19 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
Subject.Check().ShouldBeWarning(wikiFragment: "downloads-in-root-folder"); Subject.Check().ShouldBeWarning(wikiFragment: "downloads-in-root-folder");
} }
[Test]
public void should_return_warning_if_downloading_inside_root_folder()
{
var rootFolderPath = "c:\\Test".AsOsAgnostic();
var downloadRootPath = "c:\\Test\\Downloads".AsOsAgnostic();
GivenRootFolder(rootFolderPath);
_clientStatus.OutputRootFolders = new List<OsPath> { new (downloadRootPath) };
Subject.Check().ShouldBeWarning();
}
[Test] [Test]
public void should_return_ok_if_not_downloading_to_root_folder() public void should_return_ok_if_not_downloading_to_root_folder()
{ {
@@ -87,7 +100,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
} }
[Test] [Test]
[TestCaseSource("DownloadClientExceptions")] [TestCaseSource(nameof(DownloadClientExceptions))]
public void should_return_ok_if_client_throws_downloadclientexception(Exception ex) public void should_return_ok_if_client_throws_downloadclientexception(Exception ex)
{ {
_downloadClient.Setup(s => s.GetStatus()) _downloadClient.Setup(s => s.GetStatus())
@@ -63,7 +63,9 @@ namespace NzbDrone.Core.Test.Languages
new object[] { 48, Language.Malayalam }, new object[] { 48, Language.Malayalam },
new object[] { 49, Language.Kannada }, new object[] { 49, Language.Kannada },
new object[] { 50, Language.Albanian }, new object[] { 50, Language.Albanian },
new object[] { 51, Language.Afrikaans } new object[] { 51, Language.Afrikaans },
new object[] { 52, Language.Marathi },
new object[] { 53, Language.Tagalog },
}; };
public static object[] ToIntCases = public static object[] ToIntCases =
@@ -121,7 +123,9 @@ namespace NzbDrone.Core.Test.Languages
new object[] { Language.Malayalam, 48 }, new object[] { Language.Malayalam, 48 },
new object[] { Language.Kannada, 49 }, new object[] { Language.Kannada, 49 },
new object[] { Language.Albanian, 50 }, new object[] { Language.Albanian, 50 },
new object[] { Language.Afrikaans, 51 } new object[] { Language.Afrikaans, 51 },
new object[] { Language.Marathi, 52 },
new object[] { Language.Tagalog, 53 },
}; };
[Test] [Test]
@@ -7,7 +7,6 @@ using Moq;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download; using NzbDrone.Core.Download;
using NzbDrone.Core.History; using NzbDrone.Core.History;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
@@ -46,9 +45,9 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport
.With(s => s.Path = @"C:\Test\TV\30 Rock".AsOsAgnostic()) .With(s => s.Path = @"C:\Test\TV\30 Rock".AsOsAgnostic())
.Build(); .Build();
_rejectedDecisions.Add(new ImportDecision(new LocalMovie(), new Rejection("Rejected!"))); _rejectedDecisions.Add(new ImportDecision(new LocalMovie(), new ImportRejection(ImportRejectionReason.Unknown, "Rejected!")));
_rejectedDecisions.Add(new ImportDecision(new LocalMovie(), new Rejection("Rejected!"))); _rejectedDecisions.Add(new ImportDecision(new LocalMovie(), new ImportRejection(ImportRejectionReason.Unknown, "Rejected!")));
_rejectedDecisions.Add(new ImportDecision(new LocalMovie(), new Rejection("Rejected!"))); _rejectedDecisions.Add(new ImportDecision(new LocalMovie(), new ImportRejection(ImportRejectionReason.Unknown, "Rejected!")));
_approvedDecisions.Add(new ImportDecision( _approvedDecisions.Add(new ImportDecision(
new LocalMovie new LocalMovie
@@ -4,7 +4,6 @@ using FizzWare.NBuilder;
using FluentAssertions; using FluentAssertions;
using Moq; using Moq;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download; using NzbDrone.Core.Download;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.MovieImport; using NzbDrone.Core.MediaFiles.MovieImport;
@@ -49,13 +48,13 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport
_fail2 = new Mock<IImportDecisionEngineSpecification>(); _fail2 = new Mock<IImportDecisionEngineSpecification>();
_fail3 = new Mock<IImportDecisionEngineSpecification>(); _fail3 = new Mock<IImportDecisionEngineSpecification>();
_pass1.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalMovie>(), It.IsAny<DownloadClientItem>())).Returns(Decision.Accept()); _pass1.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalMovie>(), It.IsAny<DownloadClientItem>())).Returns(ImportSpecDecision.Accept());
_pass2.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalMovie>(), It.IsAny<DownloadClientItem>())).Returns(Decision.Accept()); _pass2.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalMovie>(), It.IsAny<DownloadClientItem>())).Returns(ImportSpecDecision.Accept());
_pass3.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalMovie>(), It.IsAny<DownloadClientItem>())).Returns(Decision.Accept()); _pass3.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalMovie>(), It.IsAny<DownloadClientItem>())).Returns(ImportSpecDecision.Accept());
_fail1.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalMovie>(), It.IsAny<DownloadClientItem>())).Returns(Decision.Reject("_fail1")); _fail1.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalMovie>(), It.IsAny<DownloadClientItem>())).Returns(ImportSpecDecision.Reject(ImportRejectionReason.Unknown, "_fail1"));
_fail2.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalMovie>(), It.IsAny<DownloadClientItem>())).Returns(Decision.Reject("_fail2")); _fail2.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalMovie>(), It.IsAny<DownloadClientItem>())).Returns(ImportSpecDecision.Reject(ImportRejectionReason.Unknown, "_fail2"));
_fail3.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalMovie>(), It.IsAny<DownloadClientItem>())).Returns(Decision.Reject("_fail3")); _fail3.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalMovie>(), It.IsAny<DownloadClientItem>())).Returns(ImportSpecDecision.Reject(ImportRejectionReason.Unknown, "_fail3"));
_movie = Builder<Movie>.CreateNew() _movie = Builder<Movie>.CreateNew()
.With(e => e.Path = @"C:\Test\Movie".AsOsAgnostic()) .With(e => e.Path = @"C:\Test\Movie".AsOsAgnostic())
@@ -54,6 +54,17 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport.Specifications
@"C:\Test\Downloaded\Bad Boys (2006) part1.mkv", @"C:\Test\Downloaded\Bad Boys (2006) part1.mkv",
@"C:\Test\Downloaded\Bad Boys (2006) part2.mkv" @"C:\Test\Downloaded\Bad Boys (2006) part2.mkv"
})] })]
[TestCase(new object[]
{
@"C:\Test\Downloaded\Bad Boys (2006) pt1.mkv",
@"C:\Test\Downloaded\Bad Boys (2006) pt2.mkv"
})]
[TestCase(new object[]
{
@"C:\Test\Downloaded\Bad Boys (2006) P1.mkv",
@"C:\Test\Downloaded\Bad Boys (2006) P2.mkv"
})]
[TestCase(new object[] [TestCase(new object[]
{ {
@"C:\Test\Downloaded\blah blah - cd 1.mvk", @"C:\Test\Downloaded\blah blah - cd 1.mvk",
@@ -62,5 +62,23 @@ namespace NzbDrone.Core.Test.ParserTests
var result = IsoLanguages.Find(isoCode); var result = IsoLanguages.Find(isoCode);
result.Language.Should().Be(Language.Afrikaans); result.Language.Should().Be(Language.Afrikaans);
} }
[TestCase("mr")]
[TestCase("mar")]
[TestCase("mr-IN")]
public void should_return_marathi(string isoCode)
{
var result = IsoLanguages.Find(isoCode);
result.Language.Should().Be(Language.Marathi);
}
[TestCase("tl")]
[TestCase("tgl")]
[TestCase("tl-PH")]
public void should_return_tagalog(string isoCode)
{
var result = IsoLanguages.Find(isoCode);
result.Language.Should().Be(Language.Tagalog);
}
} }
} }
@@ -52,6 +52,8 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Movie Title : Other Title 2010 x264.720p.Blu-ray Rip HD.VOSTFR.VFF. ONLY")] [TestCase("Movie Title : Other Title 2010 x264.720p.Blu-ray Rip HD.VOSTFR.VFF. ONLY")]
[TestCase("Movie Title 2019 HEVC.2160p.Blu-ray 4K.VOSTFR.VFF. JATO")] [TestCase("Movie Title 2019 HEVC.2160p.Blu-ray 4K.VOSTFR.VFF. JATO")]
[TestCase("Movie.Title.1956.MULTi.VF.Bluray.1080p.REMUX.AC3.x264")] [TestCase("Movie.Title.1956.MULTi.VF.Bluray.1080p.REMUX.AC3.x264")]
[TestCase("Movie.Title.2016.ENG-ITA-FRE.AAC.1080p.WebDL.x264")]
[TestCase("Movie Title 2016 (BDrip 1080p ENG-ITA-FRE) Multisub x264")]
public void should_parse_language_french(string postTitle) public void should_parse_language_french(string postTitle)
{ {
var result = Parser.Parser.ParseMovieTitle(postTitle, true); var result = Parser.Parser.ParseMovieTitle(postTitle, true);
@@ -87,7 +89,13 @@ namespace NzbDrone.Core.Test.ParserTests
} }
[TestCase("Movie.Title.1994.German.1080p.XviD-LOL")] [TestCase("Movie.Title.1994.German.1080p.XviD-LOL")]
[TestCase("Movie.Title.2016.GERMAN.DUBBED.WS.WEBRiP.XviD.REPACK-TVP")]
[TestCase("Movie Title 2016 - Kampfhaehne - mkv - by Videomann")]
[TestCase("Movie.Title.2016.Ger.Dub.AAC.1080p.WebDL.x264-TKP21")] [TestCase("Movie.Title.2016.Ger.Dub.AAC.1080p.WebDL.x264-TKP21")]
[TestCase("Movie.Title.2016.Ger.AAC.1080p.WebDL.x264-TKP21")]
[TestCase("Movie.Title.2016.Hun/Ger/Ita.AAC.1080p.WebDL.x264-TKP21")]
[TestCase("Movie.Title.2016.1080p.10Bit.HEVC.WEBRip.HIN-ENG-GER.DD5.1.H.265")]
[TestCase("Movie.Title.2016.HU-IT-DE.AAC.1080p.WebDL.x264")]
public void should_parse_language_german(string postTitle) public void should_parse_language_german(string postTitle)
{ {
var result = Parser.Parser.ParseMovieTitle(postTitle, true); var result = Parser.Parser.ParseMovieTitle(postTitle, true);
@@ -96,6 +104,8 @@ namespace NzbDrone.Core.Test.ParserTests
} }
[TestCase("Movie.Title.1994.Italian.1080p.XviD-LOL")] [TestCase("Movie.Title.1994.Italian.1080p.XviD-LOL")]
[TestCase("Movie.Title.2016.ENG-FRE-ITA.AAC.1080p.WebDL.x264")]
[TestCase("Movie Title 2016 (BDrip 1080p ENG-FRE-ITA) Multisub x264")]
public void should_parse_language_italian(string postTitle) public void should_parse_language_italian(string postTitle)
{ {
var result = Parser.Parser.ParseMovieTitle(postTitle, true); var result = Parser.Parser.ParseMovieTitle(postTitle, true);
@@ -120,6 +130,8 @@ namespace NzbDrone.Core.Test.ParserTests
} }
[TestCase("Movie.Title.1994.Japanese.1080p.XviD-LOL")] [TestCase("Movie.Title.1994.Japanese.1080p.XviD-LOL")]
[TestCase("Movie.Title (1988) 2160p HDR 5.1 Eng - Jpn x265 10bit")]
[TestCase("Movie Title (1985) (1080p.AC3 ITA-ENG-JPN)")]
public void should_parse_language_japanese(string postTitle) public void should_parse_language_japanese(string postTitle)
{ {
var result = Parser.Parser.ParseMovieTitle(postTitle, true); var result = Parser.Parser.ParseMovieTitle(postTitle, true);
@@ -291,6 +303,8 @@ namespace NzbDrone.Core.Test.ParserTests
} }
[TestCase("Movie.Title.1994.Korean.1080p.XviD-LOL")] [TestCase("Movie.Title.1994.Korean.1080p.XviD-LOL")]
[TestCase("Movie Title [2006] BDRip 720p [Kor Rus] GROUP")]
[TestCase("Movie.Title.2019.KOR.1080p.HDRip.H264.AAC-GROUP")]
public void should_parse_language_korean(string postTitle) public void should_parse_language_korean(string postTitle)
{ {
var result = Parser.Parser.ParseMovieTitle(postTitle, true); var result = Parser.Parser.ParseMovieTitle(postTitle, true);
@@ -460,6 +474,22 @@ namespace NzbDrone.Core.Test.ParserTests
result.Should().Contain(Language.Afrikaans); result.Should().Contain(Language.Afrikaans);
} }
[TestCase("Movie Title 2015 Marathi 1080p WebRip x264 AC3 5.1 ESubs [TMB]")]
[TestCase("Movie.Title.(2018).720p.CensorRip.Marathi.x264.AAC.-.LHDm@Telly")]
public void should_parse_language_marathi(string postTitle)
{
var result = LanguageParser.ParseLanguages(postTitle);
result.Should().Contain(Language.Marathi);
}
[TestCase("Movie Title 2024 1080p Tagalog WEB-DL HEVC x265 BONE")]
[TestCase("Movie.Title.2022.720p.Tagalog.WEB-DL.AAC.x264-Mkvking")]
public void should_parse_language_tagalog(string postTitle)
{
var result = LanguageParser.ParseLanguages(postTitle);
result.Should().Contain(Language.Tagalog);
}
[TestCase("Movie.Title.en.sub")] [TestCase("Movie.Title.en.sub")]
[TestCase("Movie Title.eng.sub")] [TestCase("Movie Title.eng.sub")]
[TestCase("Movie.Title.eng.forced.sub")] [TestCase("Movie.Title.eng.forced.sub")]
@@ -23,7 +23,7 @@ namespace NzbDrone.Core.AutoTagging.Specifications
public override int Order => 1; public override int Order => 1;
public override string ImplementationName => "Genre"; public override string ImplementationName => "Genre";
[FieldDefinition(1, Label = "Genre(s)", Type = FieldType.Tag)] [FieldDefinition(1, Label = "AutoTaggingSpecificationGenre", Type = FieldType.Tag)]
public IEnumerable<string> Value { get; set; } public IEnumerable<string> Value { get; set; }
protected override bool IsSatisfiedByWithoutNegate(Movie movie) protected override bool IsSatisfiedByWithoutNegate(Movie movie)
@@ -21,7 +21,7 @@ namespace NzbDrone.Core.AutoTagging.Specifications
public override int Order => 1; public override int Order => 1;
public override string ImplementationName => "Original Language"; public override string ImplementationName => "Original Language";
[FieldDefinition(1, Label = "Language", Type = FieldType.Select, SelectOptions = typeof(OriginalLanguageFieldConverter))] [FieldDefinition(1, Label = "AutoTaggingSpecificationOriginalLanguage", Type = FieldType.Select, SelectOptions = typeof(OriginalLanguageFieldConverter))]
public int Value { get; set; } public int Value { get; set; }
protected override bool IsSatisfiedByWithoutNegate(Movie movie) protected override bool IsSatisfiedByWithoutNegate(Movie movie)
@@ -20,7 +20,7 @@ namespace NzbDrone.Core.AutoTagging.Specifications
public override int Order => 1; public override int Order => 1;
public override string ImplementationName => "Quality Profile"; public override string ImplementationName => "Quality Profile";
[FieldDefinition(1, Label = "Quality Profile", Type = FieldType.QualityProfile)] [FieldDefinition(1, Label = "AutoTaggingSpecificationQualityProfile", Type = FieldType.QualityProfile)]
public int Value { get; set; } public int Value { get; set; }
protected override bool IsSatisfiedByWithoutNegate(Movie movie) protected override bool IsSatisfiedByWithoutNegate(Movie movie)
@@ -22,7 +22,7 @@ namespace NzbDrone.Core.AutoTagging.Specifications
public override int Order => 1; public override int Order => 1;
public override string ImplementationName => "Root Folder"; public override string ImplementationName => "Root Folder";
[FieldDefinition(1, Label = "Root Folder", Type = FieldType.RootFolder)] [FieldDefinition(1, Label = "AutoTaggingSpecificationRootFolder", Type = FieldType.RootFolder)]
public string Value { get; set; } public string Value { get; set; }
protected override bool IsSatisfiedByWithoutNegate(Movie movie) protected override bool IsSatisfiedByWithoutNegate(Movie movie)
@@ -24,10 +24,10 @@ namespace NzbDrone.Core.AutoTagging.Specifications
public override int Order => 1; public override int Order => 1;
public override string ImplementationName => "Runtime"; public override string ImplementationName => "Runtime";
[FieldDefinition(1, Label = "Minimum Runtime", Type = FieldType.Number)] [FieldDefinition(1, Label = "AutoTaggingSpecificationMinimumRuntime", Type = FieldType.Number, Unit = "minutes")]
public int Min { get; set; } public int Min { get; set; }
[FieldDefinition(2, Label = "Maximum Runtime", Type = FieldType.Number)] [FieldDefinition(2, Label = "AutoTaggingSpecificationMaximumRuntime", Type = FieldType.Number, Unit = "minutes")]
public int Max { get; set; } public int Max { get; set; }
protected override bool IsSatisfiedByWithoutNegate(Movie movie) protected override bool IsSatisfiedByWithoutNegate(Movie movie)
@@ -0,0 +1,43 @@
using System;
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Movies;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.AutoTagging.Specifications
{
public class StatusSpecificationValidator : AbstractValidator<StatusSpecification>
{
public StatusSpecificationValidator()
{
RuleFor(c => c.Status).Custom((statusType, context) =>
{
if (!Enum.IsDefined(typeof(MovieStatusType), statusType))
{
context.AddFailure($"Invalid status type condition value: {statusType}");
}
});
}
}
public class StatusSpecification : AutoTaggingSpecificationBase
{
private static readonly StatusSpecificationValidator Validator = new ();
public override int Order => 1;
public override string ImplementationName => "Status";
[FieldDefinition(1, Label = "AutoTaggingSpecificationStatus", Type = FieldType.Select, SelectOptions = typeof(MovieStatusType))]
public int Status { get; set; }
protected override bool IsSatisfiedByWithoutNegate(Movie movie)
{
return movie?.MovieMetadata?.Value?.Status == (MovieStatusType)Status;
}
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}
@@ -23,10 +23,10 @@ namespace NzbDrone.Core.AutoTagging.Specifications
public override int Order => 1; public override int Order => 1;
public override string ImplementationName => "Year"; public override string ImplementationName => "Year";
[FieldDefinition(1, Label = "Minimum Year", Type = FieldType.Number)] [FieldDefinition(1, Label = "AutoTaggingSpecificationMinimumYear", Type = FieldType.Number)]
public int Min { get; set; } public int Min { get; set; }
[FieldDefinition(2, Label = "Maximum Year", Type = FieldType.Number)] [FieldDefinition(2, Label = "AutoTaggingSpecificationMaximumYear", Type = FieldType.Number)]
public int Max { get; set; } public int Max { get; set; }
protected override bool IsSatisfiedByWithoutNegate(Movie movie) protected override bool IsSatisfiedByWithoutNegate(Movie movie)
+9 -2
View File
@@ -66,12 +66,19 @@ namespace NzbDrone.Core.Backup
{ {
_logger.ProgressInfo("Starting Backup"); _logger.ProgressInfo("Starting Backup");
var backupFolder = GetBackupFolder(backupType);
_diskProvider.EnsureFolder(_backupTempFolder); _diskProvider.EnsureFolder(_backupTempFolder);
_diskProvider.EnsureFolder(GetBackupFolder(backupType)); _diskProvider.EnsureFolder(backupFolder);
if (!_diskProvider.FolderWritable(backupFolder))
{
throw new UnauthorizedAccessException($"Backup folder {backupFolder} is not writable");
}
var dateNow = DateTime.Now; var dateNow = DateTime.Now;
var backupFilename = $"radarr_backup_v{BuildInfo.Version}_{dateNow:yyyy.MM.dd_HH.mm.ss}.zip"; var backupFilename = $"radarr_backup_v{BuildInfo.Version}_{dateNow:yyyy.MM.dd_HH.mm.ss}.zip";
var backupPath = Path.Combine(GetBackupFolder(backupType), backupFilename); var backupPath = Path.Combine(backupFolder, backupFilename);
Cleanup(); Cleanup();
@@ -27,7 +27,7 @@ namespace NzbDrone.Core.CustomFormats
public override int Order => 3; public override int Order => 3;
public override string ImplementationName => "Language"; public override string ImplementationName => "Language";
[FieldDefinition(1, Label = "Language", Type = FieldType.Select, SelectOptions = typeof(LanguageFieldConverter))] [FieldDefinition(1, Label = "CustomFormatsSpecificationLanguage", Type = FieldType.Select, SelectOptions = typeof(LanguageFieldConverter))]
public int Value { get; set; } public int Value { get; set; }
[FieldDefinition(1, Label = "CustomFormatsSpecificationExceptLanguage", HelpText = "CustomFormatsSpecificationExceptLanguageHelpText", Type = FieldType.Checkbox)] [FieldDefinition(1, Label = "CustomFormatsSpecificationExceptLanguage", HelpText = "CustomFormatsSpecificationExceptLanguageHelpText", Type = FieldType.Checkbox)]
@@ -28,7 +28,7 @@ namespace NzbDrone.Core.CustomFormats
public override int Order => 7; public override int Order => 7;
public override string ImplementationName => "Quality Modifier"; public override string ImplementationName => "Quality Modifier";
[FieldDefinition(1, Label = "Quality Modifier", Type = FieldType.Select, SelectOptions = typeof(Modifier))] [FieldDefinition(1, Label = "CustomFormatsSpecificationQualityModifier", Type = FieldType.Select, SelectOptions = typeof(Modifier))]
public int Value { get; set; } public int Value { get; set; }
protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input) protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input)
@@ -20,7 +20,7 @@ namespace NzbDrone.Core.CustomFormats
public override int Order => 6; public override int Order => 6;
public override string ImplementationName => "Resolution"; public override string ImplementationName => "Resolution";
[FieldDefinition(1, Label = "Resolution", Type = FieldType.Select, SelectOptions = typeof(Resolution))] [FieldDefinition(1, Label = "CustomFormatsSpecificationResolution", Type = FieldType.Select, SelectOptions = typeof(Resolution))]
public int Value { get; set; } public int Value { get; set; }
protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input) protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input)
@@ -21,10 +21,10 @@ namespace NzbDrone.Core.CustomFormats
public override int Order => 8; public override int Order => 8;
public override string ImplementationName => "Size"; public override string ImplementationName => "Size";
[FieldDefinition(1, Label = "Minimum Size", HelpText = "Release must be greater than this size", Unit = "GB", Type = FieldType.Number)] [FieldDefinition(1, Label = "CustomFormatsSpecificationMinimumSize", HelpText = "CustomFormatsSpecificationMinimumSizeHelpText", Unit = "GB", Type = FieldType.Number)]
public double Min { get; set; } public double Min { get; set; }
[FieldDefinition(1, Label = "Maximum Size", HelpText = "Release must be less than or equal to this size", Unit = "GB", Type = FieldType.Number)] [FieldDefinition(1, Label = "CustomFormatsSpecificationMaximumSize", HelpText = "CustomFormatsSpecificationMaximumSizeHelpText", Unit = "GB", Type = FieldType.Number)]
public double Max { get; set; } public double Max { get; set; }
protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input) protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input)
@@ -20,7 +20,7 @@ namespace NzbDrone.Core.CustomFormats
public override int Order => 5; public override int Order => 5;
public override string ImplementationName => "Source"; public override string ImplementationName => "Source";
[FieldDefinition(1, Label = "Source", Type = FieldType.Select, SelectOptions = typeof(QualitySource))] [FieldDefinition(1, Label = "CustomFormatsSpecificationSource", Type = FieldType.Select, SelectOptions = typeof(QualitySource))]
public int Value { get; set; } public int Value { get; set; }
protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input) protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input)
@@ -20,10 +20,10 @@ namespace NzbDrone.Core.CustomFormats
public override int Order => 10; public override int Order => 10;
public override string ImplementationName => "Year"; public override string ImplementationName => "Year";
[FieldDefinition(1, Label = "Minimum Year", Type = FieldType.Number)] [FieldDefinition(1, Label = "CustomFormatsSpecificationMinimumYear", Type = FieldType.Number)]
public int Min { get; set; } public int Min { get; set; }
[FieldDefinition(2, Label = "Maximum Year", Type = FieldType.Number)] [FieldDefinition(2, Label = "CustomFormatsSpecificationMaximumYear", Type = FieldType.Number)]
public int Max { get; set; } public int Max { get; set; }
protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input) protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input)
@@ -0,0 +1,55 @@
using System.Collections.Generic;
using System.Data;
using Dapper;
using FluentMigrator;
using Newtonsoft.Json.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(241)]
public class stevenlu_update_url : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Execute.WithConnection(FixStevenLuListsLink);
}
private void FixStevenLuListsLink(IDbConnection conn, IDbTransaction tran)
{
var updated = new List<object>();
using (var getStevenLuListCmd = conn.CreateCommand())
{
getStevenLuListCmd.Transaction = tran;
getStevenLuListCmd.CommandText = "SELECT \"Id\", \"Settings\" FROM \"ImportLists\" WHERE \"ConfigContract\" = 'StevenLuSettings'";
using var reader = getStevenLuListCmd.ExecuteReader();
while (reader.Read())
{
var id = reader.GetInt32(0);
var settings = Json.Deserialize<JObject>(reader.GetString(1));
var link = settings.Value<string>("link");
if (link.IsNotNullOrWhiteSpace() && link.StartsWith("https://s3.amazonaws.com/popular-movies"))
{
settings["link"] = "https://popular-movies-data.stevenlu.com/movies.json";
}
updated.Add(new
{
Id = id,
Settings = settings.ToJson()
});
}
}
var updateSql = "UPDATE \"ImportLists\" SET \"Settings\" = @Settings WHERE \"Id\" = @Id";
conn.Execute(updateSql, updated, transaction: tran);
}
}
}
@@ -1,32 +0,0 @@
namespace NzbDrone.Core.DecisionEngine
{
public class Decision
{
public bool Accepted { get; private set; }
public string Reason { get; private set; }
private static readonly Decision AcceptDecision = new Decision { Accepted = true };
private Decision()
{
}
public static Decision Accept()
{
return AcceptDecision;
}
public static Decision Reject(string reason, params object[] args)
{
return Reject(string.Format(reason, args));
}
public static Decision Reject(string reason)
{
return new Decision
{
Accepted = false,
Reason = reason
};
}
}
}
@@ -8,7 +8,7 @@ namespace NzbDrone.Core.DecisionEngine
{ {
public RemoteMovie RemoteMovie { get; private set; } public RemoteMovie RemoteMovie { get; private set; }
public IEnumerable<Rejection> Rejections { get; private set; } public IEnumerable<DownloadRejection> Rejections { get; private set; }
public bool Approved => !Rejections.Any(); public bool Approved => !Rejections.Any();
@@ -28,7 +28,7 @@ namespace NzbDrone.Core.DecisionEngine
} }
} }
public DownloadDecision(RemoteMovie movie, params Rejection[] rejections) public DownloadDecision(RemoteMovie movie, params DownloadRejection[] rejections)
{ {
RemoteMovie = movie; RemoteMovie = movie;
Rejections = rejections.ToList(); Rejections = rejections.ToList();
@@ -159,7 +159,7 @@ namespace NzbDrone.Core.DecisionEngine
return 10; return 10;
} }
return 1; return Math.Round(Math.Log10(age)) * -1;
}); });
} }
@@ -23,14 +23,14 @@ namespace NzbDrone.Core.DecisionEngine
public class DownloadDecisionMaker : IMakeDownloadDecision public class DownloadDecisionMaker : IMakeDownloadDecision
{ {
private readonly IEnumerable<IDecisionEngineSpecification> _specifications; private readonly IEnumerable<IDownloadDecisionEngineSpecification> _specifications;
private readonly IParsingService _parsingService; private readonly IParsingService _parsingService;
private readonly IConfigService _configService; private readonly IConfigService _configService;
private readonly ICustomFormatCalculationService _formatCalculator; private readonly ICustomFormatCalculationService _formatCalculator;
private readonly IRemoteMovieAggregationService _aggregationService; private readonly IRemoteMovieAggregationService _aggregationService;
private readonly Logger _logger; private readonly Logger _logger;
public DownloadDecisionMaker(IEnumerable<IDecisionEngineSpecification> specifications, public DownloadDecisionMaker(IEnumerable<IDownloadDecisionEngineSpecification> specifications,
IParsingService parsingService, IParsingService parsingService,
IConfigService configService, IConfigService configService,
ICustomFormatCalculationService formatCalculator, ICustomFormatCalculationService formatCalculator,
@@ -85,9 +85,7 @@ namespace NzbDrone.Core.DecisionEngine
if (remoteMovie.Movie == null) if (remoteMovie.Movie == null)
{ {
var reason = "Unknown Movie"; decision = new DownloadDecision(remoteMovie, new DownloadRejection(DownloadRejectionReason.UnknownMovie, "Unknown Movie. Unable to identify correct movie using release name."));
decision = new DownloadDecision(remoteMovie, new Rejection(reason));
} }
else else
{ {
@@ -96,6 +94,8 @@ namespace NzbDrone.Core.DecisionEngine
remoteMovie.CustomFormats = _formatCalculator.ParseCustomFormat(remoteMovie, remoteMovie.Release.Size); remoteMovie.CustomFormats = _formatCalculator.ParseCustomFormat(remoteMovie, remoteMovie.Release.Size);
remoteMovie.CustomFormatScore = remoteMovie?.Movie?.QualityProfile?.CalculateCustomFormatScore(remoteMovie.CustomFormats) ?? 0; remoteMovie.CustomFormatScore = remoteMovie?.Movie?.QualityProfile?.CalculateCustomFormatScore(remoteMovie.CustomFormats) ?? 0;
_logger.Trace("Custom Format Score of '{0}' [{1}] calculated for '{2}'", remoteMovie.CustomFormatScore, remoteMovie.CustomFormats?.ConcatToString(), report.Title);
remoteMovie.DownloadAllowed = remoteMovie.Movie != null; remoteMovie.DownloadAllowed = remoteMovie.Movie != null;
decision = GetDecisionForReport(remoteMovie, searchCriteria); decision = GetDecisionForReport(remoteMovie, searchCriteria);
} }
@@ -121,7 +121,7 @@ namespace NzbDrone.Core.DecisionEngine
Languages = parsedMovieInfo.Languages Languages = parsedMovieInfo.Languages
}; };
decision = new DownloadDecision(remoteMovie, new Rejection("Unable to parse release")); decision = new DownloadDecision(remoteMovie, new DownloadRejection(DownloadRejectionReason.UnableToParse, "Unable to parse release"));
} }
} }
} }
@@ -130,7 +130,7 @@ namespace NzbDrone.Core.DecisionEngine
_logger.Error(e, "Couldn't process release."); _logger.Error(e, "Couldn't process release.");
var remoteMovie = new RemoteMovie { Release = report }; var remoteMovie = new RemoteMovie { Release = report };
decision = new DownloadDecision(remoteMovie, new Rejection("Unexpected error processing release")); decision = new DownloadDecision(remoteMovie, new DownloadRejection(DownloadRejectionReason.Error, "Unexpected error processing release"));
} }
reportNumber++; reportNumber++;
@@ -173,7 +173,7 @@ namespace NzbDrone.Core.DecisionEngine
private DownloadDecision GetDecisionForReport(RemoteMovie remoteMovie, SearchCriteriaBase searchCriteria = null) private DownloadDecision GetDecisionForReport(RemoteMovie remoteMovie, SearchCriteriaBase searchCriteria = null)
{ {
var reasons = Array.Empty<Rejection>(); var reasons = Array.Empty<DownloadRejection>();
foreach (var specifications in _specifications.GroupBy(v => v.Priority).OrderBy(v => v.Key)) foreach (var specifications in _specifications.GroupBy(v => v.Priority).OrderBy(v => v.Key))
{ {
@@ -190,7 +190,7 @@ namespace NzbDrone.Core.DecisionEngine
return new DownloadDecision(remoteMovie, reasons.ToArray()); return new DownloadDecision(remoteMovie, reasons.ToArray());
} }
private Rejection EvaluateSpec(IDecisionEngineSpecification spec, RemoteMovie remoteMovie, SearchCriteriaBase searchCriteriaBase = null) private DownloadRejection EvaluateSpec(IDownloadDecisionEngineSpecification spec, RemoteMovie remoteMovie, SearchCriteriaBase searchCriteriaBase = null)
{ {
try try
{ {
@@ -198,7 +198,7 @@ namespace NzbDrone.Core.DecisionEngine
if (!result.Accepted) if (!result.Accepted)
{ {
return new Rejection(result.Reason, spec.Type); return new DownloadRejection(result.Reason, result.Message, spec.Type);
} }
} }
catch (NotImplementedException) catch (NotImplementedException)
@@ -210,7 +210,7 @@ namespace NzbDrone.Core.DecisionEngine
e.Data.Add("report", remoteMovie.Release.ToJson()); e.Data.Add("report", remoteMovie.Release.ToJson());
e.Data.Add("parsed", remoteMovie.ParsedMovieInfo.ToJson()); e.Data.Add("parsed", remoteMovie.ParsedMovieInfo.ToJson());
_logger.Error(e, "Couldn't evaluate decision on {0}, with spec: {1}", remoteMovie.Release.Title, spec.GetType().Name); _logger.Error(e, "Couldn't evaluate decision on {0}, with spec: {1}", remoteMovie.Release.Title, spec.GetType().Name);
return new Rejection($"{spec.GetType().Name}: {e.Message}"); return new DownloadRejection(DownloadRejectionReason.DecisionError, $"{spec.GetType().Name}: {e.Message}");
} }
return null; return null;
@@ -0,0 +1,9 @@
namespace NzbDrone.Core.DecisionEngine;
public class DownloadRejection : Rejection<DownloadRejectionReason>
{
public DownloadRejection(DownloadRejectionReason reason, string message, RejectionType type = RejectionType.Permanent)
: base(reason, message, type)
{
}
}
@@ -0,0 +1,67 @@
namespace NzbDrone.Core.DecisionEngine;
public enum DownloadRejectionReason
{
Unknown,
UnknownMovie,
UnableToParse,
Error,
DecisionError,
Availability,
MinimumAgeDelay,
MovieNotMonitored,
HistoryRecentCutoffMet,
HistoryCdhDisabledCutoffMet,
HistoryHigherPreference,
HistoryHigherRevision,
HistoryCutoffMet,
HistoryCustomFormatCutoffMet,
HistoryCustomFormatScore,
HistoryCustomFormatScoreIncrement,
HistoryUpgradesNotAllowed,
NoMatchingTag,
PropersDisabled,
ProperForOldFile,
WrongMovie,
UnknownRuntime,
BelowMinimumSize,
AboveMaximumSize,
AlreadyImportedSameHash,
AlreadyImportedSameName,
IndexerDisabled,
Blocklisted,
CustomFormatMinimumScore,
MinimumFreeSpace,
HardcodeSubtitles,
WantedLanguage,
MaximumSizeExceeded,
MinimumAge,
MaximumAge,
Sample,
ProtocolDisabled,
QualityNotWanted,
QualityUpgradesDisabled,
QueueHigherPreference,
QueueHigherRevision,
QueueCutoffMet,
QueueCustomFormatCutoffMet,
QueueCustomFormatScore,
QueueCustomFormatScoreIncrement,
QueueUpgradesNotAllowed,
QueuePropersDisabled,
Raw,
MustContainMissing,
MustNotContainPresent,
RepackDisabled,
RepackUnknownReleaseGroup,
RepackReleaseGroupDoesNotMatch,
RequiredFlags,
MinimumSeeders,
DiskHigherPreference,
DiskHigherRevision,
DiskCutoffMet,
DiskCustomFormatCutoffMet,
DiskCustomFormatScore,
DiskCustomFormatScoreIncrement,
DiskUpgradesNotAllowed
}
@@ -0,0 +1,34 @@
namespace NzbDrone.Core.DecisionEngine
{
public class DownloadSpecDecision
{
public bool Accepted { get; private set; }
public DownloadRejectionReason Reason { get; set; }
public string Message { get; private set; }
private static readonly DownloadSpecDecision AcceptDownloadSpecDecision = new () { Accepted = true };
private DownloadSpecDecision()
{
}
public static DownloadSpecDecision Accept()
{
return AcceptDownloadSpecDecision;
}
public static DownloadSpecDecision Reject(DownloadRejectionReason reason, string message, params object[] args)
{
return Reject(reason, string.Format(message, args));
}
public static DownloadSpecDecision Reject(DownloadRejectionReason reason, string message)
{
return new DownloadSpecDecision
{
Accepted = false,
Reason = reason,
Message = message
};
}
}
}

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