1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-03-26 17:44:24 -04:00

Compare commits

...

117 Commits

Author SHA1 Message Date
servarr[bot]
06a96ef2d1 Fixed: Movies poster view on mobile devices (#9659)
(cherry picked from commit c0b30a50281c38a103c2f800064d0ec964fae6e0)

Co-authored-by: Mark McDowall <mark@mcdowall.ca>
2024-01-21 03:44:01 +02:00
Bogdan
c77ce2459c Transpile logical assignment operators with babel
(cherry picked from commit 3cf4d2907e32e81050f35cda042dcc2b4641d40d)
2024-01-21 03:43:33 +02:00
servarr[bot]
083989d151 New: Log warning if less than 1 GB free space during update
* New: Log warning if less than 1 GB free space during update

(cherry picked from commit e66ba84fc0b5b120dd4e87f6b8ae1b3c038ee72b)

---------

Co-authored-by: Mark McDowall <mark@mcdowall.ca>
2024-01-21 03:43:10 +02:00
Mark McDowall
c003fe16de Fixed: Don't clone indexer API Key
(cherry picked from commit d336aaf3f04136471970155b5a7cc876770c64ff)
2024-01-20 07:43:06 +02:00
Mark McDowall
bc9b2cd283 Refactor icons on full color calendar events
(cherry picked from commit 9884f6f282560ff2a0ea193e9306c6284cf8672c)

Closes #9646
2024-01-19 16:10:55 +02:00
Bogdan
d0e400c55a Wrap values in log messages in FileListParser
Closes #9644
2024-01-19 09:51:12 +02:00
Stevie Robinson
77863dc2cf New: Drop commands table content before postgres migration
Signed-off-by: Stevie Robinson <stevie.robinson@gmail.com>

(cherry picked from commit 8dd3b45c90209136c0bd0a861061c6d20837d62f)
2024-01-19 09:51:04 +02:00
Stevie Robinson
18dc6f60b0 Round off the seeded ratio when checking for removal candidates
Signed-off-by: Stevie Robinson <stevie.robinson@gmail.com>

(cherry picked from commit c6bb6ad8788fb1c20ed466a495f2b47034947145)
2024-01-19 08:15:59 +02:00
Bogdan
49501a55ae Check paged requests using PageSize for Import Lists 2024-01-18 09:30:28 +02:00
Weblate
d5d77a4f1a Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Blair Noctis <fqmxz5hyfba7ft85@neon.casa>
Co-authored-by: Dani Talens <databio@gmail.com>
Co-authored-by: Deleted User <noreply+2593@weblate.org>
Co-authored-by: Koch Norbert <kochnorbert@icloud.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/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2024-01-18 09:24:07 +02:00
Bogdan
0ae8952b38 Add SizeOnDisk and HasFile to MovieResource 2024-01-17 06:25:50 +02:00
Bogdan
6292ff76b0 Rename episode to movie 2024-01-17 05:21:25 +02:00
Bogdan
646d271e81 Add title to invalid Plex RSS item log
Co-authored-by: bakerboy448 <55419169+bakerboy448@users.noreply.github.com>
2024-01-17 01:15:57 +02:00
Bogdan
3d2ca830bc Fixed: Importing Plex RSS lists with invalid items 2024-01-17 01:13:21 +02:00
Bogdan
da02ec3b04 Add missing import needed for Added column 2024-01-17 00:49:17 +02:00
bakerboy448
cc9a443473 Update logging to indicate a hardlink is being attempted
(cherry picked from commit 16e5ffa467f72e52c750143c835f6ee1c1c2460b)

Closes #9611
2024-01-17 00:12:17 +02:00
Stevie Robinson
81b6bf521d Add missing translation keys from Indexer Settings
Signed-off-by: Stevie Robinson <stevie.robinson@gmail.com>
(cherry picked from commit 07fbb0d1f464513ed28721d6c91d428dd54818cf)

Closes #9627
2024-01-17 00:10:56 +02:00
Bogdan
7edb892eb4 Throw download as failed for invalid magnet links
(cherry picked from commit 091449d9bff9023ca27a85cc1048296f7d5ea37b)

Closes #9625
Fixes #9125
2024-01-17 00:07:04 +02:00
Blair Noctis
3b36921787 Fixed: Improve help text for download client priority
(cherry picked from commit 1bba7e177b5b4173e620cd014ffdc231023309a0)

Closes #9622
2024-01-17 00:06:12 +02:00
Rubicj
c2d8bc85d0 New: Added column in Queue
(cherry picked from commit 57445bbe57a84990e284ef97d42455a06587e1ee)

Closes #9621
2024-01-17 00:04:30 +02:00
Bogdan
3e55b1cf25 Fix Content-Type in FileList fixture 2024-01-16 21:51:55 +02:00
Bogdan
0b0c93081d Check Content-Type in FileList parser 2024-01-16 21:39:03 +02:00
Servarr
91fbad72c0 Automated API Docs update 2024-01-16 20:58:27 +02:00
Bogdan
35651ac59b New: Release Groups for movie table index
* New: Release Group for movie table index

Co-authored-by: Qstick <qstick@gmail.com>

* fixup! New: Release Group for movie table index

---------

Co-authored-by: Qstick <qstick@gmail.com>
2024-01-16 20:52:07 +02:00
Qstick
1932aec131 Improved http timeout handling
(cherry picked from commit f87a66fcba6ca9ca972fa1c747a940b216e0e5e3)
2024-01-16 08:48:21 +02:00
Stevie Robinson
ea470b4ee9 Sort Custom Filters
(cherry picked from commit e4b5d559df2d5f3d55e16aae5922509e84f31e64)
2024-01-16 08:05:18 +02:00
Qstick
1bb404a912 Fixed: Only use frames for Primary video stream for analysis 2024-01-15 23:16:22 -06:00
Weblate
374d20634d Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Daniele Prevedello <dprevedello86@gmail.com>
Co-authored-by: DimitriDR <dimitridroeck@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Petr Vojar <vojar.petr@outlook.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: crayon3shawn <crayon3shawn@gmail.com>
Co-authored-by: hansaudun <hans@n5.no>
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/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_TW/
Translation: Servarr/Radarr
2024-01-15 01:44:06 +02:00
Qstick
60d9aacac6 Build report can get sent before installer finished 2024-01-14 13:27:52 -06:00
Qstick
c5992ed944 Bump Inno version to 6.2.2 2024-01-14 12:59:58 -06:00
Mark McDowall
4c4073ce1c New: Support SABnzb's new format for sorters
(cherry picked from commit d484553b310af4257c841c37a503ef54650c1426)

Closes #9351
2024-01-14 18:08:18 +02:00
Jendrik Weise
d72f78d979 New: Custom import scripts can communicate information back
(cherry picked from commit b4ac495983d61819d9ab84f49c880957ba57418b)
2024-01-14 16:15:48 +02:00
Bogdan
dca9d69aaa Bump version to 5.3.2 2024-01-14 07:10:07 +02:00
Stevie Robinson
5a64826868 Add: New icon for deleted episodes with status missing from disk
Signed-off-by: Stevie Robinson <stevie.robinson@gmail.com>
(cherry picked from commit 79907c881cc92ce9ee3973d5cf21749fe5fc58da)

Closes #9604
2024-01-14 03:47:30 +02:00
Mark McDowall
cda40312e0 New: Optional directory setting for Aria2
(cherry picked from commit fd17df0dd03a5feb088c3241a247eac20f0e8c6c)

Closes #9602
2024-01-14 03:43:37 +02:00
Bogdan
907779b4ce Fetch movie file entity from database to broadcast 2024-01-14 03:42:03 +02:00
Mark McDowall
cc03651af5 Don't use TestCase for single test
(cherry picked from commit 541d3307e1466b0353dc4149f502a4b62b4de616)
2024-01-14 03:41:26 +02:00
servarr[bot]
1ae98d618c Fixed: Movie posters flickering when width changes repeatedly
(cherry picked from commit 53cf5308931069638c23925596a3fd8aaccc5d98)

Co-authored-by: Mark McDowall <mark@mcdowall.ca>
2024-01-14 03:40:54 +02:00
Bogdan
f5914da2f9 Remove double filtering in entity history repository 2024-01-12 22:05:51 +02:00
Bogdan
f7816aa5cd Fixed: Filter history by multiple event types in PG 2024-01-12 22:05:51 +02:00
Andrejs Ķīlis
a652ce50a9 Fixed: Latvian and Russian language parsing
Improved support for Latvian with test cases I have encountered in the wild and fixed a case where Russian is not recognized (RU instead of RUS).
2024-01-12 03:00:51 +02:00
Bogdan
58b726a292 Fixed: Improve torrent blocklist matching
Closes #9585
2024-01-12 02:56:32 +02:00
Bogdan
1d8cf6a7f5 Fixed: Persist release source for pending releases
Closes #9583
2024-01-12 02:54:31 +02:00
ilike2burnthing
2c3ad380ef Remove unsupported pagination for Nyaa
(cherry picked from commit fef525ddb8b5f91bb36b3c9e652663fccb098a00)

Closes #9582
2024-01-12 02:52:53 +02:00
Stevie Robinson
0e7874aacf Fix Missing HelpText Translation Keys
(cherry picked from commit 587b600d6c6bac64c99d12225360810ef283f0aa)

Closes #9576
2024-01-12 02:45:57 +02:00
Weblate
8638d82ad3 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Aleksandr <alyarmak@gmail.com>
Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Bradley BARBIER <bradley.barbier@outlook.fr>
Co-authored-by: Dani Talens <databio@gmail.com>
Co-authored-by: DimitriDR <dimitridroeck@gmail.com>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: HuaBing <admin@hbcraft.cn>
Co-authored-by: JJonttuu <oikeaihminen@protonmail.com>
Co-authored-by: Juan Lores <juan.lores@gmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Piotr Komborski <piotr+github@kombor.ski>
Co-authored-by: RicardoVelaC <ricardovelac@gmail.com>
Co-authored-by: Watashi <drazy24@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: boan51204 <je.991707@gmail.com>
Co-authored-by: dtalens <databio@gmail.com>
Co-authored-by: liangyi <994302767@qq.com>
Co-authored-by: marius-tu <marius.tubbesing94@gmail.com>
Co-authored-by: ragote <ragote@pm.me>
Co-authored-by: reloxx <reloxx@interia.pl>
Co-authored-by: twobuttonbob <madinlol@gmail.com>
Co-authored-by: 饶志华 <879467666@qq.com>
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/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/lv/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_TW/
Translation: Servarr/Radarr
2024-01-12 02:41:40 +02:00
Bogdan
f3d6a1f99d Fixed: Release source for release/push 2024-01-11 02:07:03 +02:00
Bogdan
fa036f5807 Sorting movie list by tags 2024-01-11 00:41:53 +02:00
Bogdan
a931f8a69f Fixed: Skip fewer slides with cast/crew on smaller screens
Fixes #9571
2024-01-10 20:51:40 +02:00
Bogdan
a491c9a4a0 Fixed: Parsing custom formats for releases titles containing colon 2024-01-10 20:34:24 +02:00
Bogdan
2aafb6369c Fix app name in healthcheck 2024-01-10 01:35:08 +02:00
Mark McDowall
ef8253044e Fixed: Blocklisting torrents from indexers that do not provide torrent hash
(cherry picked from commit 3541cd7ba877fb785c7f97123745abf51162eb8e)
2024-01-09 00:30:32 +02:00
Bogdan
c1feeb72ee New: Year specification for custom formats 2024-01-08 02:29:29 +02:00
Servarr
21560cd6cc Automated API Docs update 2024-01-07 16:36:16 +02:00
Bogdan
bda2b9b0b8 Fixed: Filter history by multiple event types 2024-01-07 16:07:36 +02:00
Bogdan
4630de9616 Bump version to 5.3.1 2024-01-07 11:10:52 +02:00
Bogdan
7e83180e50 Remove title for actions in movie history
Closes #9549
2024-01-04 13:24:28 +02:00
Stevie Robinson
e60eed49c7 Translate Notifications settings
(cherry picked from commit 8f7f23c9380036e87669fa663e846321cf7ebf87)

Closes #9550
2024-01-04 12:49:26 +02:00
Gabriel Patzleiner
74cfc94b4c New: Correctly parse German DL and ML tags in releases 2024-01-02 18:58:28 -06:00
Gabriel Patzleiner
213c55c7af Fixed: Don't parse some movies with German in the movie title
fixes #6474
2024-01-02 18:58:28 -06:00
Gabriel Patzleiner
c066fa5e27 Delete tests that are not needed and not working anymore since 7ec0fd1cea 2024-01-02 18:58:28 -06:00
Gabriel Patzleiner
2741ecb968 Added new IndexerBaseFixture to test Multi tag in releases 2024-01-02 18:58:28 -06:00
Qstick
7965c29425 Fixed: Change "Manual Import" to "Manage Files" in MovieDetails
Prevent confusion with interactive search icon being identical and align to Sonarr naming.
2024-01-01 11:35:11 -06:00
Bogdan
d2cbab70a9 New: Confirmation for searching movies 2024-01-01 17:35:01 +02:00
Weblate
16381a1aef Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Mario Rodriguez <mario2423@gmail.com>
Co-authored-by: Norbi <kovinor123@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translation: Servarr/Radarr
2024-01-01 14:11:00 +02:00
Bogdan
b92e08b850 Fixed: Disable movie search button if none are listed 2024-01-01 08:20:35 +02:00
Bogdan
eab470c67f New: Movie search will look for movies that haven't been searched recently first 2024-01-01 08:20:35 +02:00
Mark McDowall
7f11659d95 New: Store last search time for MovieSearch
(cherry picked from commit 9af57c6786)
2024-01-01 08:20:35 +02:00
Mark McDowall
03dec07cbe Fixed: Disable SSL on start if certificate path is not set
(cherry picked from commit 4e19fec123900b8ba1252b640f26f2a4983683ff)
2023-12-31 18:40:21 -06:00
Qstick
554c696ee6 Fixed: MovieDetails size incorrect when moviefile store changes
Use movie prop instead

Closes #9309
2023-12-31 16:47:44 -06:00
Qstick
093f8a39fe New: Custom sort crew by job in movie details 2023-12-31 12:55:09 -06:00
Servarr
8a1663f136 Automated API Docs update 2023-12-31 12:03:51 -06:00
bakerboy448
251d2dde97 Improve Import Custom Format Compare Logging 2023-12-31 11:37:02 -06:00
Qstick
996542a4a5 Reduce size of Collection on Movie endpoint
Ensures we don't send false data and reduces the object size to only what's necessary here.

Closes #9521
2023-12-31 11:29:32 -06:00
randomllama
0914d6250c New: Add Movie Status to Kodi .nfo
Closes #9115

(cherry picked from commit b76de3987b0c30e6509d37d82e3163d067a9c6c8)
2023-12-31 11:19:25 -06:00
Bogdan
3ff8e511b5 New: Tags field for Discord 2023-12-31 18:58:39 +02:00
Qstick
3a7b27fb45 Fixed: Parse HebDubbed as Hebrew
Fixes #9513
2023-12-31 10:48:13 -06:00
Weblate
c81d2c97f5 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dimitri <dimitridroeck@gmail.com>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Koch Norbert <kochnorbert@icloud.com>
Co-authored-by: Nicola <nicola.neri@gmail.com>
Co-authored-by: SunStorm <me@sunstorm.rocks>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: chiral-lab <jan.eltner@googlemail.com>
Co-authored-by: chrizl <chrizl@gmail.com>
Co-authored-by: resi23 <x-resistant-x@gmx.de>
Co-authored-by: slammingdeath <sebastianbrudny97@gmail.com>
Co-authored-by: ube <ube@alienautopsy.net>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/it/
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/sv/
Translation: Servarr/Radarr
2023-12-31 12:26:08 +02:00
Bogdan
dae46524c4 Fix possible multiple enumeration in update collections 2023-12-31 09:49:17 +02:00
Stevie Robinson
3c6386f318 Translate fields on the backend
(cherry picked from commit 48b12f5b00429a7cd218d23f0544641b0da62a06)
2023-12-31 09:38:03 +02:00
Mark McDowall
1400a8806d New: Add qBittorrent option for Content Layout
(cherry picked from commit 4b22200708ca120cfdcf9cb796be92183adb95d1)

Closes #9522
2023-12-31 09:38:03 +02:00
Stevie Robinson
e3f33f5a61 New: Add sorting to Manage Indexer and Download Client modals
(cherry picked from commit 91053ca51ded804739f94ee936c1376a755dbe11)

Closes #9524
2023-12-31 09:38:03 +02:00
Stevie Robinson
e6f4b88cf3 New: Show Proper or Repack tag in interactive search
(cherry picked from commit efb000529b5dff42829df3ef151e4750a7b15cf6)

Closes #9523
2023-12-31 09:38:03 +02:00
Bogdan
b788464487 Fixed: Show errors when adding Root Folder
(cherry picked from commit 16d60a6586aeb458601214258da021ee154e5b6e)

Closes #9527
2023-12-31 09:38:03 +02:00
Bogdan
e29717ec6c New: Retry on failed downloads of torrent and nzb files
(cherry picked from commit bc20ef73bdd47b7cdad43d4c7d4b4bd534e49252)

Closes #9528
2023-12-31 09:38:03 +02:00
Bogdan
5d7e23092f Bump version to 5.3.0 2023-12-31 09:38:03 +02:00
Bogdan
9921d51451 Cleanup unused code in movie credit posters 2023-12-25 15:45:33 +02:00
Bogdan
213620cb29 Fixed: Navigation for cast and crew 2023-12-25 14:56:41 +02:00
Bogdan
bdc4aade0f Use extra release fields in PassThePopcorn parser 2023-12-24 06:56:48 +02:00
Weblate
b2300dbf41 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dimitri <dimitridroeck@gmail.com>
Co-authored-by: Pietro Ribeiro <xxb1exuv6@mozmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt/
Translation: Servarr/Radarr
2023-12-23 23:31:39 +02:00
Weblate
44289d30f9 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Aitzol Garmendia <aitzolgarmendia@gmail.com>
Co-authored-by: Andrés Reyes Monge <armonge@gmail.com>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Michael Schönenberger <muchi94@gmail.com>
Co-authored-by: VisoTC <szlytlyt@outlook.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: ηg <jonas.konrath@icloud.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2023-12-22 04:24:19 +02:00
luz paz
260fb88f85 Fix various typos
Found via `codespell -q 3`

(cherry picked from commit 209a250079fdf7ad2bc9168f81bfb45b9531d6b3)
2023-12-19 20:18:54 +02:00
Bogdan
119cdf6f09 Fixed: Cleanup orphaned import list movies by movie metadata 2023-12-18 00:51:59 +02:00
Bogdan
c8d30fd214 Cleanup convert root folders to TS 2023-12-17 23:23:08 +02:00
Bogdan
7e9e528d3b Fixed: Ignore empty tags when adding items to Flood
Fixed #8145
2023-12-17 22:09:13 +02:00
Bogdan
8554c0d9cb Refactor movie alternative titles connector 2023-12-17 19:57:22 +02:00
Bogdan
22cc34b4fe Bump version to 5.2.6 2023-12-17 16:01:10 +02:00
Weblate
990785ebfc Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Menno Liefstingh <mennoliefstingh@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: lifeisfreedom048 <koyuncu.ozgur@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translation: Servarr/Radarr
2023-12-16 02:41:57 +02:00
Bogdan
957be99401 Fixed: Bump media info revision for DV HDR10Plus 2023-12-16 02:41:34 +02:00
Bogdan
4bcde25e29 Improve messaging for accepted Custom Formats scoring upgrades
Co-authored-by: bakerboy448 <55419169+bakerboy448@users.noreply.github.com>

Closes #9496
2023-12-16 00:38:05 +02:00
Bogdan
1d70f36e7d New: 3D and HDR metadata for Trakt connection 2023-12-15 17:13:47 +02:00
Bogdan
cc0a448bc8 New: Rate limiting for Trakt connection 2023-12-15 17:13:47 +02:00
Bogdan
c9e977baea Simplify mapping in Trakt connection 2023-12-15 17:13:47 +02:00
Mark McDowall
6cb9a46cd4 Fixed: Imported movies updating on Calendar
(cherry picked from commit 5a3bc49392b700650a34536ff3794bce614f64a4)

Closes #9491
2023-12-15 16:50:06 +02:00
Agneev Mukherjee
eef379277a Enable browser navigation buttons for PWA
(cherry picked from commit da9a60691f363323565a293ed9eaeb6349ceccb6)

Closes #9487
2023-12-15 16:36:28 +02:00
Chad A Simmons
41fef47684 New: Support for DV HDR10Plus from media info 2023-12-15 03:36:32 +02:00
Weblate
fcda6faf3d Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: ROSERAT Ugo <roserat.ugo@gmail.com>
Co-authored-by: RicardoVelaC <ricardovelac@gmail.com>
Co-authored-by: SHUAI.W <x@ousui.org>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/lv/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2023-12-13 16:41:49 +02:00
Bogdan
79bbf9c50b Fixed: Movie status label in add movie search results 2023-12-12 21:55:41 +02:00
Bogdan
43d2f2804b New: IMDb ratings and genres in add movie search results 2023-12-12 21:55:09 +02:00
Qstick
fa62f3f66a Fixed: Correctly handle Migration when PG Host has ".db"
(cherry picked from commit 97ee24507f4306e3b62c3d00cd3ade6a09d1b957)

Closes #9478
2023-12-12 15:36:21 +02:00
Bogdan
229d91fe40 Implement DatabaseConnectionInfo
Co-authored-by: Qstick <qstick@gmail.com>
2023-12-12 15:36:14 +02:00
Bogdan
2673d1eee4 Fixed: Movie poster in search results after adding
Fixes #8029
2023-12-11 19:30:31 +02:00
Bogdan
e59fd1118f Fixed: Downloading status post-adding movie 2023-12-11 19:27:05 +02:00
Bogdan
c1fd33b152 Fix categories for NZBFinder 2023-12-10 15:50:19 +02:00
Bogdan
2f58c8676f Bump dotnet to 6.0.25 2023-12-10 15:35:59 +02:00
Fossil
defc448304 Update NZBFinder categories and remove OZnzb & NZB-Tortuga from default definitions (#9474)
NZB Finder will consolidate WEBDL & X265 into SD,HD,UHD so removed 2080 and 2090 categories.

OZnzb and NZB Tortuga are dead so removed it from the presets list.
2023-12-10 15:14:13 +02:00
Bogdan
3ec3358728 Bump version to 5.2.5 2023-12-10 13:47:06 +02:00
302 changed files with 4517 additions and 2472 deletions

View File

@@ -9,15 +9,15 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '5.2.4'
majorVersion: '5.3.2'
minorVersion: $[counter('minorVersion', 2000)]
radarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.413'
dotnetVersion: '6.0.417'
nodeVersion: '16.X'
innoVersion: '6.2.0'
innoVersion: '6.2.2'
windowsImage: 'windows-2022'
linuxImage: 'ubuntu-20.04'
macImage: 'macOS-11'
@@ -1242,6 +1242,7 @@ stages:
- stage: Report_Out
dependsOn:
- Analyze
- Installer
- Unit_Test
- Integration
- Automation

View File

@@ -254,7 +254,7 @@ InstallInno()
ProgressStart "Installing portable Inno Setup"
rm -rf _inno
curl -s --output innosetup.exe "https://files.jrsoftware.org/is/6/innosetup-${INNOVERSION:-6.2.0}.exe"
curl -s --output innosetup.exe "https://files.jrsoftware.org/is/6/innosetup-${INNOVERSION:-6.2.2}.exe"
mkdir _inno
./innosetup.exe //portable=1 //silent //currentuser //dir=.\\_inno
rm innosetup.exe

View File

@@ -2,6 +2,8 @@ const loose = true;
module.exports = {
plugins: [
'@babel/plugin-transform-logical-assignment-operators',
// Stage 1
'@babel/plugin-proposal-export-default-from',
['@babel/plugin-transform-optional-chaining', { loose }],

View File

@@ -6,7 +6,7 @@ import { icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './HistoryEventTypeCell.css';
function getIconName(eventType) {
function getIconName(eventType, data) {
switch (eventType) {
case 'grabbed':
return icons.DOWNLOADING;
@@ -17,7 +17,7 @@ function getIconName(eventType) {
case 'downloadFailed':
return icons.DOWNLOADING;
case 'movieFileDeleted':
return icons.DELETE;
return data.reason === 'MissingFromDisk' ? icons.FILE_MISSING : icons.DELETE;
case 'movieFileRenamed':
return icons.ORGANIZE;
case 'downloadIgnored':
@@ -47,7 +47,7 @@ function getTooltip(eventType, data) {
case 'downloadFailed':
return translate('MovieDownloadFailedTooltip');
case 'movieFileDeleted':
return translate('MovieFileDeletedTooltip');
return data.reason === 'MissingFromDisk' ? translate('MovieFileMissingTooltip') : translate('MovieFileDeletedTooltip');
case 'movieFileRenamed':
return translate('MovieFileRenamedTooltip');
case 'downloadIgnored':
@@ -58,7 +58,7 @@ function getTooltip(eventType, data) {
}
function HistoryEventTypeCell({ eventType, data }) {
const iconName = getIconName(eventType);
const iconName = getIconName(eventType, data);
const iconKind = getIconKind(eventType);
const tooltip = getTooltip(eventType, data);

View File

@@ -4,7 +4,7 @@ import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
import IconButton from 'Components/Link/IconButton';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import ProgressBar from 'Components/ProgressBar';
// import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import TableRow from 'Components/Table/TableRow';
@@ -97,6 +97,7 @@ class QueueRow extends Component {
outputPath,
downloadClient,
estimatedCompletionTime,
added,
timeleft,
size,
sizeleft,
@@ -315,6 +316,15 @@ class QueueRow extends Component {
);
}
if (name === 'added') {
return (
<RelativeDateCellConnector
key={name}
date={added}
/>
);
}
if (name === 'actions') {
return (
<TableRowCell
@@ -393,6 +403,7 @@ QueueRow.propTypes = {
outputPath: PropTypes.string,
downloadClient: PropTypes.string,
estimatedCompletionTime: PropTypes.string,
added: PropTypes.string,
timeleft: PropTypes.string,
size: PropTypes.number,
year: PropTypes.number,

View File

@@ -85,8 +85,13 @@
margin-top: 20px;
}
.studio,
.genres {
margin-left: 5px;
}
.links {
margin-left: 8px;
margin-left: 5px;
pointer-events: all;
}

View File

@@ -5,6 +5,7 @@ interface CssExports {
'certification': string;
'content': string;
'exclusionIcon': string;
'genres': string;
'icons': string;
'links': string;
'overlay': string;
@@ -14,6 +15,7 @@ interface CssExports {
'runtime': string;
'searchResult': string;
'statusContainer': string;
'studio': string;
'title': string;
'titleContainer': string;
'titleRow': string;

View File

@@ -1,6 +1,7 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Icon from 'Components/Icon';
import ImdbRating from 'Components/ImdbRating';
import Label from 'Components/Label';
import Link from 'Components/Link/Link';
import TmdbRating from 'Components/TmdbRating';
@@ -61,6 +62,7 @@ class AddNewMovieSearchResult extends Component {
titleSlug,
year,
studio,
genres,
status,
overview,
ratings,
@@ -76,6 +78,7 @@ class AddNewMovieSearchResult extends Component {
hasFile,
isAvailable,
movieFile,
queueItem,
runtime,
movieRuntimeFormat,
certification
@@ -197,13 +200,46 @@ class AddNewMovieSearchResult extends Component {
/>
</Label>
{
ratings.imdb ?
<Label size={sizes.LARGE}>
<ImdbRating
ratings={ratings}
iconSize={13}
/>
</Label> :
null
}
{
!!studio &&
<Label size={sizes.LARGE}>
{studio}
<Icon
name={icons.STUDIO}
size={13}
/>
<span className={styles.studio}>
{studio}
</span>
</Label>
}
{
genres.length > 0 ?
<Label size={sizes.LARGE}>
<Icon
name={icons.GENRE}
size={13}
/>
<span className={styles.genres}>
{genres.slice(0, 3).join(', ')}
</span>
</Label> :
null
}
<Tooltip
anchor={
<Label
@@ -215,15 +251,15 @@ class AddNewMovieSearchResult extends Component {
/>
<span className={styles.links}>
Links
{translate('Links')}
</span>
</Label>
}
tooltip={
<MovieDetailsLinks
tmdbId={tmdbId}
youTubeTrailerId={youTubeTrailerId}
imdbId={imdbId}
youTubeTrailerId={youTubeTrailerId}
/>
}
canFlip={true}
@@ -237,6 +273,7 @@ class AddNewMovieSearchResult extends Component {
hasMovieFiles={hasFile}
monitored={monitored}
isAvailable={isAvailable}
queueItem={queueItem}
id={id}
useLabel={true}
colorImpairedMode={colorImpairedMode}
@@ -273,6 +310,7 @@ AddNewMovieSearchResult.propTypes = {
titleSlug: PropTypes.string.isRequired,
year: PropTypes.number.isRequired,
studio: PropTypes.string,
genres: PropTypes.arrayOf(PropTypes.string),
status: PropTypes.string.isRequired,
overview: PropTypes.string,
ratings: PropTypes.object.isRequired,
@@ -283,15 +321,19 @@ AddNewMovieSearchResult.propTypes = {
isExclusionMovie: PropTypes.bool.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
id: PropTypes.number,
queueItems: PropTypes.arrayOf(PropTypes.object),
monitored: PropTypes.bool.isRequired,
hasFile: PropTypes.bool.isRequired,
isAvailable: PropTypes.bool.isRequired,
movieFile: PropTypes.object,
queueItem: PropTypes.object,
colorImpairedMode: PropTypes.bool,
runtime: PropTypes.number.isRequired,
movieRuntimeFormat: PropTypes.string.isRequired,
certification: PropTypes.string
};
AddNewMovieSearchResult.defaultProps = {
genres: []
};
export default AddNewMovieSearchResult;

View File

@@ -10,14 +10,18 @@ function createMapStateToProps() {
createExistingMovieSelector(),
createExclusionMovieSelector(),
createDimensionsSelector(),
(state) => state.queue.details.items,
(state, { internalId }) => internalId,
(state) => state.settings.ui.item.movieRuntimeFormat,
(isExistingMovie, isExclusionMovie, dimensions, internalId, movieRuntimeFormat) => {
(isExistingMovie, isExclusionMovie, dimensions, queueItems, internalId, movieRuntimeFormat) => {
const queueItem = queueItems.find((item) => internalId > 0 && item.movieId === internalId);
return {
existingMovieId: internalId,
isExistingMovie,
isExclusionMovie,
isSmallScreen: dimensions.isSmallScreen,
queueItem,
movieRuntimeFormat
};
}

View File

@@ -32,7 +32,7 @@
.contentContainer {
z-index: $popperZIndex;
margin-top: 4px;
/* 400px container witdh with 8px padding on each side */
/* 400px container width with 8px padding on each side */
width: 384px;
}

View File

@@ -148,7 +148,7 @@ class ImportMovieSelectFolder extends Component {
className={styles.addErrorAlert}
kind={kinds.DANGER}
>
{translate('UnableToAddRootFolder')}
{translate('AddRootFolderError')}
<ul>
{

View File

@@ -44,7 +44,16 @@ export interface CustomFilter {
filers: PropertyFilter[];
}
export interface AppSectionState {
dimensions: {
isSmallScreen: boolean;
width: number;
height: number;
};
}
interface AppState {
app: AppSectionState;
calendar: CalendarAppState;
commands: CommandAppState;
history: HistoryAppState;

View File

@@ -147,7 +147,7 @@ class AgendaEvent extends Component {
className={styles.statusIcon}
name={icons.MOVIE_FILE}
kind={kinds.WARNING}
title={translate('QualityCutoffHasNotBeenMet')}
title={translate('QualityCutoffNotMet')}
/>
}
</div>

View File

@@ -55,7 +55,7 @@ class CalendarConnector extends Component {
gotoCalendarToday
} = this.props;
registerPagePopulator(this.repopulate);
registerPagePopulator(this.repopulate, ['movieFileUpdated', 'movieFileDeleted']);
if (useCurrentPage) {
fetchCalendar();

View File

@@ -48,6 +48,10 @@ $fullColorGradient: rgba(244, 245, 246, 0.2);
.statusContainer {
display: flex;
align-items: center;
&:global(.fullColor) {
filter: var(--calendarFullColorFilter)
}
}
.statusIcon {

View File

@@ -76,12 +76,18 @@ class CalendarEvent extends Component {
{title}
</div>
<div className={styles.statusContainer}>
<div
className={classNames(
styles.statusContainer,
fullColorEvents && 'fullColor'
)}
>
{
queueItem ?
<span className={styles.statusIcon}>
<CalendarEventQueueDetails
{...queueItem}
fullColorEvents={fullColorEvents}
/>
</span> :
null
@@ -98,12 +104,14 @@ class CalendarEvent extends Component {
}
{
showCutoffUnmetIcon && !!movieFile && movieFile.qualityCutoffNotMet ?
showCutoffUnmetIcon &&
!!movieFile &&
movieFile.qualityCutoffNotMet ?
<Icon
className={styles.statusIcon}
name={icons.MOVIE_FILE}
kind={kinds.WARNING}
title={translate('QualityCutoffHasNotBeenMet')}
title={translate('QualityCutoffNotMet')}
/> :
null
}

View File

@@ -20,10 +20,11 @@ function Legend(props) {
if (showCutoffUnmetIcon) {
iconsToShow.push(
<LegendIconItem
name={translate('CutoffUnmet')}
name={translate('CutoffNotMet')}
icon={icons.MOVIE_FILE}
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING}
tooltip={translate('QualityOrLangCutoffHasNotBeenMet')}
kind={kinds.WARNING}
fullColorEvents={fullColorEvents}
tooltip={translate('QualityCutoffNotMet')}
/>
);
}

View File

@@ -4,4 +4,8 @@
.icon {
margin-right: 5px;
&:global(.fullColorEvents) {
filter: var(--calendarFullColorFilter)
}
}

View File

@@ -1,3 +1,4 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
@@ -6,9 +7,9 @@ import styles from './LegendIconItem.css';
function LegendIconItem(props) {
const {
name,
fullColorEvents,
icon,
kind,
darken,
tooltip
} = props;
@@ -18,9 +19,11 @@ function LegendIconItem(props) {
title={tooltip}
>
<Icon
className={styles.icon}
className={classNames(
styles.icon,
fullColorEvents && 'fullColorEvents'
)}
name={icon}
darken={darken}
kind={kind}
/>
@@ -31,14 +34,10 @@ function LegendIconItem(props) {
LegendIconItem.propTypes = {
name: PropTypes.string.isRequired,
fullColorEvents: PropTypes.bool.isRequired,
icon: PropTypes.object.isRequired,
kind: PropTypes.string.isRequired,
darken: PropTypes.bool.isRequired,
tooltip: PropTypes.string.isRequired
};
LegendIconItem.defaultProps = {
darken: false
};
export default LegendIconItem;

View File

@@ -74,11 +74,7 @@ CollectionMovieLabel.propTypes = {
CollectionMovieLabel.defaultProps = {
isSaving: false,
statistics: {
episodeFileCount: 0,
totalEpisodeCount: 0,
percentOfEpisodes: 0
}
statistics: {}
};
export default CollectionMovieLabel;

View File

@@ -30,22 +30,24 @@ function CustomFiltersModalContent(props) {
<ModalBody>
{
customFilters.map((customFilter) => {
return (
<CustomFilter
key={customFilter.id}
id={customFilter.id}
label={customFilter.label}
filters={customFilter.filters}
selectedFilterKey={selectedFilterKey}
isDeleting={isDeleting}
deleteError={deleteError}
dispatchSetFilter={dispatchSetFilter}
dispatchDeleteCustomFilter={dispatchDeleteCustomFilter}
onEditPress={onEditCustomFilter}
/>
);
})
customFilters
.sort((a, b) => a.label.localeCompare(b.label))
.map((customFilter) => {
return (
<CustomFilter
key={customFilter.id}
id={customFilter.id}
label={customFilter.label}
filters={customFilter.filters}
selectedFilterKey={selectedFilterKey}
isDeleting={isDeleting}
deleteError={deleteError}
dispatchSetFilter={dispatchSetFilter}
dispatchDeleteCustomFilter={dispatchDeleteCustomFilter}
onEditPress={onEditCustomFilter}
/>
);
})
}
<div className={styles.addButtonContainer}>

View File

@@ -12,18 +12,10 @@
.info {
color: var(--infoColor);
&:global(.darken) {
color: color(var(--infoColor) shade(30%));
}
}
.pink {
color: var(--pink);
&:global(.darken) {
color: color(var(--pink) shade(30%));
}
}
.success {

View File

@@ -18,7 +18,6 @@ class Icon extends PureComponent {
kind,
size,
title,
darken,
isSpinning,
...otherProps
} = this.props;
@@ -27,8 +26,7 @@ class Icon extends PureComponent {
<FontAwesomeIcon
className={classNames(
className,
styles[kind],
darken && 'darken'
styles[kind]
)}
icon={name}
spin={isSpinning}
@@ -61,7 +59,6 @@ Icon.propTypes = {
kind: PropTypes.string.isRequired,
size: PropTypes.number.isRequired,
title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
darken: PropTypes.bool.isRequired,
isSpinning: PropTypes.bool.isRequired,
fixedWidth: PropTypes.bool.isRequired
};
@@ -69,7 +66,6 @@ Icon.propTypes = {
Icon.defaultProps = {
kind: kinds.DEFAULT,
size: 14,
darken: false,
isSpinning: false,
fixedWidth: false
};

View File

@@ -40,18 +40,26 @@ class FilterMenuContent extends Component {
}
{
customFilters.map((filter) => {
return (
<FilterMenuItem
key={filter.id}
filterKey={filter.id}
selectedFilterKey={selectedFilterKey}
onPress={onFilterSelect}
>
{filter.label}
</FilterMenuItem>
);
})
customFilters.length > 0 ?
<MenuItemSeparator /> :
null
}
{
customFilters
.sort((a, b) => a.label.localeCompare(b.label))
.map((filter) => {
return (
<FilterMenuItem
key={filter.id}
filterKey={filter.id}
selectedFilterKey={selectedFilterKey}
onPress={onFilterSelect}
>
{filter.label}
</FilterMenuItem>
);
})
}
{

View File

@@ -167,7 +167,7 @@ class SignalRConnector extends Component {
const resource = body.resource;
const status = resource.status;
// Both sucessful and failed commands need to be
// Both successful and failed commands need to be
// completed, otherwise they spin until they timeout.
if (status === 'completed' || status === 'failed') {
@@ -187,6 +187,8 @@ class SignalRConnector extends Component {
repopulatePage('movieFileUpdated');
} else if (body.action === 'deleted') {
this.props.dispatchRemoveItem({ section, id: body.resource.id });
repopulatePage('movieFileDeleted');
}
};

View File

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

View File

@@ -59,6 +59,7 @@ import {
faEye as fasEye,
faFastBackward as fasFastBackward,
faFastForward as fasFastForward,
faFileCircleQuestion as fasFileCircleQuestion,
faFileExport as fasFileExport,
faFileInvoice as farFileInvoice,
faFilm as fasFilm,
@@ -159,6 +160,7 @@ export const EXPORT = fasFileExport;
export const EXTERNAL_LINK = fasExternalLinkAlt;
export const FATAL = fasTimesCircle;
export const FILE = farFile;
export const FILE_MISSING = fasFileCircleQuestion;
export const FILM = fasFilm;
export const FILTER = fasFilter;
export const FLAG = fasFlag;

View File

@@ -22,6 +22,10 @@
composes: cell from '~Components/Table/Cells/TableRowCell.css';
}
.quality {
white-space: nowrap;
}
.languages {
width: 100px;
}

View File

@@ -266,7 +266,7 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
</TableRowCell>
<TableRowCell className={styles.quality}>
<MovieQuality quality={quality} />
<MovieQuality quality={quality} showRevision={true} />
</TableRowCell>
<TableRowCell className={styles.customFormatScore}>

View File

@@ -50,12 +50,16 @@ class DeleteMovieModalContent extends Component {
title,
path,
hasFile,
statistics,
deleteOptions,
sizeOnDisk,
onModalClose,
onDeleteOptionChange
} = this.props;
const {
sizeOnDisk = 0
} = statistics;
const deleteFiles = this.state.deleteFiles;
const addImportExclusion = deleteOptions.addImportExclusion;
@@ -151,12 +155,16 @@ class DeleteMovieModalContent extends Component {
DeleteMovieModalContent.propTypes = {
title: PropTypes.string.isRequired,
path: PropTypes.string.isRequired,
statistics: PropTypes.object.isRequired,
hasFile: PropTypes.bool.isRequired,
sizeOnDisk: PropTypes.number.isRequired,
deleteOptions: PropTypes.object.isRequired,
onDeleteOptionChange: PropTypes.func.isRequired,
onDeletePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
DeleteMovieModalContent.defaultProps = {
statistics: {}
};
export default DeleteMovieModalContent;

View File

@@ -5,6 +5,29 @@ import { createSelector } from 'reselect';
import MovieCreditPosters from '../MovieCreditPosters';
import MovieCrewPoster from './MovieCrewPoster';
function crewSort(a, b) {
const jobOrder = ['Director', 'Writer', 'Producer', 'Executive Producer', 'Director of Photography'];
const indexA = jobOrder.indexOf(a.job);
const indexB = jobOrder.indexOf(b.job);
if (indexA === -1 && indexB === -1) {
return 0;
} else if (indexA === -1) {
return 1;
} else if (indexB === -1) {
return -1;
}
if (indexA < indexB) {
return -1;
} else if (indexA > indexB) {
return 1;
}
return 0;
}
function createMapStateToProps() {
return createSelector(
(state) => state.movieCredits.items,
@@ -17,8 +40,10 @@ function createMapStateToProps() {
return acc;
}, []);
const sortedCrew = crew.sort(crewSort);
return {
items: _.uniqBy(crew, 'personName')
items: _.uniqBy(sortedCrew, 'personName')
};
}
);

View File

@@ -9,3 +9,9 @@
.container {
padding: 10px;
}
.sliderContainer {
--swiper-navigation-color: var(--white);
display: block;
}

View File

@@ -4,6 +4,7 @@ interface CssExports {
'container': string;
'grid': string;
'movie': string;
'sliderContainer': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -14,24 +14,6 @@ import 'swiper/css/navigation';
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
const additionalColumnCount = {
small: 3,
medium: 2,
large: 1
};
function calculateColumnWidth(width, posterSize, isSmallScreen) {
const maxiumColumnWidth = isSmallScreen ? 172 : 182;
const columns = Math.floor(width / maxiumColumnWidth);
const remainder = width % maxiumColumnWidth;
if (remainder === 0 && posterSize === 'large') {
return maxiumColumnWidth;
}
return Math.floor(width / (columns + additionalColumnCount[posterSize]));
}
function calculateRowHeight(posterHeight, isSmallScreen) {
const titleHeight = 19;
const characterHeight = 19;
@@ -46,10 +28,6 @@ function calculateRowHeight(posterHeight, isSmallScreen) {
return heights.reduce((acc, height) => acc + height, 0);
}
function calculatePosterHeight(posterWidth) {
return Math.ceil((250 / 170) * posterWidth);
}
class MovieCreditPosters extends Component {
//
@@ -66,39 +44,16 @@ class MovieCreditPosters extends Component {
posterHeight: 238,
rowHeight: calculateRowHeight(238, props.isSmallScreen)
};
this._isInitialized = false;
}
//
// Control
calculateGrid = (width = this.state.width, isSmallScreen) => {
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
const columnWidth = calculateColumnWidth(width, 'small', isSmallScreen);
const columnCount = Math.max(Math.floor(width / columnWidth), 1);
const posterWidth = columnWidth - padding;
const posterHeight = calculatePosterHeight(posterWidth);
const rowHeight = calculateRowHeight(posterHeight, isSmallScreen);
this.setState({
width,
columnWidth,
columnCount,
posterWidth,
posterHeight,
rowHeight
});
};
//
// Render
render() {
const {
items,
itemComponent
itemComponent,
isSmallScreen
} = this.props;
const {
@@ -113,14 +68,13 @@ class MovieCreditPosters extends Component {
<Swiper
slidesPerView='auto'
spaceBetween={10}
slidesPerGroup={3}
slidesPerGroup={isSmallScreen ? 1 : 3}
navigation={true}
loop={false}
loopFillGroupWithBlank={true}
className="mySwiper"
modules={[Navigation]}
onInit={(swiper) => {
swiper.params.navigation.prevEl = this._swiperPrevRef;
swiper.params.navigation.nextEl = this._swiperNextRef;
swiper.navigation.init();
swiper.navigation.update();
}}

View File

@@ -238,7 +238,7 @@ class MovieDetails extends Component {
certification,
ratings,
path,
sizeOnDisk,
statistics,
qualityProfileId,
monitored,
studio,
@@ -267,6 +267,10 @@ class MovieDetails extends Component {
movieRuntimeFormat
} = this.props;
const {
sizeOnDisk = 0
} = statistics;
const {
isOrganizeModalOpen,
isEditMovieModalOpen,
@@ -320,8 +324,8 @@ class MovieDetails extends Component {
/>
<PageToolbarButton
label={translate('ManualImport')}
iconName={icons.INTERACTIVE}
label={translate('ManageFiles')}
iconName={icons.MOVIE_FILE}
onPress={this.onInteractiveImportPress}
/>
@@ -704,6 +708,7 @@ class MovieDetails extends Component {
<InteractiveImportModal
isOpen={isInteractiveImportModalOpen}
movieId={id}
modalTitle={translate('ManageFiles')}
folder={path}
allowMovieChange={false}
showFilterExistingFiles={true}
@@ -733,7 +738,7 @@ MovieDetails.propTypes = {
certification: PropTypes.string,
ratings: PropTypes.object.isRequired,
path: PropTypes.string.isRequired,
sizeOnDisk: PropTypes.number.isRequired,
statistics: PropTypes.object.isRequired,
qualityProfileId: PropTypes.number.isRequired,
monitored: PropTypes.bool.isRequired,
status: PropTypes.string.isRequired,
@@ -772,9 +777,9 @@ MovieDetails.propTypes = {
MovieDetails.defaultProps = {
genres: [],
statistics: {},
tags: [],
isSaving: false,
sizeOnDisk: 0
isSaving: false
};
export default MovieDetails;

View File

@@ -33,14 +33,11 @@ const selectMovieFiles = createSelector(
const hasMovieFiles = !!items.length;
const sizeOnDisk = items.map((item) => item.size).reduce((prev, curr) => prev + curr, 0);
return {
isMovieFilesFetching: isFetching,
isMovieFilesPopulated: isPopulated,
movieFilesError: error,
hasMovieFiles,
sizeOnDisk
hasMovieFiles
};
}
);
@@ -104,8 +101,7 @@ function createMapStateToProps() {
isMovieFilesFetching,
isMovieFilesPopulated,
movieFilesError,
hasMovieFiles,
sizeOnDisk
hasMovieFiles
} = movieFiles;
const {
@@ -161,7 +157,6 @@ function createMapStateToProps() {
movieCreditsError,
extraFilesError,
hasMovieFiles,
sizeOnDisk,
previousMovie,
nextMovie,
isSmallScreen: dimensions.isSmallScreen,

View File

@@ -6,31 +6,43 @@ import MovieTitlesTableContent from './MovieTitlesTableContent';
function createMapStateToProps() {
return createSelector(
(state, { movieId }) => movieId,
(state) => state.movies,
(movies) => {
return movies;
(movieId, movies) => {
const {
isFetching,
isPopulated,
error,
items
} = movies;
const alternateTitles = items.find((m) => m.id === movieId)?.alternateTitles;
return {
isFetching,
isPopulated,
error,
alternateTitles
};
}
);
}
const mapDispatchToProps = {
// fetchMovies
};
class MovieTitlesTableContentConnector extends Component {
//
// Render
render() {
const movie = this.props.items.filter((obj) => {
return obj.id === this.props.movieId;
});
const {
alternateTitles,
...otherProps
} = this.props;
return (
<MovieTitlesTableContent
{...this.props}
items={movie[0].alternateTitles}
{...otherProps}
items={alternateTitles}
/>
);
}
@@ -38,7 +50,11 @@ class MovieTitlesTableContentConnector extends Component {
MovieTitlesTableContentConnector.propTypes = {
movieId: PropTypes.number.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired
alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(MovieTitlesTableContentConnector);
MovieTitlesTableContentConnector.defaultProps = {
alternateTitles: []
};
export default connect(createMapStateToProps)(MovieTitlesTableContentConnector);

View File

@@ -56,7 +56,6 @@ const columns = [
},
{
name: 'actions',
label: () => translate('Actions'),
isVisible: true
}
];

View File

@@ -7,8 +7,7 @@ import ConfirmModal from 'Components/Modal/ConfirmModal';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import { icons, kinds } from 'Helpers/Props';
import MovieFormats from 'Movie/MovieFormats';
import MovieLanguage from 'Movie/MovieLanguage';
import MovieQuality from 'Movie/MovieQuality';
@@ -103,20 +102,11 @@ class MovieHistoryRow extends Component {
</TableRowCell>
<TableRowCell>
<MovieFormats
formats={customFormats}
/>
<MovieFormats formats={customFormats} />
</TableRowCell>
<TableRowCell className={styles.customFormatScore}>
<Tooltip
anchor={formatCustomFormatScore(
customFormatScore,
customFormats.length
)}
tooltip={<MovieFormats formats={customFormats} />}
position={tooltipPositions.TOP}
/>
{formatCustomFormatScore(customFormatScore, customFormats.length)}
</TableRowCell>
<RelativeDateCellConnector
@@ -134,6 +124,7 @@ class MovieHistoryRow extends Component {
<IconButton
title={translate('MarkAsFailed')}
name={icons.REMOVE}
size={14}
onPress={this.onMarkAsFailedPress}
/>
}

View File

@@ -17,13 +17,13 @@ function createUnoptimizedSelector() {
createClientSideCollectionSelector('movies', 'movieIndex'),
(movies: MoviesAppState) => {
return movies.items.map((m) => {
const { monitored, status, hasFile, sizeOnDisk } = m;
const { monitored, status, hasFile, statistics } = m;
return {
monitored,
status,
hasFile,
sizeOnDisk,
statistics,
};
});
}
@@ -44,16 +44,20 @@ export default function MovieIndexFooter() {
let monitored = 0;
let totalFileSize = 0;
movies.forEach((s) => {
if (s.hasFile) {
movies.forEach((m) => {
const { statistics = { sizeOnDisk: 0 } } = m;
const { sizeOnDisk = 0 } = statistics;
if (m.hasFile) {
movieFiles += 1;
}
if (s.monitored) {
if (m.monitored) {
monitored++;
}
totalFileSize += s.sizeOnDisk;
totalFileSize += sizeOnDisk;
});
return (

View File

@@ -1,11 +1,12 @@
import React, { useCallback, useMemo } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useSelect } from 'App/SelectContext';
import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState';
import MoviesAppState, { MovieIndexAppState } from 'App/State/MoviesAppState';
import { MOVIE_SEARCH } from 'Commands/commandNames';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import { icons } from 'Helpers/Props';
import { icons, kinds } from 'Helpers/Props';
import { executeCommand } from 'Store/Actions/commandActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import createMovieClientSideCollectionItemsSelector from 'Store/Selectors/createMovieClientSideCollectionItemsSelector';
@@ -21,11 +22,12 @@ function MovieIndexSearchButton(props: MovieIndexSearchButtonProps) {
const isSearching = useSelector(createCommandExecutingSelector(MOVIE_SEARCH));
const {
items,
totalItems,
}: MoviesAppState & MovieIndexAppState & ClientSideCollectionAppState =
useSelector(createMovieClientSideCollectionItemsSelector('movieIndex'));
const dispatch = useDispatch();
const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false);
const { isSelectMode, selectedFilterKey } = props;
const [selectState] = useSelect();
const { selectedState } = selectState;
@@ -50,6 +52,8 @@ function MovieIndexSearchButton(props: MovieIndexSearchButtonProps) {
: translate('SearchAll');
const onPress = useCallback(() => {
setIsConfirmModalOpen(false);
dispatch(
executeCommand({
name: MOVIE_SEARCH,
@@ -58,14 +62,36 @@ function MovieIndexSearchButton(props: MovieIndexSearchButtonProps) {
);
}, [dispatch, moviesToSearch]);
const onConfirmPress = useCallback(() => {
setIsConfirmModalOpen(true);
}, [setIsConfirmModalOpen]);
const onConfirmModalClose = useCallback(() => {
setIsConfirmModalOpen(false);
}, [setIsConfirmModalOpen]);
return (
<PageToolbarButton
label={isSelectMode ? searchSelectLabel : searchIndexLabel}
isSpinning={isSearching}
isDisabled={!totalItems}
iconName={icons.SEARCH}
onPress={onPress}
/>
<>
<PageToolbarButton
label={isSelectMode ? searchSelectLabel : searchIndexLabel}
isSpinning={isSearching}
isDisabled={!items.length}
iconName={icons.SEARCH}
onPress={moviesToSearch.length > 5 ? onConfirmPress : onPress}
/>
<ConfirmModal
isOpen={isConfirmModalOpen}
kind={kinds.DANGER}
title={isSelectMode ? searchSelectLabel : searchIndexLabel}
message={translate('SearchMoviesConfirmationMessageText', {
count: moviesToSearch.length,
})}
confirmLabel={isSelectMode ? searchSelectLabel : searchIndexLabel}
onConfirm={onPress}
onCancel={onConfirmModalClose}
/>
</>
);
}

View File

@@ -13,6 +13,7 @@ import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar';
import MovieIndexPosterSelect from 'Movie/Index/Select/MovieIndexPosterSelect';
import { Statistics } from 'Movie/Movie';
import MoviePoster from 'Movie/MoviePoster';
import { executeCommand } from 'Store/Actions/commandActions';
import dimensions from 'Styles/Variables/dimensions';
@@ -66,17 +67,19 @@ function MovieIndexOverview(props: MovieIndexOverviewProps) {
status,
path,
overview,
statistics = {} as Statistics,
images,
hasFile,
isAvailable,
tmdbId,
imdbId,
studio,
sizeOnDisk,
added,
youTubeTrailerId,
} = movie;
const { sizeOnDisk = 0 } = statistics;
const dispatch = useDispatch();
const [isEditMovieModalOpen, setIsEditMovieModalOpen] = useState(false);
const [isDeleteMovieModalOpen, setIsDeleteMovieModalOpen] = useState(false);

View File

@@ -16,6 +16,7 @@ import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar';
import MovieIndexPosterSelect from 'Movie/Index/Select/MovieIndexPosterSelect';
import { Statistics } from 'Movie/Movie';
import MoviePoster from 'Movie/MoviePoster';
import { executeCommand } from 'Store/Actions/commandActions';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
@@ -75,12 +76,14 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
path,
movieFile,
ratings,
sizeOnDisk,
statistics = {} as Statistics,
certification,
originalTitle,
originalLanguage,
} = movie;
const { sizeOnDisk = 0 } = statistics;
const dispatch = useDispatch();
const [hasPosterError, setHasPosterError] = useState(false);
const [isEditMovieModalOpen, setIsEditMovieModalOpen] = useState(false);

View File

@@ -244,11 +244,15 @@ export default function MovieIndexPosters(props: MovieIndexPostersProps) {
if (isSmallScreen) {
const padding = bodyPaddingSmallScreen - 5;
const width = window.innerWidth - padding * 2;
const height = window.innerHeight;
setSize({
width: window.innerWidth - padding * 2,
height: window.innerHeight,
});
if (width !== size.width || height !== size.height) {
setSize({
width,
height,
});
}
return;
}
@@ -256,13 +260,18 @@ export default function MovieIndexPosters(props: MovieIndexPostersProps) {
if (current) {
const width = current.clientWidth;
const padding = bodyPadding - 5;
const finalWidth = width - padding * 2;
if (Math.abs(size.width - finalWidth) < 20 || size.width === finalWidth) {
return;
}
setSize({
width: width - padding * 2,
width: finalWidth,
height: window.innerHeight,
});
}
}, [isSmallScreen, scrollerRef, bounds]);
}, [isSmallScreen, size, scrollerRef, bounds]);
useEffect(() => {
const currentScrollerRef = scrollerRef.current as HTMLElement;

View File

@@ -38,6 +38,7 @@
flex: 1 0 125px;
}
.releaseGroups,
.inCinemas,
.physicalRelease,
.digitalRelease,

View File

@@ -20,6 +20,7 @@ interface CssExports {
'physicalRelease': string;
'popularity': string;
'qualityProfileId': string;
'releaseGroups': string;
'rottenTomatoesRating': string;
'runtime': string;
'sizeOnDisk': string;

View File

@@ -19,6 +19,7 @@ import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
import createMovieIndexItemSelector from 'Movie/Index/createMovieIndexItemSelector';
import { Statistics } from 'Movie/Movie';
import MoviePopularityIndex from 'Movie/MoviePopularityIndex';
import MovieTitleLink from 'Movie/MovieTitleLink';
import { executeCommand } from 'Store/Actions/commandActions';
@@ -60,6 +61,7 @@ function MovieIndexRow(props: MovieIndexRowProps) {
originalLanguage,
originalTitle,
added,
statistics = {} as Statistics,
year,
inCinemas,
digitalRelease,
@@ -67,7 +69,6 @@ function MovieIndexRow(props: MovieIndexRowProps) {
runtime,
minimumAvailability,
path,
sizeOnDisk,
genres = [],
ratings,
popularity,
@@ -82,6 +83,8 @@ function MovieIndexRow(props: MovieIndexRowProps) {
isSaving = false,
} = movie;
const { sizeOnDisk = 0, releaseGroups = [] } = statistics;
const dispatch = useDispatch();
const [isEditMovieModalOpen, setIsEditMovieModalOpen] = useState(false);
const [isDeleteMovieModalOpen, setIsDeleteMovieModalOpen] = useState(false);
@@ -380,6 +383,20 @@ function MovieIndexRow(props: MovieIndexRowProps) {
);
}
if (name === 'releaseGroups') {
const joinedReleaseGroups = releaseGroups.join(', ');
const truncatedReleaseGroups =
releaseGroups.length > 3
? `${releaseGroups.slice(0, 3).join(', ')}...`
: joinedReleaseGroups;
return (
<VirtualTableRowCell key={name} className={styles[name]}>
<span title={joinedReleaseGroups}>{truncatedReleaseGroups}</span>
</VirtualTableRowCell>
);
}
if (name === 'tags') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>

View File

@@ -31,6 +31,7 @@
flex: 1 0 125px;
}
.releaseGroups,
.inCinemas,
.physicalRelease,
.digitalRelease,

View File

@@ -17,6 +17,7 @@ interface CssExports {
'physicalRelease': string;
'popularity': string;
'qualityProfileId': string;
'releaseGroups': string;
'rottenTomatoesRating': string;
'runtime': string;
'sizeOnDisk': string;

View File

@@ -12,6 +12,12 @@ export interface Collection {
title: string;
}
export interface Statistics {
movieFileCount: number;
releaseGroups: string[];
sizeOnDisk: number;
}
export interface Ratings {
imdb: object;
tmdb: object;
@@ -42,11 +48,11 @@ interface Movie extends ModelBase {
runtime: number;
minimumAvailability: string;
path: string;
sizeOnDisk: number;
genres: string[];
ratings: Ratings;
popularity: number;
certification: string;
statistics: Statistics;
tags: number[];
images: Image[];
movieFile: MovieFile;

View File

@@ -3,6 +3,7 @@ import React from 'react';
import Label from 'Components/Label';
import { kinds } from 'Helpers/Props';
import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
function getTooltip(title, quality, size, isMonitored, isCutoffNotMet) {
const revision = quality.revision;
@@ -28,6 +29,36 @@ function getTooltip(title, quality, size, isMonitored, isCutoffNotMet) {
return title;
}
function revisionLabel(className, quality, showRevision) {
if (!showRevision) {
return;
}
if (quality.revision.isRepack) {
return (
<Label
className={className}
kind={kinds.PRIMARY}
title={translate('Repack')}
>
R
</Label>
);
}
if (quality.revision.version && quality.revision.version > 1) {
return (
<Label
className={className}
kind={kinds.PRIMARY}
title={translate('Proper')}
>
P
</Label>
);
}
}
function MovieQuality(props) {
const {
className,
@@ -35,7 +66,8 @@ function MovieQuality(props) {
quality,
size,
isMonitored,
isCutoffNotMet
isCutoffNotMet,
showRevision
} = props;
let kind = kinds.DEFAULT;
@@ -50,13 +82,15 @@ function MovieQuality(props) {
}
return (
<Label
className={className}
kind={kind}
title={getTooltip(title, quality, size, isMonitored, isCutoffNotMet)}
>
{quality.quality.name}
</Label>
<span>
<Label
className={className}
kind={kind}
title={getTooltip(title, quality, size, isMonitored, isCutoffNotMet)}
>
{quality.quality.name}
</Label>{revisionLabel(className, quality, showRevision)}
</span>
);
}
@@ -66,12 +100,14 @@ MovieQuality.propTypes = {
quality: PropTypes.object.isRequired,
size: PropTypes.number,
isMonitored: PropTypes.bool,
isCutoffNotMet: PropTypes.bool
isCutoffNotMet: PropTypes.bool,
showRevision: PropTypes.bool
};
MovieQuality.defaultProps = {
title: '',
isMonitored: true
isMonitored: true,
showRevision: false
};
export default MovieQuality;

View File

@@ -1,82 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import { icons, kinds } from 'Helpers/Props';
import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
import styles from './RootFolderRow.css';
function RootFolderRow(props) {
const {
id,
path,
accessible,
freeSpace,
unmappedFolders,
onDeletePress
} = props;
const isUnavailable = !accessible;
return (
<TableRow>
<TableRowCell>
{
isUnavailable ?
<div className={styles.unavailablePath}>
{path}
<Label
className={styles.unavailableLabel}
kind={kinds.DANGER}
>
{translate('Unavailable')}
</Label>
</div> :
<Link
className={styles.link}
to={`/add/import/${id}`}
>
{path}
</Link>
}
</TableRowCell>
<TableRowCell className={styles.freeSpace}>
{(isUnavailable || isNaN(freeSpace)) ? '-' : formatBytes(freeSpace)}
</TableRowCell>
<TableRowCell className={styles.unmappedFolders}>
{isUnavailable ? '-' : unmappedFolders.length}
</TableRowCell>
<TableRowCell className={styles.actions}>
<IconButton
title={translate('RemoveRootFolder')}
name={icons.REMOVE}
onPress={onDeletePress}
/>
</TableRowCell>
</TableRow>
);
}
RootFolderRow.propTypes = {
id: PropTypes.number.isRequired,
path: PropTypes.string.isRequired,
accessible: PropTypes.bool.isRequired,
freeSpace: PropTypes.number,
unmappedFolders: PropTypes.arrayOf(PropTypes.object).isRequired,
onDeletePress: PropTypes.func.isRequired
};
RootFolderRow.defaultProps = {
unmappedFolders: []
};
export default RootFolderRow;

View File

@@ -1,92 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import RootFolderRowConnector from './RootFolderRowConnector';
const rootFolderColumns = [
{
name: 'path',
get label() {
return translate('Path');
},
isVisible: true
},
{
name: 'freeSpace',
get label() {
return translate('FreeSpace');
},
isVisible: true
},
{
name: 'unmappedFolders',
get label() {
return translate('UnmappedFolders');
},
isVisible: true
},
{
name: 'actions',
isVisible: true
}
];
function RootFolders(props) {
const {
isFetching,
isPopulated,
error,
items
} = props;
if (isFetching && !isPopulated) {
return (
<LoadingIndicator />
);
}
if (!isFetching && !!error) {
return (
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadRootFolders')}
</Alert>
);
}
return (
<Table
columns={rootFolderColumns}
>
<TableBody>
{
items.map((rootFolder) => {
return (
<RootFolderRowConnector
key={rootFolder.id}
id={rootFolder.id}
path={rootFolder.path}
accessible={rootFolder.accessible}
freeSpace={rootFolder.freeSpace}
unmappedFolders={rootFolder.unmappedFolders}
/>
);
})
}
</TableBody>
</Table>
);
}
RootFolders.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired
};
export default RootFolders;

View File

@@ -49,7 +49,7 @@ function RootFolders() {
if (!isFetching && !!error) {
return (
<Alert kind={kinds.DANGER}>{translate('UnableToLoadRootFolders')}</Alert>
<Alert kind={kinds.DANGER}>{translate('RootFoldersLoadError')}</Alert>
);
}

View File

@@ -133,7 +133,7 @@ class EditDownloadClientModalContent extends Component {
<FormInputGroup
type={inputTypes.NUMBER}
name="priority"
helpText={translate('PriorityHelpText')}
helpText={translate('DownloadClientPriorityHelpText')}
min={1}
max={50}
{...priority}

View File

@@ -14,9 +14,11 @@ import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import useSelectState from 'Helpers/Hooks/useSelectState';
import { kinds } from 'Helpers/Props';
import SortDirection from 'Helpers/Props/SortDirection';
import {
bulkDeleteDownloadClients,
bulkEditDownloadClients,
setManageDownloadClientsSort,
} from 'Store/Actions/settingsActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import { SelectStateInputProps } from 'typings/props';
@@ -80,6 +82,8 @@ const COLUMNS = [
interface ManageDownloadClientsModalContentProps {
onModalClose(): void;
sortKey?: string;
sortDirection?: SortDirection;
}
function ManageDownloadClientsModalContent(
@@ -94,6 +98,8 @@ function ManageDownloadClientsModalContent(
isSaving,
error,
items,
sortKey,
sortDirection,
}: DownloadClientAppState = useSelector(
createClientSideCollectionSelector('settings.downloadClients')
);
@@ -114,6 +120,13 @@ function ManageDownloadClientsModalContent(
const selectedCount = selectedIds.length;
const onSortPress = useCallback(
(value: string) => {
dispatch(setManageDownloadClientsSort({ sortKey: value }));
},
[dispatch]
);
const onDeletePress = useCallback(() => {
setIsDeleteModalOpen(true);
}, [setIsDeleteModalOpen]);
@@ -219,6 +232,9 @@ function ManageDownloadClientsModalContent(
allSelected={allSelected}
allUnselected={allUnselected}
onSelectAllChange={onSelectAllChange}
sortKey={sortKey}
sortDirection={sortDirection}
onSortPress={onSortPress}
>
<TableBody>
{items.map((item) => {

View File

@@ -14,9 +14,11 @@ import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import useSelectState from 'Helpers/Hooks/useSelectState';
import { kinds } from 'Helpers/Props';
import SortDirection from 'Helpers/Props/SortDirection';
import {
bulkDeleteIndexers,
bulkEditIndexers,
setManageIndexersSort,
} from 'Store/Actions/settingsActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import { SelectStateInputProps } from 'typings/props';
@@ -80,6 +82,8 @@ const COLUMNS = [
interface ManageIndexersModalContentProps {
onModalClose(): void;
sortKey?: string;
sortDirection?: SortDirection;
}
function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
@@ -92,6 +96,8 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
isSaving,
error,
items,
sortKey,
sortDirection,
}: IndexerAppState = useSelector(
createClientSideCollectionSelector('settings.indexers')
);
@@ -112,6 +118,13 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
const selectedCount = selectedIds.length;
const onSortPress = useCallback(
(value: string) => {
dispatch(setManageIndexersSort({ sortKey: value }));
},
[dispatch]
);
const onDeletePress = useCallback(() => {
setIsDeleteModalOpen(true);
}, [setIsDeleteModalOpen]);
@@ -214,6 +227,9 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
allSelected={allSelected}
allUnselected={allUnselected}
onSelectAllChange={onSelectAllChange}
sortKey={sortKey}
sortDirection={sortDirection}
onSortPress={onSortPress}
>
<TableBody>
{items.map((item) => {

View File

@@ -1,72 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import { icons, kinds, sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './AddRootFolder.css';
class AddRootFolder extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isAddNewRootFolderModalOpen: false
};
}
//
// Lifecycle
onAddNewRootFolderPress = () => {
this.setState({ isAddNewRootFolderModalOpen: true });
};
onNewRootFolderSelect = ({ value }) => {
this.props.onNewRootFolderSelect(value);
};
onAddRootFolderModalClose = () => {
this.setState({ isAddNewRootFolderModalOpen: false });
};
//
// Render
render() {
return (
<div className={styles.addRootFolderButtonContainer}>
<Button
kind={kinds.PRIMARY}
size={sizes.LARGE}
onPress={this.onAddNewRootFolderPress}
>
<Icon
className={styles.importButtonIcon}
name={icons.DRIVE}
/>
{translate('AddRootFolder')}
</Button>
<FileBrowserModal
isOpen={this.state.isAddNewRootFolderModalOpen}
name="rootFolderPath"
value=""
onChange={this.onNewRootFolderSelect}
onModalClose={this.onAddRootFolderModalClose}
/>
</div>
);
}
}
AddRootFolder.propTypes = {
onNewRootFolderSelect: PropTypes.func.isRequired
};
export default AddRootFolder;

View File

@@ -1,14 +1,18 @@
import React, { useCallback, useState } from 'react';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import Alert from 'Components/Alert';
import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import { icons, kinds, sizes } from 'Helpers/Props';
import { addRootFolder } from 'Store/Actions/rootFolderActions';
import createRootFoldersSelector from 'Store/Selectors/createRootFoldersSelector';
import translate from 'Utilities/String/translate';
import styles from './AddRootFolder.css';
function AddRootFolder() {
const { isSaving, saveError } = useSelector(createRootFoldersSelector());
const dispatch = useDispatch();
const [isAddNewRootFolderModalOpen, setIsAddNewRootFolderModalOpen] =
@@ -30,24 +34,42 @@ function AddRootFolder() {
}, [setIsAddNewRootFolderModalOpen]);
return (
<div className={styles.addRootFolderButtonContainer}>
<Button
kind={kinds.PRIMARY}
size={sizes.LARGE}
onPress={onAddNewRootFolderPress}
>
<Icon className={styles.importButtonIcon} name={icons.DRIVE} />
{translate('AddRootFolder')}
</Button>
<>
{!isSaving && saveError ? (
<Alert kind={kinds.DANGER}>
{translate('AddRootFolderError')}
<FileBrowserModal
isOpen={isAddNewRootFolderModalOpen}
name="rootFolderPath"
value=""
onChange={onNewRootFolderSelect}
onModalClose={onAddRootFolderModalClose}
/>
</div>
<ul>
{Array.isArray(saveError.responseJSON) ? (
saveError.responseJSON.map((e, index) => {
return <li key={index}>{e.errorMessage}</li>;
})
) : (
<li>{JSON.stringify(saveError.responseJSON)}</li>
)}
</ul>
</Alert>
) : null}
<div className={styles.addRootFolderButtonContainer}>
<Button
kind={kinds.PRIMARY}
size={sizes.LARGE}
onPress={onAddNewRootFolderPress}
>
<Icon className={styles.importButtonIcon} name={icons.DRIVE} />
{translate('AddRootFolder')}
</Button>
<FileBrowserModal
isOpen={isAddNewRootFolderModalOpen}
name="rootFolderPath"
value=""
onChange={onNewRootFolderSelect}
onModalClose={onAddRootFolderModalClose}
/>
</div>
</>
);
}

View File

@@ -1,4 +1,5 @@
import { createAction } from 'redux-actions';
import { sortDirections } from 'Helpers/Props';
import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler';
import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler';
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
@@ -7,6 +8,7 @@ import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHand
import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler';
import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler';
import createSetClientSideCollectionSortReducer from 'Store/Actions/Creators/Reducers/createSetClientSideCollectionSortReducer';
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import { createThunk } from 'Store/thunks';
@@ -31,9 +33,9 @@ export const DELETE_DOWNLOAD_CLIENT = 'settings/downloadClients/deleteDownloadCl
export const TEST_DOWNLOAD_CLIENT = 'settings/downloadClients/testDownloadClient';
export const CANCEL_TEST_DOWNLOAD_CLIENT = 'settings/downloadClients/cancelTestDownloadClient';
export const TEST_ALL_DOWNLOAD_CLIENTS = 'settings/downloadClients/testAllDownloadClients';
export const BULK_DELETE_DOWNLOAD_CLIENTS = 'settings/downloadClients/bulkDeleteDownloadClients';
export const BULK_EDIT_DOWNLOAD_CLIENTS = 'settings/downloadClients/bulkEditDownloadClients';
export const BULK_DELETE_DOWNLOAD_CLIENTS = 'settings/downloadClients/bulkDeleteDownloadClients';
export const SET_MANAGE_DOWNLOAD_CLIENTS_SORT = 'settings/downloadClients/setManageDownloadClientsSort';
//
// Action Creators
@@ -48,9 +50,9 @@ export const deleteDownloadClient = createThunk(DELETE_DOWNLOAD_CLIENT);
export const testDownloadClient = createThunk(TEST_DOWNLOAD_CLIENT);
export const cancelTestDownloadClient = createThunk(CANCEL_TEST_DOWNLOAD_CLIENT);
export const testAllDownloadClients = createThunk(TEST_ALL_DOWNLOAD_CLIENTS);
export const bulkDeleteDownloadClients = createThunk(BULK_DELETE_DOWNLOAD_CLIENTS);
export const bulkEditDownloadClients = createThunk(BULK_EDIT_DOWNLOAD_CLIENTS);
export const bulkDeleteDownloadClients = createThunk(BULK_DELETE_DOWNLOAD_CLIENTS);
export const setManageDownloadClientsSort = createAction(SET_MANAGE_DOWNLOAD_CLIENTS_SORT);
export const setDownloadClientValue = createAction(SET_DOWNLOAD_CLIENT_VALUE, (payload) => {
return {
@@ -90,7 +92,9 @@ export default {
isTesting: false,
isTestingAll: false,
items: [],
pendingChanges: {}
pendingChanges: {},
sortKey: 'name',
sortDirection: sortDirections.DESCENDING
},
//
@@ -124,7 +128,10 @@ export default {
return selectedSchema;
});
}
},
[SET_MANAGE_DOWNLOAD_CLIENTS_SORT]: createSetClientSideCollectionSortReducer(section)
}
};

View File

@@ -1,4 +1,5 @@
import { createAction } from 'redux-actions';
import { sortDirections } from 'Helpers/Props';
import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler';
import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler';
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
@@ -7,6 +8,7 @@ import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHand
import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler';
import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler';
import createSetClientSideCollectionSortReducer from 'Store/Actions/Creators/Reducers/createSetClientSideCollectionSortReducer';
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import { createThunk } from 'Store/thunks';
@@ -35,9 +37,9 @@ export const DELETE_INDEXER = 'settings/indexers/deleteIndexer';
export const TEST_INDEXER = 'settings/indexers/testIndexer';
export const CANCEL_TEST_INDEXER = 'settings/indexers/cancelTestIndexer';
export const TEST_ALL_INDEXERS = 'settings/indexers/testAllIndexers';
export const BULK_DELETE_INDEXERS = 'settings/indexers/bulkDeleteIndexers';
export const BULK_EDIT_INDEXERS = 'settings/indexers/bulkEditIndexers';
export const BULK_DELETE_INDEXERS = 'settings/indexers/bulkDeleteIndexers';
export const SET_MANAGE_INDEXERS_SORT = 'settings/indexers/setManageIndexersSort';
//
// Action Creators
@@ -53,9 +55,9 @@ export const deleteIndexer = createThunk(DELETE_INDEXER);
export const testIndexer = createThunk(TEST_INDEXER);
export const cancelTestIndexer = createThunk(CANCEL_TEST_INDEXER);
export const testAllIndexers = createThunk(TEST_ALL_INDEXERS);
export const bulkDeleteIndexers = createThunk(BULK_DELETE_INDEXERS);
export const bulkEditIndexers = createThunk(BULK_EDIT_INDEXERS);
export const bulkDeleteIndexers = createThunk(BULK_DELETE_INDEXERS);
export const setManageIndexersSort = createAction(SET_MANAGE_INDEXERS_SORT);
export const setIndexerValue = createAction(SET_INDEXER_VALUE, (payload) => {
return {
@@ -95,7 +97,9 @@ export default {
isTesting: false,
isTestingAll: false,
items: [],
pendingChanges: {}
pendingChanges: {},
sortKey: 'name',
sortDirection: sortDirections.DESCENDING
},
//
@@ -146,7 +150,13 @@ export default {
delete selectedSchema.name;
selectedSchema.fields = selectedSchema.fields.map((field) => {
return { ...field };
const newField = { ...field };
if (newField.privacy === 'apiKey' || newField.privacy === 'password') {
newField.value = '';
}
return newField;
});
newState.selectedSchema = selectedSchema;
@@ -157,7 +167,10 @@ export default {
};
return updateSectionState(state, section, newState);
}
},
[SET_MANAGE_INDEXERS_SORT]: createSetClientSideCollectionSortReducer(section)
}
};

View File

@@ -130,7 +130,10 @@ export const actionHandlers = handleThunks({
promise.done((data) => {
const updatedItem = _.cloneDeep(data);
updatedItem.internalId = updatedItem.id;
updatedItem.id = updatedItem.tmdbId;
delete updatedItem.images;
const actions = [
updateItem({ section: 'movies', ...data }),
updateItem({ section: 'addMovie', ...updatedItem }),

View File

@@ -128,6 +128,22 @@ export const filterPredicates = {
return predicate(originalLanguage ? originalLanguage.name : '', filterValue);
},
releaseGroups: function(item, filterValue, type) {
const predicate = filterTypePredicates[type];
const { statistics = {} } = item;
const { releaseGroups = [] } = statistics;
return predicate(releaseGroups, filterValue);
},
sizeOnDisk: function(item, filterValue, type) {
const predicate = filterTypePredicates[type];
const { statistics = {} } = item;
const sizeOnDisk = statistics && statistics.sizeOnDisk ? statistics.sizeOnDisk : 0;
return predicate(sizeOnDisk, filterValue);
},
inCinemas: function(item, filterValue, type) {
return dateFilterPredicate(item.inCinemas, filterValue, type);
},
@@ -290,6 +306,12 @@ export const sortPredicates = {
}
return Number.MAX_VALUE;
},
sizeOnDisk: function(item) {
const { statistics = {} } = item;
return statistics.sizeOnDisk || 0;
}
};

View File

@@ -206,10 +206,16 @@ export const defaultState = {
isSortable: true,
isVisible: false
},
{
name: 'releaseGroups',
label: () => translate('ReleaseGroup'),
isSortable: true,
isVisible: false
},
{
name: 'tags',
label: () => translate('Tags'),
isSortable: false,
isSortable: true,
isVisible: false
},
{
@@ -241,6 +247,17 @@ export const defaultState = {
return originalLanguage.name;
},
releaseGroups: function(item) {
const { statistics = {} } = item;
const { releaseGroups = [] } = statistics;
return releaseGroups.length ?
releaseGroups
.map((group) => group.toLowerCase())
.sort((a, b) => a.localeCompare(b)) :
undefined;
},
imdbRating: function(item) {
const { ratings = {} } = item;
@@ -313,6 +330,28 @@ export const defaultState = {
return collectionList.sort(sortByName);
}
},
{
name: 'releaseGroups',
label: () => translate('ReleaseGroups'),
type: filterBuilderTypes.ARRAY,
optionsSelector: function(items) {
const groupList = items.reduce((acc, movie) => {
const { statistics = {} } = movie;
const { releaseGroups = [] } = statistics;
releaseGroups.forEach((releaseGroup) => {
acc.push({
id: releaseGroup,
name: releaseGroup
});
});
return acc;
}, []);
return groupList.sort(sortByName);
}
},
{
name: 'status',
label: () => translate('ReleaseStatus'),

View File

@@ -147,6 +147,12 @@ export const defaultState = {
isSortable: true,
isVisible: true
},
{
name: 'added',
label: () => translate('Added'),
isSortable: true,
isVisible: false
},
{
name: 'progress',
label: () => translate('Progress'),

View File

@@ -1,8 +1,9 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
function createDimensionsSelector() {
return createSelector(
(state) => state.app.dimensions,
(state: AppState) => state.app.dimensions,
(dimensions) => {
return dimensions;
}

View File

@@ -215,6 +215,8 @@ module.exports = {
calendarTextDim: '#eee',
calendarTextDimAlternate: '#fff',
calendarFullColorFilter: 'grayscale(90%) contrast(200%) saturate(50%)',
//
// Table

View File

@@ -216,6 +216,8 @@ module.exports = {
calendarTextDim: '#666',
calendarTextDimAlternate: '#242424',
calendarFullColorFilter: 'brightness(30%)',
//
// Table

View File

@@ -28,6 +28,7 @@ interface Queue extends ModelBase {
sizeleft: number;
timeleft: string;
estimatedCompletionTime: string;
added?: string;
status: string;
trackedDownloadStatus: QueueTrackedDownloadStatus;
trackedDownloadState: QueueTrackedDownloadState;

View File

@@ -1,4 +1,5 @@
export interface UiSettings {
theme: string;
showRelativeDates: boolean;
shortDateFormat: string;
longDateFormat: string;

View File

@@ -28,7 +28,7 @@
"@fortawesome/free-solid-svg-icons": "6.4.0",
"@fortawesome/react-fontawesome": "0.2.0",
"@juggle/resize-observer": "3.4.0",
"@microsoft/signalr": "6.0.21",
"@microsoft/signalr": "6.0.25",
"@sentry/browser": "7.51.2",
"@sentry/integrations": "7.51.2",
"@types/node": "18.16.8",

View File

@@ -1,6 +1,9 @@
using System.Collections.Generic;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Localization;
using NzbDrone.Test.Common;
using Radarr.Http.ClientSchema;
@@ -9,6 +12,16 @@ namespace NzbDrone.Api.Test.ClientSchemaTests
[TestFixture]
public class SchemaBuilderFixture : TestBase
{
[SetUp]
public void Setup()
{
Mocker.GetMock<ILocalizationService>()
.Setup(s => s.GetLocalizedString(It.IsAny<string>(), It.IsAny<Dictionary<string, object>>()))
.Returns<string, Dictionary<string, object>>((s, d) => s);
SchemaBuilder.Initialize(Mocker.Container);
}
[Test]
public void should_return_field_for_every_property()
{

View File

@@ -47,7 +47,7 @@ namespace NzbDrone.Common.Test.Http
// Use mirrors for tests that use two hosts
var candidates = new[] { "httpbin1.servarr.com" };
// httpbin.org is broken right now, occassionally redirecting to https if it's unavailable.
// httpbin.org is broken right now, occasionally redirecting to https if it's unavailable.
_httpBinHost = mainHost;
_httpBinHosts = candidates.Where(IsTestSiteAvailable).ToArray();
@@ -129,6 +129,16 @@ namespace NzbDrone.Common.Test.Http
response.Content.Should().NotBeNullOrWhiteSpace();
}
[Test]
public void should_throw_timeout_request()
{
var request = new HttpRequest($"https://{_httpBinHost}/delay/10");
request.RequestTimeout = new TimeSpan(0, 0, 5);
Assert.ThrowsAsync<WebException>(async () => await Subject.ExecuteAsync(request));
}
[Test]
public async Task should_execute_https_get()
{

View File

@@ -67,7 +67,7 @@ namespace NzbDrone.Common.EnvironmentInfo
}
catch (Exception ex)
{
_logger.Warn(ex, "Coudn't set app folder permission");
_logger.Warn(ex, "Couldn't set app folder permission");
}
}

View File

@@ -102,31 +102,38 @@ namespace NzbDrone.Common.Http.Dispatchers
var httpClient = GetClient(request.Url);
using var responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cts.Token);
try
{
byte[] data = null;
try
using var responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cts.Token);
{
if (request.ResponseStream != null && responseMessage.StatusCode == HttpStatusCode.OK)
byte[] data = null;
try
{
await responseMessage.Content.CopyToAsync(request.ResponseStream, null, cts.Token);
if (request.ResponseStream != null && responseMessage.StatusCode == HttpStatusCode.OK)
{
await responseMessage.Content.CopyToAsync(request.ResponseStream, null, cts.Token);
}
else
{
data = await responseMessage.Content.ReadAsByteArrayAsync(cts.Token);
}
}
else
catch (Exception ex)
{
data = await responseMessage.Content.ReadAsByteArrayAsync(cts.Token);
throw new WebException("Failed to read complete http response", ex, WebExceptionStatus.ReceiveFailure, null);
}
var headers = responseMessage.Headers.ToNameValueCollection();
headers.Add(responseMessage.Content.Headers.ToNameValueCollection());
return new HttpResponse(request, new HttpHeader(headers), data, responseMessage.StatusCode, responseMessage.Version);
}
catch (Exception ex)
{
throw new WebException("Failed to read complete http response", ex, WebExceptionStatus.ReceiveFailure, null);
}
var headers = responseMessage.Headers.ToNameValueCollection();
headers.Add(responseMessage.Content.Headers.ToNameValueCollection());
return new HttpResponse(request, new HttpHeader(headers), data, responseMessage.StatusCode, responseMessage.Version);
}
catch (OperationCanceledException ex) when (cts.IsCancellationRequested)
{
throw new WebException("Http request timed out", ex.InnerException, WebExceptionStatus.Timeout, null);
}
}

View File

@@ -92,6 +92,10 @@ namespace NzbDrone.Common.Http
{
data = new XElement("base64", Convert.ToBase64String(bytes));
}
else if (value is Dictionary<string, string> d)
{
data = new XElement("struct", d.Select(p => new XElement("member", new XElement("name", p.Key), new XElement("value", p.Value))));
}
else
{
throw new InvalidOperationException($"Unhandled argument type {value.GetType().Name}");

View File

@@ -123,7 +123,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
_debounce = new SentryDebounce();
// initialize to true and reconfigure later
// Otherwise it will default to false and any errors occuring
// Otherwise it will default to false and any errors occurring
// before config file gets read will not be filtered
FilterEvents = true;
SentryEnabled = true;

View File

@@ -260,7 +260,7 @@ namespace NzbDrone.Common.OAuth
}
/// <summary>
/// Creates a request elements concatentation value to send with a request.
/// Creates a request elements concatenation value to send with a request.
/// This is also known as the signature base.
/// </summary>
/// <seealso href="http://oauth.net/core/1.0#rfc.section.9.1.3"/>

View File

@@ -4,17 +4,17 @@
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DryIoc.dll" Version="5.4.1" />
<PackageReference Include="DryIoc.dll" Version="5.4.3" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NLog" Version="5.2.3" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.3" />
<PackageReference Include="Npgsql" Version="7.0.4" />
<PackageReference Include="Npgsql" Version="7.0.6" />
<PackageReference Include="Sentry" Version="3.23.1" />
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
<PackageReference Include="SharpZipLib" Version="1.3.3" />
<PackageReference Include="System.Text.Json" Version="6.0.8" />
<PackageReference Include="System.Text.Json" Version="6.0.9" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.1" />

View File

@@ -215,7 +215,7 @@ namespace NzbDrone.Common
if (dacls.Contains(authenticatedUsersDacl))
{
// Permssions already set
// Permissions already set
return;
}

View File

@@ -543,6 +543,52 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests
result.HasWarnings.Should().BeTrue();
}
[Test]
public void should_test_success_if_sorters_are_empty()
{
_config.Misc.enable_tv_sorting = false;
_config.Misc.tv_categories = null;
_config.Sorters = new List<SabnzbdSorter>();
var result = new NzbDroneValidationResult(Subject.Test());
result.IsValid.Should().BeTrue();
}
[Test]
public void should_test_failed_if_sorter_is_enabled_for_non_tv_category()
{
_config.Misc.enable_tv_sorting = false;
_config.Misc.tv_categories = null;
_config.Sorters = Builder<SabnzbdSorter>.CreateListOfSize(1)
.All()
.With(s => s.is_active = true)
.With(s => s.sort_cats = new List<string> { "movie-custom" })
.Build()
.ToList();
var result = new NzbDroneValidationResult(Subject.Test());
result.IsValid.Should().BeTrue();
}
[Test]
public void should_test_failed_if_sorter_is_enabled_for_tv_category()
{
_config.Misc.enable_tv_sorting = false;
_config.Misc.tv_categories = null;
_config.Sorters = Builder<SabnzbdSorter>.CreateListOfSize(1)
.All()
.With(s => s.is_active = true)
.With(s => s.sort_cats = new List<string> { "movie" })
.Build()
.ToList();
var result = new NzbDroneValidationResult(Subject.Test());
result.IsValid.Should().BeFalse();
}
[Test]
public void should_test_success_if_tv_sorting_disabled()
{

View File

@@ -4,6 +4,7 @@ using NUnit.Framework;
using NzbDrone.Core.Housekeeping.Housekeepers;
using NzbDrone.Core.ImportLists;
using NzbDrone.Core.ImportLists.ImportListMovies;
using NzbDrone.Core.Movies;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
@@ -42,8 +43,13 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
{
GivenImportList();
var movieMetadata = Builder<MovieMetadata>.CreateNew().BuildNew();
Db.Insert(movieMetadata);
var status = Builder<ImportListMovie>.CreateNew()
.With(h => h.ListId = _importList.Id)
.With(b => b.MovieMetadataId = movieMetadata.Id)
.BuildNew();
Db.Insert(status);

View File

@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
Mocker.GetMock<IHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), recentFeed)));
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, recentFeed)));
var releases = await Subject.FetchRecent();

View File

@@ -0,0 +1,49 @@
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using Moq;
using NLog;
using NUnit.Framework;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.IndexerTests;
[TestFixture]
public class IndexerBaseFixture : CoreTest<IndexerBase<TestIndexerSettings>>
{
private TestIndexer _indexer;
[SetUp]
public void Setup()
{
_indexer = new TestIndexer(new Mock<IHttpClient>().Object,
new Mock<IIndexerStatusService>().Object,
new Mock<IConfigService>().Object,
new Mock<IParsingService>().Object,
new Mock<Logger>().Object)
{
Definition = new IndexerDefinition
{
Settings = new TestIndexerSettings
{
MultiLanguages = new List<int> { Language.German.Id, Language.English.Id }
}
}
};
}
[TestCase("The.Movie.Name.2016.Multi.DTS.720p.BluRay.x264-RlsGrp")]
public void should_parse_multi_language(string postTitle)
{
var result = _indexer.CleanupReleases(new ReleaseInfo[] { new () { Title = postTitle, Languages = new List<Language>() } });
result.Single().Languages.Count.Should().Be(2);
result.Single().Languages.Should().Contain(Language.German);
result.Single().Languages.Should().Contain(Language.English);
}
}

View File

@@ -1,8 +1,10 @@
using NLog;
using System.Collections.Generic;
using NLog;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Test.IndexerTests
{
@@ -31,5 +33,10 @@ namespace NzbDrone.Core.Test.IndexerTests
{
return _parser;
}
public new IList<ReleaseInfo> CleanupReleases(IEnumerable<ReleaseInfo> releases)
{
return base.CleanupReleases(releases);
}
}
}

View File

@@ -156,7 +156,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
Subject.Scan(_movie);
Mocker.GetMock<IDiskProvider>()
.Verify(v => v.GetFiles(It.IsAny<string>(), It.IsAny<bool>()), Times.Once());
.Verify(v => v.GetFiles(It.IsAny<string>(), It.IsAny<bool>()), Times.Exactly(2));
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _movie, false), Times.Once());

View File

@@ -15,6 +15,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo.MediaInfoFormatterTests
[TestCase(HdrFormat.Hdr10Plus, "HDR10Plus")]
[TestCase(HdrFormat.DolbyVision, "DV")]
[TestCase(HdrFormat.DolbyVisionHdr10, "DV HDR10")]
[TestCase(HdrFormat.DolbyVisionHdr10Plus, "DV HDR10Plus")]
[TestCase(HdrFormat.DolbyVisionHlg, "DV HLG")]
[TestCase(HdrFormat.DolbyVisionSdr, "DV SDR")]
public void should_format_video_dynamic_range_type(HdrFormat format, string expectedVideoDynamicRangeType)

View File

@@ -1,4 +1,5 @@
using System.IO;
using System.Collections.Generic;
using System.IO;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
@@ -71,7 +72,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo
GivenFileExists();
GivenSuccessfulScan();
Subject.Handle(new MovieScannedEvent(_movie));
Subject.Handle(new MovieScannedEvent(_movie, new List<string>()));
Mocker.GetMock<IVideoFileInfoReader>()
.Verify(v => v.GetMediaInfo(Path.Combine(_movie.Path, "media.mkv")), Times.Exactly(2));
@@ -97,7 +98,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo
GivenFileExists();
GivenSuccessfulScan();
Subject.Handle(new MovieScannedEvent(_movie));
Subject.Handle(new MovieScannedEvent(_movie, new List<string>()));
Mocker.GetMock<IVideoFileInfoReader>()
.Verify(v => v.GetMediaInfo(Path.Combine(_movie.Path, "media.mkv")), Times.Exactly(2));
@@ -123,7 +124,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo
GivenFileExists();
GivenSuccessfulScan();
Subject.Handle(new MovieScannedEvent(_movie));
Subject.Handle(new MovieScannedEvent(_movie, new List<string>()));
Mocker.GetMock<IVideoFileInfoReader>()
.Verify(v => v.GetMediaInfo(Path.Combine(_movie.Path, "media.mkv")), Times.Exactly(3));
@@ -146,7 +147,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo
GivenSuccessfulScan();
Subject.Handle(new MovieScannedEvent(_movie));
Subject.Handle(new MovieScannedEvent(_movie, new List<string>()));
Mocker.GetMock<IVideoFileInfoReader>()
.Verify(v => v.GetMediaInfo("media.mkv"), Times.Never());
@@ -173,7 +174,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo
GivenSuccessfulScan();
GivenFailedScan(Path.Combine(_movie.Path, "media2.mkv"));
Subject.Handle(new MovieScannedEvent(_movie));
Subject.Handle(new MovieScannedEvent(_movie, new List<string>()));
Mocker.GetMock<IVideoFileInfoReader>()
.Verify(v => v.GetMediaInfo(Path.Combine(_movie.Path, "media.mkv")), Times.Exactly(1));
@@ -203,7 +204,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo
GivenFileExists();
GivenSuccessfulScan();
Subject.Handle(new MovieScannedEvent(_movie));
Subject.Handle(new MovieScannedEvent(_movie, new List<string>()));
Mocker.GetMock<IVideoFileInfoReader>()
.Verify(v => v.GetMediaInfo(It.IsAny<string>()), Times.Never());

View File

@@ -116,6 +116,8 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo
[TestCase(10, "bt2020", "smpte2084", "FFMpegCore.HdrDynamicMetadataSpmte2094", null, HdrFormat.Hdr10Plus)]
[TestCase(10, "bt2020", "smpte2084", "FFMpegCore.DoviConfigurationRecordSideData", null, HdrFormat.DolbyVision)]
[TestCase(10, "bt2020", "smpte2084", "FFMpegCore.DoviConfigurationRecordSideData", 1, HdrFormat.DolbyVisionHdr10)]
[TestCase(10, "bt2020", "smpte2084", "FFMpegCore.DoviConfigurationRecordSideData,FFMpegCore.HdrDynamicMetadataSpmte2094", 1, HdrFormat.DolbyVisionHdr10Plus)]
[TestCase(10, "bt2020", "smpte2084", "FFMpegCore.DoviConfigurationRecordSideData,FFMpegCore.HdrDynamicMetadataSpmte2094", 6, HdrFormat.DolbyVisionHdr10Plus)]
[TestCase(10, "bt2020", "smpte2084", "FFMpegCore.DoviConfigurationRecordSideData", 2, HdrFormat.DolbyVisionSdr)]
[TestCase(10, "bt2020", "smpte2084", "FFMpegCore.DoviConfigurationRecordSideData", 4, HdrFormat.DolbyVisionHlg)]
public void should_detect_hdr_correctly(int bitDepth, string colourPrimaries, string transferFunction, string sideDataTypes, int? doviConfigId, HdrFormat expected)

View File

@@ -48,13 +48,14 @@ namespace NzbDrone.Core.Test.NotificationTests
Subject.Definition = _traktDefinition;
}
private void GiventValidMediaInfo(Quality quality, string audioChannels, string audioFormat, string scanType)
private void GiventValidMediaInfo(Quality quality, string audioChannels, string audioFormat, string scanType, HdrFormat hdrFormat = HdrFormat.None)
{
_downloadMessage.MovieFile.MediaInfo = new MediaInfoModel
{
AudioChannelPositions = audioChannels,
AudioFormat = audioFormat,
ScanType = scanType
ScanType = scanType,
VideoHdrFormat = hdrFormat
};
_downloadMessage.MovieFile.Quality.Quality = quality;
@@ -72,7 +73,7 @@ namespace NzbDrone.Core.Test.NotificationTests
[Test]
public void should_add_collection_movie_if_valid_mediainfo()
{
GiventValidMediaInfo(Quality.Bluray1080p, "5.1", "DTS", "Progressive");
GiventValidMediaInfo(Quality.Bluray2160p, "5.1", "DTS", "Progressive", HdrFormat.DolbyVisionHdr10);
Subject.OnDownload(_downloadMessage);
@@ -80,15 +81,16 @@ namespace NzbDrone.Core.Test.NotificationTests
.Verify(v => v.AddToCollection(It.Is<TraktCollectMoviesResource>(t =>
t.Movies.First().Audio == "dts" &&
t.Movies.First().AudioChannels == "5.1" &&
t.Movies.First().Resolution == "hd_1080p" &&
t.Movies.First().MediaType == "bluray"),
t.Movies.First().Resolution == "uhd_4k" &&
t.Movies.First().MediaType == "bluray" &&
t.Movies.First().Hdr == "hdr10"),
It.IsAny<string>()), Times.Once());
}
[Test]
public void should_format_audio_channels_to_one_decimal_when_adding_collection_movie()
{
GiventValidMediaInfo(Quality.Bluray1080p, "2.0", "DTS", "Progressive");
GiventValidMediaInfo(Quality.Bluray2160p, "2.0", "DTS", "Progressive", HdrFormat.DolbyVisionHdr10);
Subject.OnDownload(_downloadMessage);
@@ -96,8 +98,9 @@ namespace NzbDrone.Core.Test.NotificationTests
.Verify(v => v.AddToCollection(It.Is<TraktCollectMoviesResource>(t =>
t.Movies.First().Audio == "dts" &&
t.Movies.First().AudioChannels == "2.0" &&
t.Movies.First().Resolution == "hd_1080p" &&
t.Movies.First().MediaType == "bluray"),
t.Movies.First().Resolution == "uhd_4k" &&
t.Movies.First().MediaType == "bluray" &&
t.Movies.First().Hdr == "hdr10"),
It.IsAny<string>()), Times.Once());
}
}

View File

@@ -140,6 +140,8 @@ namespace NzbDrone.Core.Test.ParserTests
}
[TestCase("Movie.Title.1994.Russian.1080p.XviD-LOL")]
[TestCase("Movie.Title.2020.WEB-DLRip.AVC.AC3.EN.RU.ENSub.RUSub-LOL")]
[TestCase("Movie Title (2020) WEB-DL (720p) Rus-Eng")]
public void should_parse_language_russian(string postTitle)
{
var result = Parser.Parser.ParseMovieTitle(postTitle, true);
@@ -300,6 +302,7 @@ namespace NzbDrone.Core.Test.ParserTests
}
[TestCase("Movie.Title.1994.Hebrew.1080p.XviD-LOL")]
[TestCase("Movie.Title.1994.1080p.BluRay.HebDubbed.Also.English.x264-P2P")]
public void should_parse_language_hebrew(string postTitle)
{
var result = Parser.Parser.ParseMovieTitle(postTitle, true);
@@ -387,6 +390,8 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Movie.Title.2022.lv.WEBRip.XviD-LOL")]
[TestCase("Movie.Title.2022.LATVIAN.WEBRip.XviD-LOL")]
[TestCase("Movie.Title.2022.Latvian.WEBRip.XviD-LOL")]
[TestCase("Movie.Title.2022.1080p.WEB-DL.DDP5.1.Atmos.H.264.Lat.Eng")]
[TestCase("Movie.Title.2022.1080p.WEB-DL.LAV.RUS-NPPK")]
public void should_parse_language_latvian(string postTitle)
{
var result = Parser.Parser.ParseMovieTitle(postTitle);
@@ -429,5 +434,35 @@ namespace NzbDrone.Core.Test.ParserTests
var result = LanguageParser.ParseSubtitleLanguage(fileName);
result.Should().Be(Language.Unknown);
}
[TestCase("The.Movie.Name.2016.German.DTS.DL.720p.BluRay.x264-RlsGrp")]
public void should_add_original_language_to_german_release_with_dl_tag(string postTitle)
{
var result = Parser.Parser.ParseMovieTitle(postTitle);
result.Languages.Count.Should().Be(2);
result.Languages.Should().Contain(Language.German);
result.Languages.Should().Contain(Language.Original);
}
[TestCase("The.Movie.Name.2016.GERMAN.WEB-DL.h264-RlsGrp")]
[TestCase("The.Movie.Name.2016.GERMAN.WEB.DL.h264-RlsGrp")]
[TestCase("The Movie Name 2016 GERMAN WEB DL h264-RlsGrp")]
[TestCase("The.Movie.Name.2016.GERMAN.WEBDL.h264-RlsGrp")]
public void should_not_add_original_language_to_german_release_when_title_contains_web_dl(string postTitle)
{
var result = Parser.Parser.ParseMovieTitle(postTitle);
result.Languages.Count.Should().Be(1);
result.Languages.Should().Contain(Language.German);
}
[TestCase("The.Movie.Name.2023.German.ML.EAC3.720p.NF.WEB.H264-RlsGrp")]
public void should_add_original_language_and_english_to_german_release_with_ml_tag(string postTitle)
{
var result = Parser.Parser.ParseMovieTitle(postTitle);
result.Languages.Count.Should().Be(3);
result.Languages.Should().Contain(Language.German);
result.Languages.Should().Contain(Language.Original);
result.Languages.Should().Contain(Language.English);
}
}
}

View File

@@ -62,6 +62,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("G.I.Movie.Movie.2013.THEATRiCAL.COMPLETE.BLURAY-GLiMMER", "G.I. Movie Movie")]
[TestCase("www.Torrenting.org - Movie.2008.720p.X264-DIMENSION", "Movie")]
[TestCase("The.French.Movie.2013.720p.BluRay.x264 - ROUGH[PublicHD]", "The French Movie")]
[TestCase("The.Good.German.2006.720p.BluRay.x264-RlsGrp", "The Good German", Description = "Hardcoded to exclude from German regex")]
public void should_parse_movie_title(string postTitle, string title)
{
Parser.Parser.ParseMovieTitle(postTitle).PrimaryMovieTitle.Should().Be(title);
@@ -124,6 +125,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Die.fantastische.Reise.des.Dr.Dolittle.2020.German.DL.LD.1080p.WEBRip.x264-PRD", "Die fantastische Reise des Dr. Dolittle", "", 2020, Description = "dot after dr")]
[TestCase("Der.Film.deines.Lebens.German.2011.PAL.DVDR-ETM", "Der Film deines Lebens", "", 2011, Description = "year at wrong position")]
[TestCase("Kick.Ass.2.2013.German.DTS.DL.720p.BluRay.x264-Pate_", "Kick Ass 2", "", 2013, Description = "underscore at the end")]
[TestCase("The.Good.German.2006.GERMAN.720p.HDTV.x264-RLsGrp", "The Good German", "", 2006, Description = "German in the title")]
public void should_parse_german_movie(string postTitle, string title, string edition, int year)
{
var movie = Parser.Parser.ParseMovieTitle(postTitle);
@@ -238,6 +240,10 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("The.Italian.Movie.2025.720p.BluRay.X264-AMIABLE")]
[TestCase("The.French.Movie.2013.720p.BluRay.x264 - ROUGH[PublicHD]")]
[TestCase("The.German.Doctor.2013.LIMITED.DVDRip.x264-RedBlade", Description = "When German is not followed by a year or a SCENE word it is not matched")]
[TestCase("The.Good.German.2006.720p.HDTV.x264-TVP", Description = "The Good German is hardcoded not to match")]
[TestCase("German.Lancers.2019.720p.BluRay.x264-UNiVERSUM", Description = "German at the beginning is never matched")]
[TestCase("The.German.2019.720p.BluRay.x264-UNiVERSUM", Description = "The German is hardcoded not to match")]
public void should_not_parse_wrong_language_in_title(string postTitle)
{
var parsed = Parser.Parser.ParseMovieTitle(postTitle, true);
@@ -245,22 +251,6 @@ namespace NzbDrone.Core.Test.ParserTests
parsed.Languages.First().Should().Be(Language.Unknown);
}
[TestCase("The.Movie.Name.2016.German.DTS.DL.720p.BluRay.x264-MULTiPLEX")]
public void should_not_parse_multi_language_in_releasegroup(string postTitle)
{
var parsed = Parser.Parser.ParseMovieTitle(postTitle, true);
parsed.Languages.Count.Should().Be(1);
parsed.Languages.First().Should().Be(Language.German);
}
[TestCase("The.Movie.Name.2016.German.Multi.DTS.DL.720p.BluRay.x264-MULTiPLEX")]
public void should_parse_multi_language(string postTitle)
{
var parsed = Parser.Parser.ParseMovieTitle(postTitle, true);
parsed.Languages.Count.Should().Be(1);
parsed.Languages.Should().Contain(Language.German);
}
[TestCase("Movie.Title.2016.1080p.KORSUB.WEBRip.x264.AAC2.0-RADARR", "KORSUB")]
[TestCase("Movie.Title.2016.1080p.KORSUBS.WEBRip.x264.AAC2.0-RADARR", "KORSUBS")]
[TestCase("Movie Title 2017 HC 720p HDRiP DD5 1 x264-LEGi0N", "Generic Hardcoded Subs")]

View File

@@ -41,6 +41,23 @@ namespace NzbDrone.Core.Annotations
public string Hint { get; set; }
}
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public class FieldTokenAttribute : Attribute
{
public FieldTokenAttribute(TokenField field, string label = "", string token = "", object value = null)
{
Label = label;
Field = field;
Token = token;
Value = value?.ToString();
}
public string Label { get; set; }
public TokenField Field { get; set; }
public string Token { get; set; }
public string Value { get; set; }
}
public class FieldSelectOption
{
public int Value { get; set; }
@@ -84,4 +101,11 @@ namespace NzbDrone.Core.Annotations
ApiKey,
UserName
}
public enum TokenField
{
Label,
HelpText,
HelpTextWarning
}
}

View File

@@ -15,6 +15,7 @@ namespace NzbDrone.Core.Blocklisting
public interface IBlocklistService
{
bool Blocklisted(int movieId, ReleaseInfo release);
bool BlocklistedTorrentHash(int movieId, string hash);
PagingSpec<Blocklist> Paged(PagingSpec<Blocklist> pagingSpec);
List<Blocklist> GetByMovieId(int movieId);
void Block(RemoteMovie remoteMovie, string message);
@@ -37,30 +38,34 @@ namespace NzbDrone.Core.Blocklisting
public bool Blocklisted(int movieId, ReleaseInfo release)
{
var blocklistedByTitle = _blocklistRepository.BlocklistedByTitle(movieId, release.Title);
if (release.DownloadProtocol == DownloadProtocol.Torrent)
{
var torrentInfo = release as TorrentInfo;
if (torrentInfo == null)
if (release is not TorrentInfo torrentInfo)
{
return false;
}
if (torrentInfo.InfoHash.IsNullOrWhiteSpace())
if (torrentInfo.InfoHash.IsNotNullOrWhiteSpace())
{
return blocklistedByTitle.Where(b => b.Protocol == DownloadProtocol.Torrent)
.Any(b => SameTorrent(b, torrentInfo));
var blocklistedByTorrentInfohash = _blocklistRepository.BlocklistedByTorrentInfoHash(movieId, torrentInfo.InfoHash);
return blocklistedByTorrentInfohash.Any(b => SameTorrent(b, torrentInfo));
}
var blocklistedByTorrentInfohash = _blocklistRepository.BlocklistedByTorrentInfoHash(movieId, torrentInfo.InfoHash);
return blocklistedByTorrentInfohash.Any(b => SameTorrent(b, torrentInfo));
return _blocklistRepository.BlocklistedByTitle(movieId, release.Title)
.Where(b => b.Protocol == DownloadProtocol.Torrent)
.Any(b => SameTorrent(b, torrentInfo));
}
return blocklistedByTitle.Where(b => b.Protocol == DownloadProtocol.Usenet)
.Any(b => SameNzb(b, release));
return _blocklistRepository.BlocklistedByTitle(movieId, release.Title)
.Where(b => b.Protocol == DownloadProtocol.Usenet)
.Any(b => SameNzb(b, release));
}
public bool BlocklistedTorrentHash(int movieId, string hash)
{
return _blocklistRepository.BlocklistedByTorrentInfoHash(movieId, hash).Any(b =>
b.TorrentInfoHash.Equals(hash, StringComparison.InvariantCultureIgnoreCase));
}
public PagingSpec<Blocklist> Paged(PagingSpec<Blocklist> pagingSpec)

View File

@@ -330,8 +330,8 @@ namespace NzbDrone.Core.Configuration
return;
}
// If SSL is enabled and a cert hash is still in the config file disable SSL
if (EnableSsl && GetValue("SslCertHash", null).IsNotNullOrWhiteSpace())
// If SSL is enabled and a cert hash is still in the config file or cert path is empty disable SSL
if (EnableSsl && (GetValue("SslCertHash", null).IsNotNullOrWhiteSpace() || SslCertPath.IsNullOrWhiteSpace()))
{
SetValue("EnableSsl", false);
}

View File

@@ -0,0 +1,41 @@
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.CustomFormats
{
public class YearSpecificationValidator : AbstractValidator<YearSpecification>
{
public YearSpecificationValidator()
{
RuleFor(c => c.Min).NotEmpty().GreaterThan(0);
RuleFor(c => c.Max).NotEmpty().GreaterThanOrEqualTo(c => c.Min);
}
}
public class YearSpecification : CustomFormatSpecificationBase
{
private static readonly YearSpecificationValidator Validator = new ();
public override int Order => 10;
public override string ImplementationName => "Year";
[FieldDefinition(1, Label = "Minimum Year", Type = FieldType.Number)]
public int Min { get; set; }
[FieldDefinition(2, Label = "Maximum Year", Type = FieldType.Number)]
public int Max { get; set; }
protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input)
{
var year = input.MovieInfo?.Year ?? input.Movie?.MovieMetadata?.Value?.Year;
return year >= Min && year <= Max;
}
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}

View File

@@ -9,8 +9,8 @@ namespace NzbDrone.Core.Datastore
{
public interface IConnectionStringFactory
{
string MainDbConnectionString { get; }
string LogDbConnectionString { get; }
DatabaseConnectionInfo MainDbConnection { get; }
DatabaseConnectionInfo LogDbConnection { get; }
string GetDatabasePath(string connectionString);
}
@@ -22,15 +22,15 @@ namespace NzbDrone.Core.Datastore
{
_configFileProvider = configFileProvider;
MainDbConnectionString = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresMainDb) :
MainDbConnection = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresMainDb) :
GetConnectionString(appFolderInfo.GetDatabase());
LogDbConnectionString = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresLogDb) :
LogDbConnection = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresLogDb) :
GetConnectionString(appFolderInfo.GetLogDatabase());
}
public string MainDbConnectionString { get; private set; }
public string LogDbConnectionString { get; private set; }
public DatabaseConnectionInfo MainDbConnection { get; private set; }
public DatabaseConnectionInfo LogDbConnection { get; private set; }
public string GetDatabasePath(string connectionString)
{
@@ -39,7 +39,7 @@ namespace NzbDrone.Core.Datastore
return connectionBuilder.DataSource;
}
private static string GetConnectionString(string dbPath)
private static DatabaseConnectionInfo GetConnectionString(string dbPath)
{
var connectionBuilder = new SQLiteConnectionStringBuilder
{
@@ -57,21 +57,22 @@ namespace NzbDrone.Core.Datastore
connectionBuilder.Add("Full FSync", true);
}
return connectionBuilder.ConnectionString;
return new DatabaseConnectionInfo(DatabaseType.SQLite, connectionBuilder.ConnectionString);
}
private string GetPostgresConnectionString(string dbName)
private DatabaseConnectionInfo GetPostgresConnectionString(string dbName)
{
var connectionBuilder = new NpgsqlConnectionStringBuilder();
var connectionBuilder = new NpgsqlConnectionStringBuilder
{
Database = dbName,
Host = _configFileProvider.PostgresHost,
Username = _configFileProvider.PostgresUser,
Password = _configFileProvider.PostgresPassword,
Port = _configFileProvider.PostgresPort,
Enlist = false
};
connectionBuilder.Database = dbName;
connectionBuilder.Host = _configFileProvider.PostgresHost;
connectionBuilder.Username = _configFileProvider.PostgresUser;
connectionBuilder.Password = _configFileProvider.PostgresPassword;
connectionBuilder.Port = _configFileProvider.PostgresPort;
connectionBuilder.Enlist = false;
return connectionBuilder.ConnectionString;
return new DatabaseConnectionInfo(DatabaseType.PostgreSQL, connectionBuilder.ConnectionString);
}
}
}

View File

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

View File

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

View File

@@ -87,7 +87,7 @@ namespace NzbDrone.Core.Datastore
}
/// <summary>
/// Visits the memeber access expression. To be implemented by user.
/// Visits the member access expression. To be implemented by user.
/// </summary>
/// <param name="expression"></param>
/// <returns></returns>

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