1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-03-05 13:20:20 -05:00

Compare commits

...

117 Commits

Author SHA1 Message Date
Mark McDowall
78913a3e9f New: Subtitles indexer flag to indicate releases with subtitles
Closes #7625
2025-09-01 15:06:44 -07:00
康小广
7fdc4d6638 Follow redirects when fetching Custom Lists 2025-09-01 14:59:34 -07:00
grapexy
309b55fe38 New: Georgian language 2025-09-01 14:58:40 -07:00
oxfordllama
d6f265c7b5 Subtitles indexer flag to indicate BTN releases with subtitles 2025-09-01 14:58:19 -07:00
Weblate
e757dca038 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: ArLab1 <arnaud.laberge@hotmail.com>
Co-authored-by: Gallyam Biktashev <gallyamb@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Hugoren Martinako <aumpfbahn@gmail.com>
Co-authored-by: NanderTGA <nander.roobaert@gmail.com>
Co-authored-by: ReDFiRe <wwsoft@abv.bg>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Xoores <servarr-35466@xoores.cz>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: mrchonks <chonkstv@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/
Translation: Servarr/Sonarr
2025-09-01 14:56:39 -07:00
Mark McDowall
9ebe043bd9 New: Move auth success logging to debug
Closes #7978
2025-08-10 21:26:53 -07:00
Mark McDowall
f055e8a3e5 Fixed: Parsing English as the second language in a release name
Closes #8006
2025-08-10 21:26:39 -07:00
Sonarr
8c697afa67 Automated API Docs update
ignore-downstream
2025-08-10 21:20:03 -07:00
Weblate
8d68879edd Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: ArLab1 <arnaud.laberge@hotmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/
Translation: Servarr/Sonarr
2025-08-10 21:19:54 -07:00
Trey Turner
e9c82078da Fixed: File air date being updated every refresh
Closes #7989
2025-08-10 21:18:34 -07:00
Stevie Robinson
f0798550af New Include Finale type in Webhook and Custom Script connections
Closes #7999
2025-08-10 21:10:48 -07:00
Mark McDowall
d9c7838329 Fixed: Sub group parsing could result in extra brackets being parsed
Closes #7994
2025-08-10 21:10:11 -07:00
Mark McDowall
b00229e53c Fixed: Treat TaoE and QxR as release group instead of encoder
Closes #7972
2025-08-10 21:10:11 -07:00
Luigi
880628fb68 New: Select with poster click in series selection 2025-08-10 21:09:50 -07:00
Stevie Robinson
b09c6f0811 New: Include Mal and AniList IDs in API response and Webhooks
Closes #7973
2025-08-10 21:08:25 -07:00
Mark McDowall
b376b63c9e New: Parse '(JA)' as Japanese
Closes #7956
2025-08-10 21:07:32 -07:00
bparkin1283
99feaa34d2 Replace service --status-all with systemctl is-active 2025-08-10 21:07:27 -07:00
Stevie Robinson
d7f82a72c2 Fixed: Update nzb.su domain to nzb.life 2025-08-10 21:06:41 -07:00
Stevie Robinson
bd20ebfad7 New: Indexer option for Season Pack Seed Ratio 2025-08-10 21:06:20 -07:00
jutoft
71553ad67b New: Tribler 8 download client
Closes #1813
2025-08-10 21:05:40 -07:00
Weblate
41c39f1f28 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: ArLab1 <arnaud.laberge@hotmail.com>
Co-authored-by: Dino <me@dinodev.org>
Co-authored-by: Gallyam Biktashev <gallyamb@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Hugoren Martinako <aumpfbahn@gmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: mrchonks <chonkstv@gmail.com>
Co-authored-by: myrad2267 <myrad2267@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/
Translation: Servarr/Sonarr
2025-08-10 21:04:57 -07:00
Mark McDowall
d0066358eb Upgraded SixLabors.ImageSharp to 3.1.11 2025-08-01 09:31:00 -07:00
Weblate
6f1d461dad Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: wavybox <leeviervoemil@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fi/
Translation: Servarr/Sonarr
2025-07-07 17:38:19 -07:00
Stevie Robinson
6ccab3cfc8 Fixed: Improve parsing of bit depth 2025-07-07 17:37:45 -07:00
Mark McDowall
5e47cc3baa Fixed: Improve parsing of 4-digit absolute numbering batches 2025-07-07 17:37:19 -07:00
Mark McDowall
78ca30d1f8 Don't log debug messages for API key validation
Closes #7934
2025-07-07 17:37:10 -07:00
Mark McDowall
f9d0abada3 Fixed: Manual Import could lead to duplicate notifications
Closes #7922
2025-07-07 17:37:02 -07:00
Mark McDowall
4bdb0408f1 Return error if Manual Import called without items
Closes #7942
2025-07-07 17:36:53 -07:00
Stevie Robinson
40ea6ce4e5 New: Persist queue removal option in browser
Closes #7938
2025-07-07 17:36:45 -07:00
nuxen
ccf33033dc Fixed: xvid not always detected correctly 2025-07-07 17:36:06 -07:00
Mark McDowall
996c0e9f50 Upgrade MonoTorrent to 3.0.2
Closes #7270
2025-07-07 17:35:37 -07:00
Stevie Robinson
8b7f9daab0 Improve Emby/Jellyfin connection test 2025-07-07 17:35:32 -07:00
Mark McDowall
dfb6fdfbeb Change authentication to Forms if set to Basic 2025-07-07 17:34:34 -07:00
Mark McDowall
29d0073ee6 Make remove root folder Sonarr specific 2025-07-07 17:34:34 -07:00
Mark McDowall
9cf6be32fa Fixed deleting tags from UI 2025-07-07 17:34:34 -07:00
Ben Martin
fee3f8150e New: Option to add series tags when adding torrents to qbit
Closes #7930
2025-07-07 17:34:25 -07:00
nuxen
010bbbd222 Support for multiple seriesIds in Rename API endpoint 2025-07-07 17:33:40 -07:00
ricecrackerfiend
d3c3a6ebce Fixed: Prevent long series titles from cutting off synopsis
Closes #7875
2025-07-07 17:33:15 -07:00
Stevie Robinson
f26344ae75 New: Allow user to define additional rejected extensions
Closes #7721
2025-07-07 17:32:34 -07:00
Weblate
034f731308 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Gallyam Biktashev <gallyamb@gmail.com>
Co-authored-by: Gjur0 <denjy0@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: HanaO00 <lwin24452@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Hugoren Martinako <aumpfbahn@gmail.com>
Co-authored-by: Marius Nechifor <flm.marius@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/
Translation: Servarr/Sonarr
2025-07-07 17:32:02 -07:00
Mark McDowall
4b50861a6b Revert "Added 'libicu72' as a required package for Debian install script"
This reverts commit 103b1335b9.
2025-06-10 06:33:52 -07:00
Weblate
f977b8ba1b Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/
Translation: Servarr/Sonarr
2025-06-09 17:22:15 -07:00
Bogdan
8374ebc25b Fixed: Buttons cutoff when page toolbar shows all buttons on smaller screens 2025-06-09 17:16:19 -07:00
Michael Peleshenko
71851d038c Fixed: Prevent series without IMDB ID from being removed erroneously 2025-06-09 17:15:36 -07:00
Stevie Robinson
9ffcd141a5 Fixed: Include network drive types in Disk Space 2025-06-09 17:14:13 -07:00
Bogdan
a6f50408f2 Fixed: Quality sliders on some browsers 2025-06-09 17:13:41 -07:00
Bogdan
6e43b08dab Typings for iCal state options 2025-06-09 17:13:17 -07:00
Bogdan
90c4791d5f Fix status for grabbed episodes part of grouped calendar events 2025-06-09 17:13:17 -07:00
v3DJG6GL
030910babc New: Map Swiss German to German 2025-06-09 17:12:58 -07:00
Stevie Robinson
59af86cea4 New: Rename Series Monitoring to Episode Monitoring 2025-06-09 17:12:33 -07:00
Bogdan
4cb25228b6 Cleanup series title normalizer 2025-06-09 17:12:09 -07:00
Stevie Robinson
bf34b43094 Fixed validation for Remote Path Mapping 2025-06-09 17:12:04 -07:00
Bogdan
1cdca8ef3e Follow redirects for usenet grabs on non-prod builds 2025-06-09 17:11:26 -07:00
Cybertinus
103b1335b9 Added 'libicu72' as a required package for Debian install script 2025-06-09 17:11:17 -07:00
Ghworg
b3d830c475 Fixed: Bitrate showing as Megabytes instead of Megabits
Closes #7879
2025-06-09 17:10:33 -07:00
Mark McDowall
a279240335 Fixed: Trakt Import List authentication after 24 hours
Closes #7874
2025-06-09 17:09:53 -07:00
Mark McDowall
3eed84c679 Prevent should refresh series from failing
Fixed: Prevent error checking if series should be refreshed from failing refresh series task
2025-06-09 17:09:41 -07:00
Mark McDowall
51c17fd312 New: Update wording when removing a root folder
Closes #7868
2025-06-09 17:09:28 -07:00
Mark McDowall
70c74fc176 Fixed: Escape backticks in discord notifications
Closes #7836
2025-06-09 17:09:23 -07:00
Weblate
cfda24536c Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Gallyam Biktashev <gallyamb@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Ilbebino <tommasobellandi08@gmail.com>
Co-authored-by: NanderTGA <nander.roobaert@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/
Translation: Servarr/Sonarr
2025-06-09 16:56:03 -07:00
Bogdan
14e324ee30 Use the thrown exception in http timeout handling 2025-05-22 01:58:54 +01:00
Bogdan
32ba06ecd0 Bump babel, fontawesome icons, fuse.js, react-lazyload, react-use-measure and react-window 2025-05-22 01:58:17 +01:00
Bogdan
61807fede0 Bump core-js to 3.42 2025-05-22 01:58:17 +01:00
Bogdan
2a1efe5f59 Fixed: Searching all missing from Wanted 2025-05-22 01:58:10 +01:00
Bogdan
0f43f8c9f6 Fixed: Loading suggestions for header search input 2025-05-22 01:58:04 +01:00
Stevie Robinson
a853c537db new: ignore volumes containing .timemachine from Disk Space 2025-05-22 01:57:50 +01:00
Bogdan
f9dccd6ec7 New: Include series poster for Apprise 2025-05-22 01:57:41 +01:00
v3DJG6GL
72b3b825eb New: Add Romansh language 2025-05-21 17:57:33 -07:00
carrossos
818ae02a7a Treat HTTP 410 response for failed download similarly to HTTP 404 2025-05-21 17:57:11 -07:00
Stevie Robinson
5ba3ff5987 New: Don't allow remote path to start with space 2025-05-21 17:56:15 -07:00
Mark McDowall
e38deb3422 Increase maximum backup restoration size to 5GB
Closes #7840
2025-05-22 01:52:30 +01:00
Bogdan
f35888e053 Avoid varying logging message template between calls 2025-05-22 01:52:24 +01:00
Stevie Robinson
a50d256264 Ensure Custom Format Maximum Size won't overflow 2025-05-21 17:52:16 -07:00
Weblate
70165bddc8 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Gallyam Biktashev <gallyamb@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Hugoren Martinako <aumpfbahn@gmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: warkurre86 <tom.novo.86@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/
Translation: Servarr/Sonarr
2025-05-22 01:41:49 +01:00
Sonarr
4258e94e90 Automated API Docs update
ignore-downstream
2025-04-28 17:18:44 -07:00
Mark McDowall
066b39032b Fixed: Parsing titles with 3 digit season numbers
Closes #7826
2025-04-28 17:14:19 -07:00
Mark McDowall
728df146ad Improve messaging when NZB contains invalid XML
Closes #7829
2025-04-28 17:14:09 -07:00
Bogdan
751a07bb40 Fixed sidebar flickering on mobile 2025-04-28 17:13:59 -07:00
Bogdan
d4ce60bd41 Bump caniuse db 2025-04-28 17:13:43 -07:00
Bogdan
4b868d3f06 Bump core-js to 3.41 2025-04-28 17:13:43 -07:00
Bogdan
817d13e85c Update default log level message 2025-04-28 17:12:51 -07:00
Bogdan
fae014c8be Fix various typos 2025-04-28 17:12:41 -07:00
Bogdan
2fa02472ee Fixed auto tagging tag specification 2025-04-28 17:12:33 -07:00
Bogdan
7c3c577811 Fixed selecting No Change for quality profile inputs 2025-04-28 17:12:10 -07:00
Bogdan
9fdf545f47 Improve typings for enhanced select options 2025-04-28 17:11:50 -07:00
Bogdan
e537a2dc8f Improve typings for Select Input options 2025-04-28 17:11:50 -07:00
Bogdan
1047e71b7d Log when expected episode file is missing from disk on upgrade 2025-04-28 17:11:25 -07:00
Bogdan
415498efb3 Fixed free space and missing for selected root folder value 2025-04-28 17:11:13 -07:00
Bogdan
cf08e947c4 Fixed grouping qualities inside a quality profile 2025-04-28 17:10:54 -07:00
Bogdan
bb872ee35b Add SeriesFolderController for v5 2025-04-28 17:10:09 -07:00
Bogdan
ab0d8352e8 Clean up progress log messages in ImportListSyncService 2025-04-28 17:09:32 -07:00
Bogdan
9683b0af35 Pass messages with arguments to NLog in LoggerExtensions 2025-04-28 17:09:32 -07:00
Bogdan
76b1130b68 Fixed: Sending Manual Interaction Needed for Custom Script with unparsed series
Closes #7807
2025-04-28 17:09:14 -07:00
Jamie Bartlett
5be58249f8 New: Auto tag based on network
Closes #7793
2025-04-28 17:08:57 -07:00
Weblate
4d67b8ae2b Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Gallyam Biktashev <gallyamb@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Hugoren Martinako <aumpfbahn@gmail.com>
Co-authored-by: José Fonseca <jpof88@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/
Translation: Servarr/Sonarr
2025-04-28 17:08:12 -07:00
ojmaster
66633b9b07 New: Add Urdu language 2025-04-07 17:02:48 -07:00
Bogdan
4728fa29ef Fixed: Avoid requests without categories for FileList 2025-04-07 16:45:02 -07:00
Bogdan
9cb9c711be Fixed: Respect series type in Newznab/Torznab searches for specials
Closes #7783
2025-04-07 16:45:02 -07:00
Bogdan
d62eea604a Validation for tags label 2025-04-07 16:43:58 -07:00
Weblate
3185315343 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Hugoren Martinako <aumpfbahn@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ca/
Translation: Servarr/Sonarr
2025-04-07 16:43:11 -07:00
Mark McDowall
e52b68ee7d Fix issues with Floating UI 2025-04-07 16:42:47 -07:00
Bogdan
f7eece32e7 Log delete statements only once 2025-04-07 16:42:36 -07:00
Stevie Robinson
c96c47af9e Add size validation for Quality Profiles 2025-04-07 16:41:22 -07:00
Tim Gunter
a5999b1410 Add command-line argument and unattended mode for Debian installs 2025-04-07 16:40:53 -07:00
Mark McDowall
ac1bb497ef Fixed: Default runtime to 45 minutes if unavailable when importing episode files
Closes #7780
2025-04-07 16:38:33 -07:00
Mark McDowall
9bd619ccfe Update WikiUrl type in API docs
Closes #7788
2025-04-07 16:38:24 -07:00
Mark McDowall
dfbf12b711 Fixed: Delete orphaned extra and subtitle files during housekeeping
Closes #7785
2025-04-07 16:38:20 -07:00
Bogdan
0ae07898ba Fixed: Improve error message for queue items from Transmission 2025-04-07 16:38:09 -07:00
Stevie Robinson
2314d0b506 New: Ability to clone Import Lists
Closes #7758
2025-04-07 16:34:41 -07:00
Josh Archer
2093f08a57 Fixed: Include total space in root folder endpoint
Closes #7769
2025-04-07 16:33:44 -07:00
Bogdan
0a7ffb64f0 Save Publish Dates as UTC for grabbed episodes 2025-04-07 16:33:16 -07:00
Mark McDowall
41b65abd1d New: Show episode count in season interactive search modal
Closes #7747
2025-04-07 16:33:09 -07:00
Mark McDowall
0f904e0917 New: Prevent Remote Path Mapping local folder being set to System folder or '/'
Closes #7686
2025-04-07 16:32:50 -07:00
Mark McDowall
f8e57b0985 Fixed: Set output encoding to UTF-8 when running external processes
Closes #7622
2025-04-07 16:32:46 -07:00
Bogdan
9e774f4026 New: Indexer Flags in Webhook Import Notifications 2025-04-07 16:32:40 -07:00
Stevie Robinson
2acc4c8865 New: Indexer Flags in Webhook On Grab Notifications
Closes #7741
2025-04-07 16:32:13 -07:00
Weblate
0fcd92e441 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Gallyam Biktashev <gallyamb@gmail.com>
Co-authored-by: Hugoren Martinako <aumpfbahn@gmail.com>
Co-authored-by: José Fonseca <jpof88@gmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ru/
Translation: Servarr/Sonarr
2025-04-07 16:31:37 -07:00
254 changed files with 5538 additions and 1789 deletions

113
distribution/debian/install.sh Normal file → Executable file
View File

@@ -6,6 +6,8 @@
### Version V1.0.1 2024-01-02 - StevieTV - remove UTF8-BOM
### Version V1.0.2 2024-01-03 - markus101 - Get user input from /dev/tty
### Version V1.0.3 2024-01-06 - StevieTV - exit script when it is ran from install directory
### Version V1.0.4 2025-04-05 - kaecyra - Allow user/group to be supplied via CLI, add unattended mode
### Version V1.0.5 2025-07-08 - bparkin1283 - use systemctl instead of service for stopping app
### Boilerplate Warning
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
@@ -16,8 +18,8 @@
#OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
#WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
scriptversion="1.0.3"
scriptdate="2024-01-06"
scriptversion="1.0.4"
scriptdate="2025-04-05"
set -euo pipefail
@@ -49,18 +51,106 @@ if [ "$installdir" == "$(dirname -- "$( readlink -f -- "$0"; )")" ] || [ "$bindi
exit
fi
# Prompt User
read -r -p "What user should ${app^} run as? (Default: $app): " app_uid < /dev/tty
show_help() {
cat <<EOF
Usage: $(basename "$0") [OPTIONS]
Options:
--user <name> What user will $app run under?
User will be created if it doesn't already exist.
--group <name> What group will $app run under?
Group will be created if it doesn't already exist.
-u Unattended mode
The installer will not prompt or pause, making it suitable for automated installations.
This option requires the use of --user and --group to supply those inputs for the script.
-h, --help Show this help message and exit
EOF
}
# Default values for command-line arguments
arg_user=""
arg_group=""
arg_unattended=false
# Parse command-line arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--user=*)
arg_user="${1#*=}"
shift
;;
--user)
if [[ -n "$2" && "$2" != -* ]]; then
arg_user="$2"
shift 2
else
echo "Error: --user requires a value." >&2
exit 1
fi
;;
--group=*)
arg_group="${1#*=}"
shift
;;
--group)
if [[ -n "$2" && "$2" != -* ]]; then
arg_group="$2"
shift 2
else
echo "Error: --group requires a value." >&2
exit 1
fi
;;
-u)
arg_unattended=true
shift
;;
-h|--help)
show_help
exit 0
;;
*)
echo "Unknown option: $1" >&2
echo "Use --help to see valid options." >&2
exit 1
;;
esac
done
# If unattended mode is requested, require user and group
if $arg_unattended; then
if [[ -z "$arg_user" || -z "$arg_group" ]]; then
echo "Error: --user and --group are required when using -u (unattended mode)." >&2
exit 1
fi
fi
# Prompt User if necessary
if [ -n "$arg_user" ]; then
app_uid="$arg_user"
else
read -r -p "What user should ${app^} run as? (Default: $app): " app_uid < /dev/tty
fi
app_uid=$(echo "$app_uid" | tr -d ' ')
app_uid=${app_uid:-$app}
# Prompt Group
read -r -p "What group should ${app^} run as? (Default: media): " app_guid < /dev/tty
# Prompt Group if necessary
if [ -n "$arg_group" ]; then
app_guid="$arg_group"
else
read -r -p "What group should ${app^} run as? (Default: media): " app_guid < /dev/tty
fi
app_guid=$(echo "$app_guid" | tr -d ' ')
app_guid=${app_guid:-media}
echo "This will install [${app^}] to [$bindir] and use [$datadir] for the AppData Directory"
echo "${app^} will run as the user [$app_uid] and group [$app_guid]. By continuing, you've confirmed that the selected user and group will have READ and WRITE access to your Media Library and Download Client Completed Download directories"
read -n 1 -r -s -p $'Press enter to continue or ctrl+c to exit...\n' < /dev/tty
if ! $arg_unattended; then
read -n 1 -r -s -p $'Press enter to continue or ctrl+c to exit...\n' < /dev/tty
fi
# Create User / Group as needed
if [ "$app_guid" != "$app_uid" ]; then
@@ -78,11 +168,10 @@ if ! getent group "$app_guid" | grep -qw "$app_uid"; then
echo "Added User [$app_uid] to Group [$app_guid]"
fi
# Stop the App if running
if service --status-all | grep -Fq "$app"; then
systemctl stop "$app"
systemctl disable "$app".service
echo "Stopped existing $app"
# Stop and disable the App if running
if [ $(systemctl is-active "$app") = "active" ]; then
systemctl disable --now -q "$app"
echo "Stopped and disabled existing $app"
fi
# Create Appdata Directory

View File

@@ -176,7 +176,7 @@ module.exports = (env) => {
loose: true,
debug: false,
useBuiltIns: 'entry',
corejs: '3.39'
corejs: '3.42'
}
]
]

View File

@@ -1,7 +1,10 @@
import React, { useCallback, useMemo, useState } from 'react';
import React, { useCallback, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import Button from 'Components/Link/Button';
import Modal from 'Components/Modal/Modal';
import ModalBody from 'Components/Modal/ModalBody';
@@ -9,6 +12,8 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
import { setQueueRemovalOption } from 'Store/Actions/queueActions';
import { InputChanged } from 'typings/inputs';
import translate from 'Utilities/String/translate';
import styles from './RemoveQueueItemModal.css';
@@ -30,12 +35,6 @@ interface RemoveQueueItemModalProps {
onModalClose: () => void;
}
type RemovalMethod = 'removeFromClient' | 'changeCategory' | 'ignore';
type BlocklistMethod =
| 'doNotBlocklist'
| 'blocklistAndSearch'
| 'blocklistOnly';
function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
const {
isOpen,
@@ -48,12 +47,13 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
onModalClose,
} = props;
const dispatch = useDispatch();
const multipleSelected = selectedCount && selectedCount > 1;
const [removalMethod, setRemovalMethod] =
useState<RemovalMethod>('removeFromClient');
const [blocklistMethod, setBlocklistMethod] =
useState<BlocklistMethod>('doNotBlocklist');
const { removalMethod, blocklistMethod } = useSelector(
(state: AppState) => state.queue.removalOptions
);
const { title, message } = useMemo(() => {
if (!selectedCount) {
@@ -79,7 +79,7 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
}, [sourceTitle, selectedCount]);
const removalMethodOptions = useMemo(() => {
return [
const options: EnhancedSelectInputValue<string>[] = [
{
key: 'removeFromClient',
value: translate('RemoveFromDownloadClient'),
@@ -106,10 +106,12 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
: translate('IgnoreDownloadHint'),
},
];
return options;
}, [canChangeCategory, canIgnore, multipleSelected]);
const blocklistMethodOptions = useMemo(() => {
return [
const options: EnhancedSelectInputValue<string>[] = [
{
key: 'doNotBlocklist',
value: translate('DoNotBlocklist'),
@@ -131,20 +133,15 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
: translate('BlocklistOnlyHint'),
},
];
return options;
}, [isPending, multipleSelected]);
const handleRemovalMethodChange = useCallback(
({ value }: { value: RemovalMethod }) => {
setRemovalMethod(value);
const handleRemovalOptionInputChange = useCallback(
({ name, value }: InputChanged) => {
dispatch(setQueueRemovalOption({ [name]: value }));
},
[setRemovalMethod]
);
const handleBlocklistMethodChange = useCallback(
({ value }: { value: BlocklistMethod }) => {
setBlocklistMethod(value);
},
[setBlocklistMethod]
[dispatch]
);
const handleConfirmRemove = useCallback(() => {
@@ -154,23 +151,11 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
blocklist: blocklistMethod !== 'doNotBlocklist',
skipRedownload: blocklistMethod === 'blocklistOnly',
});
setRemovalMethod('removeFromClient');
setBlocklistMethod('doNotBlocklist');
}, [
removalMethod,
blocklistMethod,
setRemovalMethod,
setBlocklistMethod,
onRemovePress,
]);
}, [removalMethod, blocklistMethod, onRemovePress]);
const handleModalClose = useCallback(() => {
setRemovalMethod('removeFromClient');
setBlocklistMethod('doNotBlocklist');
onModalClose();
}, [setRemovalMethod, setBlocklistMethod, onModalClose]);
}, [onModalClose]);
return (
<Modal isOpen={isOpen} size={sizes.MEDIUM} onModalClose={handleModalClose}>
@@ -193,7 +178,7 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
helpTextWarning={translate(
'RemoveQueueItemRemovalMethodHelpTextWarning'
)}
onChange={handleRemovalMethodChange}
onChange={handleRemovalOptionInputChange}
/>
</FormGroup>
)}
@@ -211,7 +196,7 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
value={blocklistMethod}
values={blocklistMethodOptions}
helpText={translate('BlocklistReleaseHelpText')}
onChange={handleBlocklistMethodChange}
onChange={handleRemovalOptionInputChange}
/>
</FormGroup>
</ModalBody>

View File

@@ -32,6 +32,17 @@ export interface QueuePagedAppState
removeError: Error;
}
export type RemovalMethod = 'removeFromClient' | 'changeCategory' | 'ignore';
export type BlocklistMethod =
| 'doNotBlocklist'
| 'blocklistAndSearch'
| 'blocklistOnly';
interface RemovalOptions {
removalMethod: RemovalMethod;
blocklistMethod: BlocklistMethod;
}
interface QueueAppState {
status: AppSectionItemState<QueueStatus>;
details: QueueDetailsAppState;
@@ -39,6 +50,7 @@ interface QueueAppState {
options: {
includeUnknownSeriesItems: boolean;
};
removalOptions: RemovalOptions;
}
export default QueueAppState;

View File

@@ -22,9 +22,9 @@ function createIsDownloadingSelector(episodeIds: number[]) {
return createSelector(
(state: AppState) => state.queue.details,
(details) => {
return details.items.some((item) => {
return !!(item.episodeId && episodeIds.includes(item.episodeId));
});
return details.items.some(
(item) => item.episodeId && episodeIds.includes(item.episodeId)
);
}
);
}
@@ -61,10 +61,10 @@ function CalendarEventGroup({
const endTime = moment(lastEpisode.airDateUtc).add(series.runtime, 'minutes');
const seasonNumber = firstEpisode.seasonNumber;
const { allDownloaded, anyQueued, anyMonitored, allAbsoluteEpisodeNumbers } =
const { allDownloaded, anyGrabbed, anyMonitored, allAbsoluteEpisodeNumbers } =
useMemo(() => {
let files = 0;
let queued = 0;
let grabbed = 0;
let monitored = 0;
let absoluteEpisodeNumbers = 0;
@@ -73,8 +73,8 @@ function CalendarEventGroup({
files++;
}
if (event.queued) {
queued++;
if (event.grabbed) {
grabbed++;
}
if (series.monitored && event.monitored) {
@@ -88,13 +88,13 @@ function CalendarEventGroup({
return {
allDownloaded: files === events.length,
anyQueued: queued > 0,
anyGrabbed: grabbed > 0,
anyMonitored: monitored > 0,
allAbsoluteEpisodeNumbers: absoluteEpisodeNumbers === events.length,
};
}, [series, events]);
const anyDownloading = isDownloading || anyQueued;
const anyDownloading = isDownloading || anyGrabbed;
const statusStyle = getStatusStyle(
allDownloaded,

View File

@@ -22,7 +22,12 @@ interface CalendarLinkModalContentProps {
function CalendarLinkModalContent({
onModalClose,
}: CalendarLinkModalContentProps) {
const [state, setState] = useState({
const [state, setState] = useState<{
unmonitored: boolean;
premieresOnly: boolean;
asAllDay: boolean;
tags: number[];
}>({
unmonitored: false,
premieresOnly: false,
asAllDay: false,

View File

@@ -93,9 +93,10 @@ function AutoSuggestInput<T = any>(props: AutoSuggestInputProps<T>) {
mainAxis: true,
}),
size({
apply({ rects, elements }) {
apply({ availableHeight, elements, rects }) {
Object.assign(elements.floating.style, {
width: `${rects.reference.width}px`,
minWidth: `${rects.reference.width}px`,
maxHeight: `${Math.max(0, availableHeight)}px`,
});
},
}),

View File

@@ -139,11 +139,11 @@ type PickProps<V, C extends InputType> = C extends 'text'
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
EnhancedSelectInputProps<any, V>
: C extends 'seriesTag'
? SeriesTagInputProps
? SeriesTagInputProps<V>
: C extends 'seriesTypeSelect'
? SeriesTypeSelectInputProps
: C extends 'tag'
? SeriesTagInputProps
? SeriesTagInputProps<V>
: C extends 'tagSelect'
? TagSelectInputProps
: C extends 'text'
@@ -222,7 +222,7 @@ function FormInputGroup<T, C extends InputType>(
<div className={containerClassName}>
<div className={className}>
<div className={styles.inputContainer}>
{/* @ts-expect-error - tpyes are validated already */}
{/* @ts-expect-error - types are validated already */}
<InputComponent
className={inputClassName}
helpText={helpText}

View File

@@ -120,7 +120,7 @@ function ProviderFieldFormGroup<T>({
helpTextWarning={helpTextWarning}
helpLink={helpLink}
placeholder={placeholder}
// @ts-expect-error - this isn;'t available on all types
// @ts-expect-error - this isn't available on all types
selectOptionsProviderAction={selectOptionsProviderAction}
value={value}
values={selectValues}

View File

@@ -42,15 +42,10 @@
color: var(--disabledInputColor);
}
.optionsContainer {
z-index: $popperZIndex;
max-height: vh(50);
width: auto;
}
.options {
composes: scroller from '~Components/Scroller/Scroller.css';
z-index: $popperZIndex;
border: 1px solid var(--inputBorderColor);
border-radius: 4px;
background-color: var(--inputBackgroundColor);

View File

@@ -13,7 +13,6 @@ interface CssExports {
'mobileCloseButton': string;
'mobileCloseButtonContainer': string;
'options': string;
'optionsContainer': string;
'optionsInnerModalBody': string;
'optionsModal': string;
'optionsModalBody': string;

View File

@@ -14,6 +14,7 @@ import React, {
KeyboardEvent,
ReactNode,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
@@ -180,15 +181,21 @@ function EnhancedSelectInput<T extends EnhancedSelectInputValue<V>, V>(
mainAxis: true,
}),
size({
apply({ rects, elements }) {
apply({ availableHeight, elements, rects }) {
Object.assign(elements.floating.style, {
'min-width': `${rects.reference.width}px`,
minWidth: `${rects.reference.width}px`,
maxHeight: `${Math.max(
0,
Math.min(window.innerHeight / 2, availableHeight)
)}px`,
});
},
}),
],
open: isOpen,
placement: 'bottom-start',
whileElementsMounted: autoUpdate,
onOpenChange: setIsOpen,
});
const click = useClick(context);
@@ -214,12 +221,8 @@ function EnhancedSelectInput<T extends EnhancedSelectInputValue<V>, V>(
}, [value, values, isMultiSelect]);
const handlePress = useCallback(() => {
if (!isOpen && onOpen) {
onOpen();
}
setIsOpen(!isOpen);
}, [isOpen, setIsOpen, onOpen]);
setIsOpen((prevIsOpen) => !prevIsOpen);
}, []);
const handleSelect = useCallback(
(newValue: ArrayElement<V>) => {
@@ -372,6 +375,12 @@ function EnhancedSelectInput<T extends EnhancedSelectInputValue<V>, V>(
[onChange]
);
useEffect(() => {
if (isOpen) {
onOpen?.();
}
}, [isOpen, onOpen]);
return (
<>
<div ref={refs.setReference} {...getReferenceProps()}>
@@ -443,46 +452,43 @@ function EnhancedSelectInput<T extends EnhancedSelectInputValue<V>, V>(
</Link>
)}
</div>
{isOpen ? (
{!isMobile && isOpen ? (
<FloatingPortal id="portal-root">
<div
<Scroller
ref={refs.setFloating}
className={styles.optionsContainer}
className={styles.options}
style={floatingStyles}
{...getFloatingProps()}
>
{isOpen && !isMobile ? (
<Scroller className={styles.options}>
{values.map((v, index) => {
const hasParent = v.parentKey !== undefined;
const depth = hasParent ? 1 : 0;
const parentSelected =
v.parentKey !== undefined &&
Array.isArray(value) &&
value.includes(v.parentKey);
{values.map((v, index) => {
const hasParent = v.parentKey !== undefined;
const depth = hasParent ? 1 : 0;
const parentSelected =
v.parentKey !== undefined &&
Array.isArray(value) &&
value.includes(v.parentKey);
const { key, ...other } = v;
const { key, ...other } = v;
return (
<OptionComponent
key={v.key}
id={v.key}
depth={depth}
isSelected={isSelectedItem(index, value, values)}
isDisabled={parentSelected}
isMultiSelect={isMultiSelect}
{...valueOptions}
{...other}
isMobile={false}
onSelect={handleSelect}
>
{v.value}
</OptionComponent>
);
})}
</Scroller>
) : null}
</div>
return (
<OptionComponent
key={v.key}
id={v.key}
depth={depth}
isSelected={isSelectedItem(index, value, values)}
isDisabled={parentSelected}
isMultiSelect={isMultiSelect}
{...valueOptions}
{...other}
isMobile={false}
onSelect={handleSelect}
>
{v.value}
</OptionComponent>
);
})}
</Scroller>
</FloatingPortal>
) : null}

View File

@@ -88,13 +88,10 @@ function QualityProfileSelectInput({
);
const handleChange = useCallback(
({ value: newValue }: EnhancedSelectInputChanged<string | number>) => {
onChange({
name,
value: newValue === 'noChange' ? value : newValue,
});
({ value }: EnhancedSelectInputChanged<string | number>) => {
onChange({ name, value });
},
[name, value, onChange]
[name, onChange]
);
useEffect(() => {

View File

@@ -21,6 +21,7 @@ const ADD_NEW_KEY = 'addNew';
export interface RootFolderSelectInputValue
extends EnhancedSelectInputValue<string> {
freeSpace?: number;
isMissing?: boolean;
}
@@ -42,66 +43,58 @@ function createRootFolderOptionsSelector(
includeNoChange: boolean,
includeNoChangeDisabled: boolean
) {
return createSelector(
createRootFoldersSelector(),
(rootFolders) => {
const values: RootFolderSelectInputValue[] = rootFolders.items.map(
(rootFolder) => {
return {
key: rootFolder.path,
value: rootFolder.path,
freeSpace: rootFolder.freeSpace,
isMissing: false,
};
}
);
if (includeNoChange) {
values.unshift({
key: 'noChange',
get value() {
return translate('NoChange');
},
isDisabled: includeNoChangeDisabled,
return createSelector(createRootFoldersSelector(), (rootFolders) => {
const values: RootFolderSelectInputValue[] = rootFolders.items.map(
(rootFolder) => {
return {
key: rootFolder.path,
value: rootFolder.path,
freeSpace: rootFolder.freeSpace,
isMissing: false,
});
};
}
);
if (!values.length) {
values.push({
key: '',
value: '',
isDisabled: true,
isHidden: true,
});
}
if (
includeMissingValue &&
value &&
!values.find((v) => v.key === value)
) {
values.push({
key: value,
value,
isMissing: true,
isDisabled: true,
});
}
values.push({
key: ADD_NEW_KEY,
value: translate('AddANewPath'),
if (includeNoChange) {
values.unshift({
key: 'noChange',
get value() {
return translate('NoChange');
},
isDisabled: includeNoChangeDisabled,
isMissing: false,
});
return {
values,
isSaving: rootFolders.isSaving,
saveError: rootFolders.saveError,
};
}
);
if (!values.length) {
values.push({
key: '',
value: '',
isDisabled: true,
isHidden: true,
});
}
if (includeMissingValue && value && !values.find((v) => v.key === value)) {
values.push({
key: value,
value,
isMissing: true,
isDisabled: true,
});
}
values.push({
key: ADD_NEW_KEY,
value: translate('AddANewPath'),
});
return {
values,
isSaving: rootFolders.isSaving,
saveError: rootFolders.saveError,
};
});
}
function RootFolderSelectInput({

View File

@@ -18,18 +18,16 @@ interface RootFolderSelectInputOptionProps
isWindows?: boolean;
}
function RootFolderSelectInputOption(props: RootFolderSelectInputOptionProps) {
const {
id,
value,
freeSpace,
isMissing,
seriesFolder,
isMobile,
isWindows,
...otherProps
} = props;
function RootFolderSelectInputOption({
id,
value,
freeSpace,
isMissing,
seriesFolder,
isMobile,
isWindows,
...otherProps
}: RootFolderSelectInputOptionProps) {
const slashCharacter = isWindows ? '\\' : '/';
return (

View File

@@ -30,3 +30,11 @@
text-align: right;
font-size: $smallFontSize;
}
.isMissing {
flex: 0 0 auto;
margin-left: 15px;
color: var(--dangerColor);
text-align: right;
font-size: $smallFontSize;
}

View File

@@ -2,6 +2,7 @@
// Please do not change this file!
interface CssExports {
'freeSpace': string;
'isMissing': string;
'path': string;
'pathContainer': string;
'selectedValue': string;

View File

@@ -8,27 +8,23 @@ import styles from './RootFolderSelectInputSelectedValue.css';
interface RootFolderSelectInputSelectedValueProps {
selectedValue: string;
values: RootFolderSelectInputValue[];
freeSpace?: number;
seriesFolder?: string;
isWindows?: boolean;
includeFreeSpace?: boolean;
}
function RootFolderSelectInputSelectedValue(
props: RootFolderSelectInputSelectedValueProps
) {
const {
selectedValue,
values,
freeSpace,
seriesFolder,
includeFreeSpace = true,
isWindows,
...otherProps
} = props;
function RootFolderSelectInputSelectedValue({
selectedValue,
values,
seriesFolder,
includeFreeSpace = true,
isWindows,
...otherProps
}: RootFolderSelectInputSelectedValueProps) {
const slashCharacter = isWindows ? '\\' : '/';
const value = values.find((v) => v.key === selectedValue)?.value;
const { value, freeSpace, isMissing } =
values.find((v) => v.key === selectedValue) ||
({} as RootFolderSelectInputValue);
return (
<EnhancedSelectInputSelectedValue
@@ -53,6 +49,10 @@ function RootFolderSelectInputSelectedValue(
})}
</div>
) : null}
{isMissing ? (
<div className={styles.isMissing}>{translate('Missing')}</div>
) : null}
</EnhancedSelectInputSelectedValue>
);
}

View File

@@ -2,10 +2,12 @@
import React, { SyntheticEvent } from 'react';
import { InputChanged } from 'typings/inputs';
import translate from 'Utilities/String/translate';
import EnhancedSelectInput from './EnhancedSelectInput';
import EnhancedSelectInput, {
EnhancedSelectInputValue,
} from './EnhancedSelectInput';
import styles from './UMaskInput.css';
const umaskOptions = [
const umaskOptions: EnhancedSelectInputValue<string>[] = [
{
key: '755',
get value() {

View File

@@ -1,9 +1,15 @@
import classNames from 'classnames';
import React, { ChangeEvent, SyntheticEvent, useCallback } from 'react';
import React, {
ChangeEvent,
ComponentProps,
SyntheticEvent,
useCallback,
} from 'react';
import { InputChanged } from 'typings/inputs';
import styles from './SelectInput.css';
interface SelectInputOption {
export interface SelectInputOption
extends Pick<ComponentProps<'option'>, 'disabled'> {
key: string | number;
value: string | number | (() => string | number);
}

View File

@@ -1,4 +1,4 @@
import React, { useCallback } from 'react';
import React, { useCallback, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import { addTag } from 'Store/Actions/tagActions';
@@ -12,10 +12,10 @@ interface SeriesTag extends TagBase {
name: string;
}
export interface SeriesTagInputProps {
export interface SeriesTagInputProps<V> {
name: string;
value: number[];
onChange: (change: InputChanged<number[]>) => void;
value: V;
onChange: (change: InputChanged<V>) => void;
}
const VALID_TAG_REGEX = new RegExp('[^-_a-z0-9]', 'i');
@@ -59,28 +59,48 @@ function createSeriesTagsSelector(tags: number[]) {
});
}
export default function SeriesTagInput({
export default function SeriesTagInput<V extends number | number[]>({
name,
value,
onChange,
}: SeriesTagInputProps) {
}: SeriesTagInputProps<V>) {
const dispatch = useDispatch();
const isArray = Array.isArray(value);
const arrayValue = useMemo(() => {
if (isArray) {
return value as number[];
}
return value === 0 ? [] : [value as number];
}, [isArray, value]);
const { tags, tagList, allTags } = useSelector(
createSeriesTagsSelector(value)
createSeriesTagsSelector(arrayValue)
);
const handleTagCreated = useCallback(
(tag: SeriesTag) => {
onChange({ name, value: [...value, tag.id] });
if (isArray) {
onChange({ name, value: [...value, tag.id] as V });
} else {
onChange({
name,
value: tag.id as V,
});
}
},
[name, value, onChange]
[name, value, isArray, onChange]
);
const handleTagAdd = useCallback(
(newTag: SeriesTag) => {
if (newTag.id) {
onChange({ name, value: [...value, newTag.id] });
if (isArray) {
onChange({ name, value: [...value, newTag.id] as V });
} else {
onChange({ name, value: newTag.id as V });
}
return;
}
@@ -96,17 +116,21 @@ export default function SeriesTagInput({
);
}
},
[name, value, allTags, handleTagCreated, onChange, dispatch]
[name, value, isArray, allTags, handleTagCreated, onChange, dispatch]
);
const handleTagDelete = useCallback(
({ index }: { index: number }) => {
const newValue = value.slice();
newValue.splice(index, 1);
if (isArray) {
const newValue = value.slice();
newValue.splice(index, 1);
onChange({ name, value: newValue });
onChange({ name, value: newValue as V });
} else {
onChange({ name, value: 0 as V });
}
},
[name, value, onChange]
[name, value, isArray, onChange]
);
return (

View File

@@ -14,7 +14,7 @@ import {
RenderSuggestion,
SuggestionsFetchRequestedParams,
} from 'react-autosuggest';
import useDebouncedCallback from 'Helpers/Hooks/useDebouncedCallback';
import { useDebouncedCallback } from 'use-debounce';
import { Kind } from 'Helpers/Props/kinds';
import { InputChanged } from 'typings/inputs';
import AutoSuggestInput from '../AutoSuggestInput';

View File

@@ -58,16 +58,6 @@ function Menu({
onPress: handleMenuButtonPress,
});
const handleFloaterPress = useCallback((_event: MouseEvent) => {
// TODO: Menu items should handle closing when they are clicked.
// This is handled before the menu item click event is handled, so wait 100ms before closing.
setTimeout(() => {
setIsMenuOpen(false);
}, 100);
return true;
}, []);
const handleWindowResize = useCallback(() => {
updateMaxHeight();
}, [updateMaxHeight]);
@@ -118,8 +108,31 @@ function Menu({
onOpenChange: setIsMenuOpen,
});
const handleFloaterPress = useCallback(
(event: MouseEvent) => {
if (
refs.reference &&
(refs.reference.current as HTMLElement).contains(
event.target as HTMLElement
)
) {
return false;
}
// TODO: Menu items should handle closing when they are clicked.
// This is handled before the menu item click event is handled, so wait 100ms before closing.
setTimeout(() => {
setIsMenuOpen(false);
}, 100);
return true;
},
[refs.reference]
);
const click = useClick(context);
const dismiss = useDismiss(context, {
outsidePressEvent: 'click',
outsidePress: handleFloaterPress,
});

View File

@@ -13,10 +13,10 @@ import React, {
import Autosuggest from 'react-autosuggest';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import { useDebouncedCallback } from 'use-debounce';
import { Tag } from 'App/State/TagsAppState';
import Icon from 'Components/Icon';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import useDebouncedCallback from 'Helpers/Hooks/useDebouncedCallback';
import useKeyboardShortcuts from 'Helpers/Hooks/useKeyboardShortcuts';
import { icons } from 'Helpers/Props';
import Series from 'Series/Series';
@@ -316,7 +316,7 @@ function SeriesSearchInput() {
return;
}
// If an suggestion is not selected go to the first series,
// If a suggestion is not selected go to the first series,
// otherwise go to the selected series.
const selectedSuggestion =

View File

@@ -359,34 +359,37 @@ function PageSidebar({ isSidebarVisible, isSmallScreen }: PageSidebarProps) {
});
}, []);
const handleTouchEnd = useCallback((event: TouchEvent) => {
const touches = event.changedTouches;
const currentTouch = touches[0].pageX;
const handleTouchEnd = useCallback(
(event: TouchEvent) => {
const touches = event.changedTouches;
const currentTouch = touches[0].pageX;
if (!touchStartX.current) {
return;
}
if (!touchStartX.current) {
return;
}
if (currentTouch > touchStartX.current && currentTouch > 50) {
setSidebarTransform({
transition: 'none',
transform: 0,
});
} else if (currentTouch < touchStartX.current && currentTouch < 80) {
setSidebarTransform({
transition: 'transform 50ms ease-in-out',
transform: SIDEBAR_WIDTH * -1,
});
} else {
setSidebarTransform({
transition: 'none',
transform: 0,
});
}
if (currentTouch > touchStartX.current && currentTouch > 50) {
setSidebarTransform({
transition: 'none',
transform: 0,
});
} else if (currentTouch < touchStartX.current && currentTouch < 80) {
setSidebarTransform({
transition: 'transform 50ms ease-in-out',
transform: SIDEBAR_WIDTH * -1,
});
} else {
setSidebarTransform({
transition: 'none',
transform: isSidebarVisible ? 0 : SIDEBAR_WIDTH * -1,
});
}
touchStartX.current = null;
touchStartY.current = null;
}, []);
touchStartX.current = null;
touchStartY.current = null;
},
[isSidebarVisible]
);
const handleTouchCancel = useCallback(() => {
touchStartX.current = null;

View File

@@ -80,8 +80,12 @@ function PageToolbarSection({
if (buttonCount - 1 === maxButtons) {
const overflowItems: PageToolbarButtonProps[] = [];
const buttonsWithoutSeparators = validChildren.filter(
(child) => Object.keys(child.props).length > 0
);
return {
buttons: validChildren,
buttons: buttonsWithoutSeparators,
buttonCount,
overflowItems,
};

View File

@@ -1,6 +1,6 @@
import classNames from 'classnames';
import React, { useCallback, useMemo, useState } from 'react';
import SelectInput from 'Components/Form/SelectInput';
import SelectInput, { SelectInputOption } from 'Components/Form/SelectInput';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
@@ -34,7 +34,7 @@ function TablePager({
const isLastPage = page === totalPages;
const pages = useMemo(() => {
return Array.from(new Array(totalPages), (_x, i) => {
return Array.from(new Array(totalPages), (_x, i): SelectInputOption => {
const pageNumber = i + 1;
return {

View File

@@ -22,10 +22,6 @@ interface Episode extends ModelBase {
monitored: boolean;
grabbed?: boolean;
unverifiedSceneNumbering: boolean;
endTime?: string;
grabDate?: string;
seriesTitle?: string;
queued?: boolean;
series?: Series;
finaleType?: string;
}

View File

@@ -1,16 +0,0 @@
import { debounce, DebouncedFunc, DebounceSettings } from 'lodash';
import { useCallback } from 'react';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default function useDebouncedCallback<T extends (...args: any) => any>(
callback: T,
delay: number,
options?: DebounceSettings
): DebouncedFunc<T> {
// eslint-disable-next-line react-hooks/exhaustive-deps
return useCallback(debounce(callback, delay, options), [
callback,
delay,
options,
]);
}

View File

@@ -5,7 +5,7 @@ import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
import * as commandNames from 'Commands/commandNames';
import SelectInput from 'Components/Form/SelectInput';
import SelectInput, { SelectInputOption } from 'Components/Form/SelectInput';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import SpinnerButton from 'Components/Link/SpinnerButton';
@@ -164,7 +164,7 @@ const COLUMNS = [
},
];
const importModeOptions = [
const importModeOptions: SelectInputOption[] = [
{
key: 'chooseImportMode',
value: () => translate('ChooseImportMode'),
@@ -343,7 +343,7 @@ function InteractiveImportModalContent(
}
);
const options = [
const options: SelectInputOption[] = [
{
key: 'select',
value: translate('SelectDropdown'),

View File

@@ -7,6 +7,7 @@ import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ModalBody from 'Components/Modal/ModalBody';
@@ -69,7 +70,7 @@ function SelectQualityModalContent(props: SelectQualityModalContentProps) {
);
const qualityOptions = useMemo(() => {
return items.map(({ id, name }) => {
return items.map(({ id, name }): EnhancedSelectInputValue<number> => {
return {
key: id,
value: name,

View File

@@ -82,9 +82,9 @@ function RootFolderRow(props: RootFolderRowProps) {
<ConfirmModal
isOpen={isDeleteModalOpen}
kind={kinds.DANGER}
title={translate('DeleteRootFolder')}
message={translate('DeleteRootFolderMessageText', { path })}
confirmLabel={translate('Delete')}
title={translate('RemoveRootFolder')}
message={translate('RemoveRootFolderWithSeriesMessageText', { path })}
confirmLabel={translate('Remove')}
onConfirm={onConfirmDelete}
onCancel={onDeleteModalClose}
/>

View File

@@ -5,7 +5,6 @@
.header {
position: relative;
width: 100%;
height: 425px;
}
.backdrop {
@@ -30,20 +29,18 @@
width: 100%;
height: 100%;
color: var(--white);
gap: 35px;
}
.poster {
flex-shrink: 0;
margin-right: 35px;
width: 250px;
height: 368px;
}
.info {
display: flex;
flex-direction: column;
flex-grow: 1;
overflow: hidden;
width: 100%;
}
.titleRow {
@@ -59,10 +56,13 @@
}
.title {
overflow: auto;
max-height: calc(3 * 50px);
text-wrap: balance;
font-weight: 300;
font-size: 50px;
line-height: 50px;
line-clamp: 3;
}
.toggleMonitoredContainer {
@@ -170,6 +170,8 @@
}
.title {
overflow: hidden;
max-height: calc(3 * 30px);
font-weight: 300;
font-size: 30px;
line-height: 30px;

View File

@@ -1,7 +1,6 @@
import moment from 'moment';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import TextTruncate from 'react-text-truncate';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import * as commandNames from 'Commands/commandNames';
@@ -21,7 +20,6 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import Popover from 'Components/Tooltip/Popover';
import Tooltip from 'Components/Tooltip/Tooltip';
import useMeasure from 'Helpers/Hooks/useMeasure';
import usePrevious from 'Helpers/Hooks/usePrevious';
import {
align,
@@ -56,7 +54,6 @@ import {
import { toggleSeriesMonitored } from 'Store/Actions/seriesActions';
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
import fonts from 'Styles/Variables/fonts';
import sortByProp from 'Utilities/Array/sortByProp';
import { findCommand, isCommandExecuting } from 'Utilities/Command';
import formatBytes from 'Utilities/Number/formatBytes';
@@ -75,9 +72,6 @@ import SeriesProgressLabel from './SeriesProgressLabel';
import SeriesTags from './SeriesTags';
import styles from './SeriesDetails.css';
const defaultFontSize = parseInt(fonts.defaultFontSize);
const lineHeight = parseFloat(fonts.lineHeight);
function getFanartUrl(images: Image[]) {
return images.find((image) => image.coverType === 'fanart')?.url;
}
@@ -246,7 +240,6 @@ function SeriesDetails({ seriesId }: SeriesDetailsProps) {
allCollapsed: false,
seasons: {},
});
const [overviewRef, { height: overviewHeight }] = useMeasure();
const wasRefreshing = usePrevious(isRefreshing);
const wasRenaming = usePrevious(isRenaming);
@@ -523,7 +516,7 @@ function SeriesDetails({ seriesId }: SeriesDetailsProps) {
<PageToolbarSeparator />
<PageToolbarButton
label={translate('SeriesMonitoring')}
label={translate('EpisodeMonitoring')}
iconName={icons.MONITORED}
onPress={handleMonitorOptionsPress}
/>
@@ -796,16 +789,7 @@ function SeriesDetails({ seriesId }: SeriesDetailsProps) {
/>
</div>
<div ref={overviewRef} className={styles.overview}>
<TextTruncate
line={
Math.floor(
overviewHeight / (defaultFontSize * lineHeight)
) - 1
}
text={overview}
/>
</div>
<div className={styles.overview}>{overview}</div>
<MetadataAttribution />
</div>

View File

@@ -562,6 +562,7 @@ function SeriesDetailsSeason({
<SeasonInteractiveSearchModal
isOpen={isInteractiveSearchModalOpen}
episodeCount={totalEpisodeCount}
seriesId={seriesId}
seasonNumber={seasonNumber}
onModalClose={handleInteractiveSearchModalClose}

View File

@@ -4,6 +4,7 @@ import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
@@ -14,7 +15,7 @@ import { setSeriesOverviewOption } from 'Store/Actions/seriesIndexActions';
import translate from 'Utilities/String/translate';
import selectOverviewOptions from '../selectOverviewOptions';
const posterSizeOptions = [
const posterSizeOptions: EnhancedSelectInputValue<string>[] = [
{
key: 'small',
get value() {

View File

@@ -1,11 +0,0 @@
.grid {
flex: 1 0 auto;
}
.container {
&:hover {
.content {
background-color: var(--tableRowHoverBackgroundColor);
}
}
}

View File

@@ -80,17 +80,17 @@ function SeriesIndexOverviews(props: SeriesIndexOverviewsProps) {
const [size, setSize] = useState({ width: 0, height: 0 });
const posterWidth = useMemo(() => {
const maxiumPosterWidth = isSmallScreen ? 152 : 162;
const maximumPosterWidth = isSmallScreen ? 152 : 162;
if (posterSize === 'large') {
return maxiumPosterWidth;
return maximumPosterWidth;
}
if (posterSize === 'medium') {
return Math.floor(maxiumPosterWidth * 0.75);
return Math.floor(maximumPosterWidth * 0.75);
}
return Math.floor(maxiumPosterWidth * 0.5);
return Math.floor(maximumPosterWidth * 0.5);
}, [posterSize, isSmallScreen]);
const posterHeight = useMemo(() => {

View File

@@ -4,6 +4,7 @@ import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
@@ -14,7 +15,7 @@ import selectPosterOptions from 'Series/Index/Posters/selectPosterOptions';
import { setSeriesPosterOption } from 'Store/Actions/seriesIndexActions';
import translate from 'Utilities/String/translate';
const posterSizeOptions = [
const posterSizeOptions: EnhancedSelectInputValue<string>[] = [
{
key: 'small',
get value() {

View File

@@ -1,6 +1,7 @@
import classNames from 'classnames';
import React, { useCallback, useState } from 'react';
import React, { SyntheticEvent, useCallback, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useSelect } from 'App/SelectContext';
import { REFRESH_SERIES, SERIES_SEARCH } from 'Commands/commandNames';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
@@ -122,8 +123,31 @@ function SeriesIndexPoster(props: SeriesIndexPosterProps) {
setIsDeleteSeriesModalOpen(false);
}, [setIsDeleteSeriesModalOpen]);
const [selectState, selectDispatch] = useSelect();
const onSelectPress = useCallback(
(event: SyntheticEvent<HTMLElement, MouseEvent>) => {
if (event.nativeEvent.ctrlKey || event.nativeEvent.metaKey) {
window.open(`/series/${titleSlug}`, '_blank');
return;
}
const shiftKey = event.nativeEvent.shiftKey;
selectDispatch({
type: 'toggleSelected',
id: seriesId,
isSelected: !selectState.selectedState[seriesId],
shiftKey,
});
},
[seriesId, selectState.selectedState, selectDispatch, titleSlug]
);
const link = `/series/${titleSlug}`;
const linkProps = isSelectMode ? { onPress: onSelectPress } : { to: link };
const elementStyle = {
width: `${posterWidth}px`,
height: `${posterHeight}px`,
@@ -175,7 +199,7 @@ function SeriesIndexPoster(props: SeriesIndexPosterProps) {
/>
) : null}
<Link className={styles.link} style={elementStyle} to={link}>
<Link className={styles.link} style={elementStyle} {...linkProps}>
<SeriesPoster
style={elementStyle}
images={images}

View File

@@ -2,6 +2,7 @@ import React, { useCallback, useState } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
@@ -31,7 +32,7 @@ interface EditSeriesModalContentProps {
const NO_CHANGE = 'noChange';
const monitoredOptions = [
const monitoredOptions: EnhancedSelectInputValue<string>[] = [
{
key: NO_CHANGE,
get value() {
@@ -53,7 +54,7 @@ const monitoredOptions = [
},
];
const seasonFolderOptions = [
const seasonFolderOptions: EnhancedSelectInputValue<string>[] = [
{
key: NO_CHANGE,
get value() {

View File

@@ -1,5 +1,6 @@
import React, { useCallback, useState } from 'react';
import SeriesMonitoringOptionsPopoverContent from 'AddSeries/SeriesMonitoringOptionsPopoverContent';
import Alert from 'Components/Alert';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
@@ -11,7 +12,7 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import Popover from 'Components/Tooltip/Popover';
import { icons, inputTypes, tooltipPositions } from 'Helpers/Props';
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './ChangeMonitoringModalContent.css';
@@ -46,9 +47,12 @@ function ChangeMonitoringModalContent(
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('MonitorSeries')}</ModalHeader>
<ModalHeader>{translate('MonitorEpisodes')}</ModalHeader>
<ModalBody>
<Alert kind={kinds.INFO}>
<div>{translate('MonitorEpisodesModalInfo')}</div>
</Alert>
<Form {...otherProps}>
<FormGroup>
<FormLabel>

View File

@@ -6,6 +6,7 @@ import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import Label from 'Components/Label';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
@@ -66,7 +67,7 @@ function TagsModalContent(props: TagsModalContentProps) {
onApplyTagsPress(tags, applyTags);
}, [tags, applyTags, onApplyTagsPress]);
const applyTagsOptions = [
const applyTagsOptions: EnhancedSelectInputValue<string>[] = [
{
key: 'add',
value: translate('Add'),

View File

@@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import SeriesMonitoringOptionsPopoverContent from 'AddSeries/SeriesMonitoringOptionsPopoverContent';
import AppState from 'App/State/AppState';
import Alert from 'Components/Alert';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
@@ -15,7 +16,7 @@ import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import Popover from 'Components/Tooltip/Popover';
import usePrevious from 'Helpers/Hooks/usePrevious';
import { icons, tooltipPositions } from 'Helpers/Props';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import { updateSeriesMonitor } from 'Store/Actions/seriesActions';
import { InputChanged } from 'typings/inputs';
import translate from 'Utilities/String/translate';
@@ -66,9 +67,12 @@ function MonitoringOptionsModalContent({
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('MonitorSeries')}</ModalHeader>
<ModalHeader>{translate('MonitorEpisodes')}</ModalHeader>
<ModalBody>
<Alert kind={kinds.INFO}>
<div>{translate('MonitorEpisodesModalInfo')}</div>
</Alert>
<Form>
<FormGroup>
<FormLabel>

View File

@@ -6,19 +6,19 @@ import {
cancelFetchReleases,
clearReleases,
} from 'Store/Actions/releaseActions';
import SeasonInteractiveSearchModalContent from './SeasonInteractiveSearchModalContent';
import SeasonInteractiveSearchModalContent, {
SeasonInteractiveSearchModalContentProps,
} from './SeasonInteractiveSearchModalContent';
interface SeasonInteractiveSearchModalProps {
interface SeasonInteractiveSearchModalProps
extends SeasonInteractiveSearchModalContentProps {
isOpen: boolean;
seriesId: number;
seasonNumber: number;
onModalClose(): void;
}
function SeasonInteractiveSearchModal(
props: SeasonInteractiveSearchModalProps
) {
const { isOpen, seriesId, seasonNumber, onModalClose } = props;
const { isOpen, episodeCount, seriesId, seasonNumber, onModalClose } = props;
const dispatch = useDispatch();
@@ -44,6 +44,7 @@ function SeasonInteractiveSearchModal(
onModalClose={handleModalClose}
>
<SeasonInteractiveSearchModalContent
episodeCount={episodeCount}
seriesId={seriesId}
seasonNumber={seasonNumber}
onModalClose={handleModalClose}

View File

@@ -0,0 +1,5 @@
.modalFooter {
composes: modalFooter from '~Components/Modal/ModalFooter.css';
justify-content: space-between;
}

View File

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

View File

@@ -8,18 +8,21 @@ import { scrollDirections } from 'Helpers/Props';
import InteractiveSearch from 'InteractiveSearch/InteractiveSearch';
import formatSeason from 'Season/formatSeason';
import translate from 'Utilities/String/translate';
import styles from './SeasonInteractiveSearchModalContent.css';
interface SeasonInteractiveSearchModalContentProps {
export interface SeasonInteractiveSearchModalContentProps {
episodeCount: number;
seriesId: number;
seasonNumber: number;
onModalClose(): void;
}
function SeasonInteractiveSearchModalContent(
props: SeasonInteractiveSearchModalContentProps
) {
const { seriesId, seasonNumber, onModalClose } = props;
function SeasonInteractiveSearchModalContent({
episodeCount,
seriesId,
seasonNumber,
onModalClose,
}: SeasonInteractiveSearchModalContentProps) {
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
@@ -40,7 +43,13 @@ function SeasonInteractiveSearchModalContent(
/>
</ModalBody>
<ModalFooter>
<ModalFooter className={styles.modalFooter}>
<div>
{translate('EpisodesInSeason', {
episodeCount,
})}
</div>
<Button onPress={onModalClose}>{translate('Close')}</Button>
</ModalFooter>
</ModalContent>

View File

@@ -2,6 +2,7 @@ import React, { useCallback, useState } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
@@ -23,7 +24,7 @@ interface ManageCustomFormatsEditModalContentProps {
const NO_CHANGE = 'noChange';
const enableOptions = [
const enableOptions: EnhancedSelectInputValue<string>[] = [
{
key: NO_CHANGE,
get value() {

View File

@@ -2,6 +2,7 @@ import React, { useCallback, useState } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
@@ -27,7 +28,7 @@ interface ManageDownloadClientsEditModalContentProps {
const NO_CHANGE = 'noChange';
const enableOptions = [
const enableOptions: EnhancedSelectInputValue<string>[] = [
{
key: NO_CHANGE,
get value() {

View File

@@ -8,6 +8,7 @@ import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import Label from 'Components/Label';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
@@ -71,7 +72,7 @@ function TagsModalContent(props: TagsModalContentProps) {
onApplyTagsPress(tags, applyTags);
}, [tags, applyTags, onApplyTagsPress]);
const applyTagsOptions = [
const applyTagsOptions: EnhancedSelectInputValue<string>[] = [
{
key: 'add',
get value() {

View File

@@ -3,6 +3,7 @@ import FieldSet from 'Components/FieldSet';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import useShowAdvancedSettings from 'Helpers/Hooks/useShowAdvancedSettings';
import { inputTypes } from 'Helpers/Props';
import { InputChanged } from 'typings/inputs';
@@ -10,7 +11,7 @@ import { PendingSection } from 'typings/pending';
import General from 'typings/Settings/General';
import translate from 'Utilities/String/translate';
const logLevelOptions = [
const logLevelOptions: EnhancedSelectInputValue<string>[] = [
{
key: 'info',
get value() {

View File

@@ -3,6 +3,7 @@ import FieldSet from 'Components/FieldSet';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import { inputTypes, sizes } from 'Helpers/Props';
import { InputChanged } from 'typings/inputs';
import { PendingSection } from 'typings/pending';
@@ -32,7 +33,7 @@ function ProxySettings({
proxyBypassLocalAddresses,
onInputChange,
}: ProxySettingsProps) {
const proxyTypeOptions = [
const proxyTypeOptions: EnhancedSelectInputValue<string>[] = [
{
key: 'http',
value: translate('HttpHttps'),

View File

@@ -6,6 +6,7 @@ import FormGroup from 'Components/Form/FormGroup';
import FormInputButton from 'Components/Form/FormInputButton';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import Icon from 'Components/Icon';
import ClipboardButton from 'Components/Link/ClipboardButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
@@ -16,7 +17,7 @@ import { PendingSection } from 'typings/pending';
import General from 'typings/Settings/General';
import translate from 'Utilities/String/translate';
export const authenticationMethodOptions = [
export const authenticationMethodOptions: EnhancedSelectInputValue<string>[] = [
{
key: 'none',
get value() {
@@ -47,22 +48,23 @@ export const authenticationMethodOptions = [
},
];
export const authenticationRequiredOptions = [
{
key: 'enabled',
get value() {
return translate('Enabled');
export const authenticationRequiredOptions: EnhancedSelectInputValue<string>[] =
[
{
key: 'enabled',
get value() {
return translate('Enabled');
},
},
},
{
key: 'disabledForLocalAddresses',
get value() {
return translate('DisabledForLocalAddresses');
{
key: 'disabledForLocalAddresses',
get value() {
return translate('DisabledForLocalAddresses');
},
},
},
];
];
const certificateValidationOptions = [
const certificateValidationOptions: EnhancedSelectInputValue<string>[] = [
{
key: 'enabled',
get value() {

View File

@@ -3,6 +3,7 @@ import FieldSet from 'Components/FieldSet';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import useShowAdvancedSettings from 'Helpers/Hooks/useShowAdvancedSettings';
import { inputTypes, sizes } from 'Helpers/Props';
import useSystemStatus from 'System/useSystemStatus';
@@ -38,7 +39,7 @@ function UpdateSettings({
const usingExternalUpdateMechanism = packageUpdateMechanism !== 'builtIn';
const updateOptions = [];
const updateOptions: EnhancedSelectInputValue<string>[] = [];
if (usingExternalUpdateMechanism) {
updateOptions.push({

View File

@@ -4,6 +4,11 @@
width: 290px;
}
.nameContainer {
display: flex;
justify-content: space-between;
}
.name {
@add-mixin truncate;
@@ -12,6 +17,12 @@
font-size: 24px;
}
.cloneButton {
composes: button from '~Components/Link/IconButton.css';
height: 36px;
}
.enabled {
display: flex;
flex-wrap: wrap;

View File

@@ -1,9 +1,11 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'cloneButton': string;
'enabled': string;
'list': string;
'name': string;
'nameContainer': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -2,9 +2,10 @@ import React, { useCallback, useState } from 'react';
import { useDispatch } from 'react-redux';
import Card from 'Components/Card';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import TagList from 'Components/TagList';
import { kinds } from 'Helpers/Props';
import { icons, kinds } from 'Helpers/Props';
import { deleteImportList } from 'Store/Actions/settingsActions';
import useTags from 'Tags/useTags';
import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan';
@@ -18,6 +19,7 @@ interface ImportListProps {
enableAutomaticAdd: boolean;
tags: number[];
minRefreshInterval: string;
onCloneImportListPress: (id: number) => void;
}
function ImportList({
@@ -26,6 +28,7 @@ function ImportList({
enableAutomaticAdd,
tags,
minRefreshInterval,
onCloneImportListPress,
}: ImportListProps) {
const dispatch = useDispatch();
const tagList = useTags();
@@ -57,13 +60,26 @@ function ImportList({
dispatch(deleteImportList({ id }));
}, [id, dispatch]);
const handleCloneImportListPress = useCallback(() => {
onCloneImportListPress(id);
}, [id, onCloneImportListPress]);
return (
<Card
className={styles.list}
overlayContent={true}
onPress={handleEditImportListPress}
>
<div className={styles.name}>{name}</div>
<div className={styles.nameContainer}>
<div className={styles.name}>{name}</div>
<IconButton
className={styles.cloneButton}
title={translate('CloneImportList')}
name={icons.CLONE}
onPress={handleCloneImportListPress}
/>
</div>
<div className={styles.enabled}>
{enableAutomaticAdd ? (

View File

@@ -7,7 +7,10 @@ import Icon from 'Components/Icon';
import PageSectionContent from 'Components/Page/PageSectionContent';
import { icons } from 'Helpers/Props';
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
import { fetchImportLists } from 'Store/Actions/settingsActions';
import {
cloneImportList,
fetchImportLists,
} from 'Store/Actions/settingsActions';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import ImportListModel from 'typings/ImportList';
import sortByProp from 'Utilities/Array/sortByProp';
@@ -49,6 +52,14 @@ function ImportLists() {
setIsEditImportListModalOpen(false);
}, []);
const handleCloneImportListPress = useCallback(
(id: number) => {
dispatch(cloneImportList({ id }));
setIsEditImportListModalOpen(true);
},
[dispatch]
);
useEffect(() => {
dispatch(fetchImportLists());
dispatch(fetchRootFolders());
@@ -64,7 +75,13 @@ function ImportLists() {
>
<div className={styles.lists}>
{items.map((item) => {
return <ImportList key={item.id} {...item} />;
return (
<ImportList
key={item.id}
{...item}
onCloneImportListPress={handleCloneImportListPress}
/>
);
})}
<Card className={styles.addList} onPress={handleAddImportListPress}>

View File

@@ -2,6 +2,7 @@ import React, { useCallback, useState } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
@@ -26,7 +27,7 @@ interface ManageImportListsEditModalContentProps {
const NO_CHANGE = 'noChange';
const autoAddOptions = [
const autoAddOptions: EnhancedSelectInputValue<string>[] = [
{
key: NO_CHANGE,
get value() {

View File

@@ -8,6 +8,7 @@ import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import Label from 'Components/Label';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
@@ -69,7 +70,7 @@ function TagsModalContent(props: TagsModalContentProps) {
onApplyTagsPress(tags, applyTags);
}, [tags, applyTags, onApplyTagsPress]);
const applyTagsOptions = [
const applyTagsOptions: EnhancedSelectInputValue<string>[] = [
{
key: 'add',
get value() {

View File

@@ -6,6 +6,7 @@ import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import useShowAdvancedSettings from 'Helpers/Hooks/useShowAdvancedSettings';
import { inputTypes, kinds } from 'Helpers/Props';
@@ -19,7 +20,7 @@ import createSettingsSectionSelector from 'Store/Selectors/createSettingsSection
import translate from 'Utilities/String/translate';
const SECTION = 'importListOptions';
const cleanLibraryLevelOptions = [
const cleanLibraryLevelOptions: EnhancedSelectInputValue<string>[] = [
{
key: 'disabled',
get value() {

View File

@@ -2,6 +2,7 @@ import React, { useCallback, useState } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
@@ -28,7 +29,7 @@ interface ManageIndexersEditModalContentProps {
const NO_CHANGE = 'noChange';
const enableOptions = [
const enableOptions: EnhancedSelectInputValue<string>[] = [
{
key: NO_CHANGE,
get value() {

View File

@@ -8,6 +8,7 @@ import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import Label from 'Components/Label';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
@@ -69,7 +70,7 @@ function TagsModalContent(props: TagsModalContentProps) {
onApplyTagsPress(tags, applyTags);
}, [tags, applyTags, onApplyTagsPress]);
const applyTagsOptions = [
const applyTagsOptions: EnhancedSelectInputValue<string>[] = [
{
key: 'add',
get value() {

View File

@@ -7,6 +7,7 @@ import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
@@ -31,7 +32,7 @@ import AddRootFolder from './RootFolder/AddRootFolder';
const SECTION = 'mediaManagement';
const episodeTitleRequiredOptions = [
const episodeTitleRequiredOptions: EnhancedSelectInputValue<string>[] = [
{
key: 'always',
get value() {
@@ -52,7 +53,7 @@ const episodeTitleRequiredOptions = [
},
];
const rescanAfterRefreshOptions = [
const rescanAfterRefreshOptions: EnhancedSelectInputValue<string>[] = [
{
key: 'always',
get value() {
@@ -73,7 +74,7 @@ const rescanAfterRefreshOptions = [
},
];
const downloadPropersAndRepacksOptions = [
const downloadPropersAndRepacksOptions: EnhancedSelectInputValue<string>[] = [
{
key: 'preferAndUpgrade',
get value() {
@@ -94,7 +95,7 @@ const downloadPropersAndRepacksOptions = [
},
];
const fileDateOptions = [
const fileDateOptions: EnhancedSelectInputValue<string>[] = [
{
key: 'none',
get value() {
@@ -360,6 +361,24 @@ function MediaManagement() {
/>
</FormGroup>
) : null}
<FormGroup
advancedSettings={showAdvancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('UserRejectedExtensions')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="userRejectedExtensions"
helpTexts={[
translate('UserRejectedExtensionsHelpText'),
translate('UserRejectedExtensionsTextsExamples'),
]}
onChange={handleInputChange}
{...settings.userRejectedExtensions}
/>
</FormGroup>
</FieldSet>
) : null}

View File

@@ -9,6 +9,7 @@ import FormGroup from 'Components/Form/FormGroup';
import FormInputButton from 'Components/Form/FormInputButton';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
@@ -169,7 +170,7 @@ function Naming() {
const replaceIllegalCharacters =
hasSettings && settings.replaceIllegalCharacters.value;
const multiEpisodeStyleOptions = [
const multiEpisodeStyleOptions: EnhancedSelectInputValue<number>[] = [
{ key: 0, value: translate('Extend'), hint: 'S01E01-02-03' },
{ key: 1, value: translate('Duplicate'), hint: 'S01E01.S01E02' },
{ key: 2, value: translate('Repeat'), hint: 'S01E01E02E03' },
@@ -178,7 +179,7 @@ function Naming() {
{ key: 5, value: translate('PrefixedRange'), hint: 'S01E01-E03' },
];
const colonReplacementOptions = [
const colonReplacementOptions: EnhancedSelectInputValue<number>[] = [
{ key: 0, value: translate('Delete') },
{ key: 1, value: translate('ReplaceWithDash') },
{ key: 2, value: translate('ReplaceWithSpaceDash') },

View File

@@ -1,6 +1,6 @@
import React, { useCallback, useState } from 'react';
import FieldSet from 'Components/FieldSet';
import SelectInput from 'Components/Form/SelectInput';
import SelectInput, { SelectInputOption } from 'Components/Form/SelectInput';
import TextInput from 'Components/Form/TextInput';
import Button from 'Components/Link/Button';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
@@ -17,7 +17,15 @@ import TokenCase from './TokenCase';
import TokenSeparator from './TokenSeparator';
import styles from './NamingModal.css';
const separatorOptions: { key: TokenSeparator; value: string }[] = [
type SeparatorInputOption = Omit<SelectInputOption, 'key'> & {
key: TokenSeparator;
};
type CaseInputOption = Omit<SelectInputOption, 'key'> & {
key: TokenCase;
};
const separatorOptions: SeparatorInputOption[] = [
{
key: ' ',
get value() {
@@ -44,7 +52,7 @@ const separatorOptions: { key: TokenSeparator; value: string }[] = [
},
];
const caseOptions: { key: TokenCase; value: string }[] = [
const caseOptions: CaseInputOption[] = [
{
key: 'title',
get value() {

View File

@@ -7,6 +7,7 @@ import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import Button from 'Components/Link/Button';
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
@@ -41,7 +42,7 @@ const newDelayProfile: DelayProfile & { [key: string]: unknown } = {
tags: [],
};
const protocolOptions = [
const protocolOptions: EnhancedSelectInputValue<string>[] = [
{
key: 'preferUsenet',
get value() {

View File

@@ -289,7 +289,7 @@ function EditQualityProfileModalContent({
});
// @ts-expect-error - actions are not typed
dispatch(setQualityProfileValue({ name: 'items', newItems }));
dispatch(setQualityProfileValue({ name: 'items', value: newItems }));
},
[items, dispatch]
);

View File

@@ -54,7 +54,7 @@
}
.createGroupButton {
composes: buton from '~Components/Link/IconButton.css';
composes: button from '~Components/Link/IconButton.css';
display: flex;
align-items: center;

View File

@@ -36,7 +36,7 @@ interface ItemProps {
preferredSize: number | null;
isInGroup?: boolean;
onCreateGroupPress?: (qualityId: number) => void;
onItemAllowedChange: (id: number, allowd: boolean) => void;
onItemAllowedChange: (id: number, allowed: boolean) => void;
}
interface GroupProps {
@@ -45,8 +45,8 @@ interface GroupProps {
items: QualityProfileQualityItem[];
qualityIndex: string;
onDeleteGroupPress: (groupId: number) => void;
onItemAllowedChange: (id: number, allowd: boolean) => void;
onGroupAllowedChange: (id: number, allowd: boolean) => void;
onItemAllowedChange: (id: number, allowed: boolean) => void;
onGroupAllowedChange: (id: number, allowed: boolean) => void;
onItemGroupNameChange: (groupId: number, name: string) => void;
}
@@ -67,9 +67,9 @@ export type QualityProfileItemDragSourceProps = CommonProps &
export interface QualityProfileItemDragSourceActionProps {
onCreateGroupPress?: (qualityId: number) => void;
onItemAllowedChange: (id: number, allowd: boolean) => void;
onItemAllowedChange: (id: number, allowed: boolean) => void;
onDeleteGroupPress: (groupId: number) => void;
onGroupAllowedChange: (id: number, allowd: boolean) => void;
onGroupAllowedChange: (id: number, allowed: boolean) => void;
onItemGroupNameChange: (groupId: number, name: string) => void;
onDragMove: (move: DragMoveState) => void;
onDragEnd: (didDrop: boolean) => void;

View File

@@ -79,7 +79,7 @@
}
.deleteGroupButton {
composes: buton from '~Components/Link/IconButton.css';
composes: button from '~Components/Link/IconButton.css';
display: flex;
align-items: center;

View File

@@ -183,6 +183,7 @@ export default function QualityProfileItemSize({
// @ts-ignore allowCross is still available in the version currently used
allowCross={false}
snapDragDisabled={true}
pearling={true}
renderThumb={thumbRenderer}
renderTrack={trackRenderer}
onChange={handleSliderChange}
@@ -243,7 +244,7 @@ export default function QualityProfileItemSize({
max={preferredSize ? preferredSize - 5 : MAX - 5}
step={0.1}
isFloat={true}
// @ts-expect-error - Typngs are too loose
// @ts-expect-error - Typings are too loose
onChange={handleMinSizeChange}
/>
<Label kind={kinds.INFO}>
@@ -261,7 +262,7 @@ export default function QualityProfileItemSize({
max={maxSize ? maxSize - 5 : MAX - 5}
step={0.1}
isFloat={true}
// @ts-expect-error - Typngs are too loose
// @ts-expect-error - Typings are too loose
onChange={handlePreferredSizeChange}
/>
@@ -280,7 +281,7 @@ export default function QualityProfileItemSize({
max={MAX}
step={0.1}
isFloat={true}
// @ts-expect-error - Typngs are too loose
// @ts-expect-error - Typings are too loose
onChange={handleMaxSizeChange}
/>

View File

@@ -55,13 +55,13 @@ function Tag({ id, label }: TagProps) {
}, []);
const handleConfirmDeleteTag = useCallback(() => {
setIsDeleteTagModalOpen(false);
}, []);
const handleDeleteTagModalClose = useCallback(() => {
dispatch(deleteTag({ id }));
}, [id, dispatch]);
const handleDeleteTagModalClose = useCallback(() => {
setIsDeleteTagModalOpen(false);
}, []);
return (
<Card
className={styles.tag}

View File

@@ -6,6 +6,7 @@ import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
@@ -25,7 +26,7 @@ import translate from 'Utilities/String/translate';
const SECTION = 'ui';
export const firstDayOfWeekOptions = [
export const firstDayOfWeekOptions: EnhancedSelectInputValue<number>[] = [
{
key: 0,
get value() {
@@ -40,14 +41,14 @@ export const firstDayOfWeekOptions = [
},
];
export const weekColumnOptions = [
export const weekColumnOptions: EnhancedSelectInputValue<string>[] = [
{ key: 'ddd M/D', value: 'Tue 3/25', hint: 'ddd M/D' },
{ key: 'ddd MM/DD', value: 'Tue 03/25', hint: 'ddd MM/DD' },
{ key: 'ddd D/M', value: 'Tue 25/3', hint: 'ddd D/M' },
{ key: 'ddd DD/MM', value: 'Tue 25/03', hint: 'ddd DD/MM' },
];
const shortDateFormatOptions = [
const shortDateFormatOptions: EnhancedSelectInputValue<string>[] = [
{ key: 'MMM D YYYY', value: 'Mar 25 2014', hint: 'MMM D YYYY' },
{ key: 'DD MMM YYYY', value: '25 Mar 2014', hint: 'DD MMM YYYY' },
{ key: 'MM/D/YYYY', value: '03/25/2014', hint: 'MM/D/YYYY' },
@@ -56,12 +57,12 @@ const shortDateFormatOptions = [
{ key: 'YYYY-MM-DD', value: '2014-03-25', hint: 'YYYY-MM-DD' },
];
const longDateFormatOptions = [
const longDateFormatOptions: EnhancedSelectInputValue<string>[] = [
{ key: 'dddd, MMMM D YYYY', value: 'Tuesday, March 25, 2014' },
{ key: 'dddd, D MMMM YYYY', value: 'Tuesday, 25 March, 2014' },
];
export const timeFormatOptions = [
export const timeFormatOptions: EnhancedSelectInputValue<string>[] = [
{ key: 'h(:mm)a', value: '5pm/5:30pm' },
{ key: 'HH:mm', value: '17:00/17:30' },
];

View File

@@ -10,7 +10,10 @@ import createTestProviderHandler, { createCancelTestProviderHandler } from 'Stor
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import { createThunk } from 'Store/thunks';
import getSectionState from 'Utilities/State/getSectionState';
import selectProviderSchema from 'Utilities/State/selectProviderSchema';
import updateSectionState from 'Utilities/State/updateSectionState';
import translate from 'Utilities/String/translate';
//
// Variables
@@ -33,6 +36,7 @@ export const CANCEL_TEST_IMPORT_LIST = 'settings/importLists/cancelTestImportLis
export const TEST_ALL_IMPORT_LISTS = 'settings/importLists/testAllImportLists';
export const BULK_EDIT_IMPORT_LISTS = 'settings/importLists/bulkEditImportLists';
export const BULK_DELETE_IMPORT_LISTS = 'settings/importLists/bulkDeleteImportLists';
export const CLONE_IMPORT_LIST = 'settings/importLists/cloneImportList';
//
// Action Creators
@@ -64,6 +68,8 @@ export const setImportListFieldValue = createAction(SET_IMPORT_LIST_FIELD_VALUE,
};
});
export const cloneImportList = createAction(CLONE_IMPORT_LIST);
//
// Details
@@ -127,6 +133,37 @@ export default {
return selectedSchema;
});
},
[CLONE_IMPORT_LIST]: (state, { payload }) => {
const id = payload.id;
const newState = getSectionState(state, section);
const item = newState.items.find((i) => i.id === id);
const selectedSchema = { ...item };
delete selectedSchema.id;
delete selectedSchema.name;
// Use selectedSchema so `createProviderSettingsSelector` works properly
selectedSchema.fields = selectedSchema.fields.map((field) => {
const newField = { ...field };
if (newField.privacy === 'apiKey' || newField.privacy === 'password') {
newField.value = '';
}
return newField;
});
newState.selectedSchema = selectedSchema;
const pendingChanges = { ...item, id: 0 };
delete pendingChanges.id;
pendingChanges.name = translate('DefaultNameCopiedImportList', { name: pendingChanges.name });
newState.pendingChanges = pendingChanges;
return updateSectionState(state, section, newState);
}
}

View File

@@ -31,6 +31,11 @@ export const defaultState = {
includeUnknownSeriesItems: true
},
removalOptions: {
removalMethod: 'removeFromClient',
blocklistMethod: 'doNotBlocklist'
},
status: {
isFetching: false,
isPopulated: false,
@@ -225,6 +230,7 @@ export const defaultState = {
export const persistState = [
'queue.options',
'queue.removalOptions',
'queue.paged.pageSize',
'queue.paged.sortKey',
'queue.paged.sortDirection',
@@ -257,6 +263,7 @@ export const SET_QUEUE_SORT = 'queue/setQueueSort';
export const SET_QUEUE_FILTER = 'queue/setQueueFilter';
export const SET_QUEUE_TABLE_OPTION = 'queue/setQueueTableOption';
export const SET_QUEUE_OPTION = 'queue/setQueueOption';
export const SET_QUEUE_REMOVAL_OPTION = 'queue/setQueueRemoveOption';
export const CLEAR_QUEUE = 'queue/clearQueue';
export const GRAB_QUEUE_ITEM = 'queue/grabQueueItem';
@@ -282,6 +289,7 @@ export const setQueueSort = createThunk(SET_QUEUE_SORT);
export const setQueueFilter = createThunk(SET_QUEUE_FILTER);
export const setQueueTableOption = createAction(SET_QUEUE_TABLE_OPTION);
export const setQueueOption = createAction(SET_QUEUE_OPTION);
export const setQueueRemovalOption = createAction(SET_QUEUE_REMOVAL_OPTION);
export const clearQueue = createAction(CLEAR_QUEUE);
export const grabQueueItem = createThunk(GRAB_QUEUE_ITEM);
@@ -529,6 +537,18 @@ export const reducers = createHandleActions({
};
},
[SET_QUEUE_REMOVAL_OPTION]: function(state, { payload }) {
const queueRemovalOptions = state.removalOptions;
return {
...state,
removalOptions: {
...queueRemovalOptions,
...payload
}
};
},
[CLEAR_QUEUE]: createClearReducer(paged, {
isFetching: false,
isPopulated: false,

View File

@@ -377,7 +377,7 @@ export const reducers = createHandleActions({
const items = newState.items;
const index = items.findIndex((item) => item.guid === guid);
// Don't try to update if there isnt a matching item (the user closed the modal)
// Don't try to update if there isn't a matching item (the user closed the modal)
if (index >= 0) {
const item = Object.assign({}, items[index], payload);

View File

@@ -2,8 +2,8 @@ import { createSelector } from 'reselect';
import { isCommandExecuting } from 'Utilities/Command';
import createCommandSelector from './createCommandSelector';
function createCommandExecutingSelector(name: string, contraints = {}) {
return createSelector(createCommandSelector(name, contraints), (command) => {
function createCommandExecutingSelector(name: string, constraints = {}) {
return createSelector(createCommandSelector(name, constraints), (command) => {
return command ? isCommandExecuting(command) : false;
});
}

View File

@@ -2,9 +2,9 @@ import { createSelector } from 'reselect';
import { findCommand } from 'Utilities/Command';
import createCommandsSelector from './createCommandsSelector';
function createCommandSelector(name: string, contraints = {}) {
function createCommandSelector(name: string, constraints = {}) {
return createSelector(createCommandsSelector(), (commands) => {
return findCommand(commands, { name, ...contraints });
return findCommand(commands, { name, ...constraints });
});
}

View File

@@ -7,8 +7,9 @@ function formatBitrate(input: string | number) {
return '';
}
const { value, symbol } = filesize(size, {
const { value, symbol } = filesize(size / 8, {
base: 10,
bits: true,
round: 1,
output: 'object',
});

View File

@@ -1,6 +1,7 @@
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import translate from 'Utilities/String/translate';
const monitorNewItemsOptions = [
const monitorNewItemsOptions: EnhancedSelectInputValue<string>[] = [
{
key: 'all',
get value() {

View File

@@ -72,7 +72,7 @@ function Missing() {
} = useSelector((state: AppState) => state.wanted.missing);
const isSearchingForAllEpisodes = useSelector(
createCommandExecutingSelector(commandNames.CUTOFF_UNMET_EPISODE_SEARCH)
createCommandExecutingSelector(commandNames.MISSING_EPISODE_SEARCH)
);
const isSearchingForSelectedEpisodes = useSelector(
createCommandExecutingSelector(commandNames.EPISODE_SEARCH)
@@ -155,7 +155,7 @@ function Missing() {
const handleSearchAllMissingConfirmed = useCallback(() => {
dispatch(
executeCommand({
name: commandNames.CUTOFF_UNMET_EPISODE_SEARCH,
name: commandNames.MISSING_EPISODE_SEARCH,
commandFinished: () => {
dispatch(fetchMissing());
},
@@ -353,11 +353,11 @@ function Missing() {
<ConfirmModal
isOpen={isConfirmSearchAllModalOpen}
kind={kinds.DANGER}
title={translate('SearchForMissingEpisodes')}
title={translate('SearchForAllMissingEpisodes')}
message={
<div>
<div>
{translate('SearchForMissingEpisodesConfirmationCount', {
{translate('SearchForAllMissingEpisodesConfirmationCount', {
totalRecords,
})}
</div>

View File

@@ -8,7 +8,7 @@ window.console.debug = window.console.debug || function() {};
window.console.warn = window.console.warn || function() {};
window.console.assert = window.console.assert || function() {};
// TODO: Remove in v5, well suppoprted in browsers
// TODO: Remove in v5, well supported in browsers
if (!String.prototype.startsWith) {
Object.defineProperty(String.prototype, 'startsWith', {
enumerable: false,
@@ -21,7 +21,7 @@ if (!String.prototype.startsWith) {
});
}
// TODO: Remove in v5, well suppoprted in browsers
// TODO: Remove in v5, well supported in browsers
if (!String.prototype.endsWith) {
Object.defineProperty(String.prototype, 'endsWith', {
enumerable: false,

View File

@@ -18,5 +18,6 @@ export default interface MediaManagement {
scriptImportPath: string;
importExtraFiles: boolean;
extraFileExtensions: string;
userRejectedExtensions: string;
enableMediaInfo: boolean;
}

View File

@@ -22,24 +22,24 @@
],
"dependencies": {
"@floating-ui/react": "0.27.5",
"@fortawesome/fontawesome-free": "6.7.1",
"@fortawesome/fontawesome-svg-core": "6.7.1",
"@fortawesome/free-regular-svg-icons": "6.7.1",
"@fortawesome/free-solid-svg-icons": "6.7.1",
"@fortawesome/fontawesome-free": "6.7.2",
"@fortawesome/fontawesome-svg-core": "6.7.2",
"@fortawesome/free-regular-svg-icons": "6.7.2",
"@fortawesome/free-solid-svg-icons": "6.7.2",
"@fortawesome/react-fontawesome": "0.2.2",
"@juggle/resize-observer": "3.4.0",
"@microsoft/signalr": "8.0.7",
"@sentry/browser": "7.119.1",
"@tanstack/react-query": "5.61.0",
"@types/node": "20.16.11",
"@types/react": "18.3.12",
"@types/react": "18.3.21",
"@types/react-dom": "18.3.1",
"classnames": "2.5.1",
"connected-react-router": "6.9.3",
"copy-to-clipboard": "3.3.3",
"element-class": "0.2.2",
"filesize": "10.1.6",
"fuse.js": "7.0.0",
"fuse.js": "7.1.0",
"history": "4.10.1",
"jdu": "1.0.0",
"jquery": "3.7.1",
@@ -64,7 +64,7 @@
"react-dom": "18.3.1",
"react-focus-lock": "2.9.4",
"react-google-recaptcha": "2.1.0",
"react-lazyload": "3.2.0",
"react-lazyload": "3.2.1",
"react-measure": "1.4.7",
"react-redux": "7.2.4",
"react-router": "5.2.0",
@@ -72,8 +72,8 @@
"react-slider": "1.1.4",
"react-tabs": "4.3.0",
"react-text-truncate": "0.19.0",
"react-use-measure": "2.1.1",
"react-window": "1.8.10",
"react-use-measure": "2.1.7",
"react-window": "1.8.11",
"redux": "4.2.1",
"redux-actions": "2.6.5",
"redux-batched-actions": "0.5.0",
@@ -82,16 +82,17 @@
"reselect": "4.1.8",
"stacktrace-js": "2.0.2",
"typescript": "5.7.2",
"use-debounce": "10.0.4",
"zustand": "5.0.3"
},
"devDependencies": {
"@babel/core": "7.26.0",
"@babel/eslint-parser": "7.25.9",
"@babel/plugin-proposal-export-default-from": "7.25.9",
"@babel/core": "7.27.1",
"@babel/eslint-parser": "7.27.1",
"@babel/plugin-proposal-export-default-from": "7.27.1",
"@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/preset-env": "7.26.0",
"@babel/preset-react": "7.26.3",
"@babel/preset-typescript": "7.26.0",
"@babel/preset-env": "7.27.2",
"@babel/preset-react": "7.27.1",
"@babel/preset-typescript": "7.27.1",
"@types/lodash": "4.14.195",
"@types/mousetrap": "1.6.15",
"@types/qs": "6.9.16",
@@ -111,7 +112,7 @@
"babel-loader": "9.2.1",
"babel-plugin-inline-classnames": "2.0.1",
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
"core-js": "3.39.0",
"core-js": "3.42.0",
"css-loader": "6.7.3",
"css-modules-typescript-loader": "4.0.1",
"eslint": "8.57.1",

View File

@@ -5,7 +5,7 @@
<ItemGroup>
<PackageReference Include="GitHubActionsTestLogger" Version="2.4.1" />
<PackageReference Include="Selenium.Support" Version="3.141.0" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="111.0.5563.6400" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="134.0.6998.16500" />
<PackageReference Include="StyleCop.Analyzers.Unstable" Version="1.2.0.556">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -15,7 +15,7 @@ namespace NzbDrone.Common.EnvironmentInfo
var attributes = assembly.GetCustomAttributes(true);
Branch = "unknow";
Branch = "unknown";
var config = attributes.OfType<AssemblyConfigurationAttribute>().FirstOrDefault();
if (config != null)

View File

@@ -4,6 +4,8 @@ namespace NzbDrone.Common.Extensions
{
public static class DateTimeExtensions
{
public static readonly DateTime EpochTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
public static bool InNextDays(this DateTime dateTime, int days)
{
return InNext(dateTime, new TimeSpan(days, 0, 0, 0));
@@ -43,5 +45,10 @@ namespace NzbDrone.Common.Extensions
{
return dateTime.AddTicks(-(dateTime.Ticks % TimeSpan.TicksPerSecond));
}
public static DateTime WithTicksFrom(this DateTime dateTime, DateTime other)
{
return dateTime.WithoutTicks().AddTicks(other.Ticks % TimeSpan.TicksPerSecond);
}
}
}

View File

@@ -141,7 +141,7 @@ namespace NzbDrone.Common.Http.Dispatchers
}
catch (OperationCanceledException ex) when (cts.IsCancellationRequested)
{
throw new WebException("Http request timed out", ex.InnerException, WebExceptionStatus.Timeout, null);
throw new WebException("Http request timed out", ex, WebExceptionStatus.Timeout, null);
}
}

View File

@@ -390,5 +390,12 @@ namespace NzbDrone.Common.Http
return this;
}
public virtual HttpRequestBuilder AllowRedirect(bool allowAutoRedirect = true)
{
AllowAutoRedirect = allowAutoRedirect;
return this;
}
}
}

View File

@@ -4,27 +4,27 @@ namespace NzbDrone.Common.Instrumentation.Extensions
{
public static class LoggerExtensions
{
[MessageTemplateFormatMethod("message")]
public static void ProgressInfo(this Logger logger, string message, params object[] args)
{
var formattedMessage = string.Format(message, args);
LogProgressMessage(logger, LogLevel.Info, formattedMessage);
LogProgressMessage(logger, LogLevel.Info, message, args);
}
[MessageTemplateFormatMethod("message")]
public static void ProgressDebug(this Logger logger, string message, params object[] args)
{
var formattedMessage = string.Format(message, args);
LogProgressMessage(logger, LogLevel.Debug, formattedMessage);
LogProgressMessage(logger, LogLevel.Debug, message, args);
}
[MessageTemplateFormatMethod("message")]
public static void ProgressTrace(this Logger logger, string message, params object[] args)
{
var formattedMessage = string.Format(message, args);
LogProgressMessage(logger, LogLevel.Trace, formattedMessage);
LogProgressMessage(logger, LogLevel.Trace, message, args);
}
private static void LogProgressMessage(Logger logger, LogLevel level, string message)
private static void LogProgressMessage(Logger logger, LogLevel level, string message, object[] parameters)
{
var logEvent = new LogEventInfo(level, logger.Name, message);
var logEvent = new LogEventInfo(level, logger.Name, null, message, parameters);
logEvent.Properties.Add("Status", "");
logger.Log(logEvent);

View File

@@ -215,6 +215,7 @@ namespace NzbDrone.Common.Instrumentation
c.ForLogger("Microsoft.*").WriteToNil(LogLevel.Warn);
c.ForLogger("Microsoft.Hosting.Lifetime*").WriteToNil(LogLevel.Info);
c.ForLogger("Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware").WriteToNil(LogLevel.Fatal);
c.ForLogger("Sonarr.Http.Authentication.ApiKeyAuthenticationHandler").WriteToNil(LogLevel.Info);
});
}

View File

@@ -6,6 +6,7 @@ using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using NLog;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Model;
@@ -117,7 +118,9 @@ namespace NzbDrone.Common.Processes
UseShellExecute = false,
RedirectStandardError = true,
RedirectStandardOutput = true,
RedirectStandardInput = true
RedirectStandardInput = true,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8
};
if (environmentVariables != null)

View File

@@ -0,0 +1,88 @@
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Datastore.Migration;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Datastore.Migration
{
[TestFixture]
public class nzb_su_url_to_nzb_lifeFixture : MigrationTest<nzb_su_url_to_nzb_life>
{
[TestCase("Newznab", "https://api.nzb.su")]
[TestCase("Newznab", "http://api.nzb.su")]
public void should_replace_old_url(string impl, string baseUrl)
{
var db = WithMigrationTestDb(c =>
{
c.Insert.IntoTable("Indexers").Row(new
{
Name = "Nzb.su",
Implementation = impl,
Settings = new NewznabSettings219
{
BaseUrl = baseUrl,
ApiPath = "/api"
}.ToJson(),
ConfigContract = impl + "Settings",
EnableInteractiveSearch = false
});
});
var items = db.Query<IndexerDefinition219>("SELECT * FROM \"Indexers\"");
items.Should().HaveCount(1);
items.First().Settings.ToObject<NewznabSettings219>().BaseUrl.Should().Be(baseUrl.Replace("su", "life"));
}
[TestCase("Newznab", "https://api.indexer.com")]
public void should_not_replace_different_url(string impl, string baseUrl)
{
var db = WithMigrationTestDb(c =>
{
c.Insert.IntoTable("Indexers").Row(new
{
Name = "Indexer.com",
Implementation = impl,
Settings = new NewznabSettings219
{
BaseUrl = baseUrl,
ApiPath = "/api"
}.ToJson(),
ConfigContract = impl + "Settings",
EnableInteractiveSearch = false
});
});
var items = db.Query<IndexerDefinition219>("SELECT * FROM \"Indexers\"");
items.Should().HaveCount(1);
items.First().Settings.ToObject<NewznabSettings219>().BaseUrl.Should().Be(baseUrl);
}
}
internal class IndexerDefinition219
{
public int Id { get; set; }
public string Name { get; set; }
public JObject Settings { get; set; }
public int Priority { get; set; }
public string Implementation { get; set; }
public string ConfigContract { get; set; }
public bool EnableRss { get; set; }
public bool EnableAutomaticSearch { get; set; }
public bool EnableInteractiveSearch { get; set; }
public HashSet<int> Tags { get; set; }
public int DownloadClientId { get; set; }
public int SeasonSearchMaximumSingleEpisodeAge { get; set; }
}
internal class NewznabSettings219
{
public string BaseUrl { get; set; }
public string ApiPath { get; set; }
}
}

View File

@@ -115,6 +115,7 @@ namespace NzbDrone.Core.Test.DiskSpace
[TestCase("/var/lib/docker")]
[TestCase("/some/place/docker/aufs")]
[TestCase("/etc/network")]
[TestCase("/Volumes/.timemachine/ABC123456-A1BC-12A3B45678C9/2025-05-13-181401.backup")]
public void should_not_check_diskspace_for_irrelevant_mounts(string path)
{
var mount = new Mock<IMount>();

View File

@@ -27,6 +27,7 @@
"TvrageID":"4055",
"ImdbID":"0320037",
"InfoHash":"123",
"Tags": ["Subtitles"],
"DownloadURL":"https:\/\/broadcasthe.net\/torrents.php?action=download&id=123&authkey=123&torrent_pass=123"
},
"1234":{
@@ -54,8 +55,9 @@
"TvrageID":"38472",
"ImdbID":"2377081",
"InfoHash":"1234",
"Tags": [],
"DownloadURL":"https:\/\/broadcasthe.net\/torrents.php?action=download&id=1234&authkey=1234&torrent_pass=1234"
}},
"results":"117927"
}
}
}

View File

@@ -124,5 +124,34 @@
<newznab:attr name="nuked" value="0"/>
</item>
<item>
<title>title</title>
<guid isPermaLink="true">subs=eng</guid>
<link>link</link>
<comments>comments</comments>
<pubDate>Sat, 31 Aug 2024 12:28:40 +0300</pubDate>
<category>category</category>
<description>description</description>
<enclosure url="url" length="500" type="application/x-nzb"/>
<newznab:attr name="haspretime" value="0"/>
<newznab:attr name="nuked" value="0"/>
<newznab:attr name="subs" value="Eng"/>
</item>
<item>
<title>title</title>
<guid isPermaLink="true">subs=''</guid>
<link>link</link>
<comments>comments</comments>
<pubDate>Sat, 31 Aug 2024 12:28:40 +0300</pubDate>
<category>category</category>
<description>description</description>
<enclosure url="url" length="500" type="application/x-nzb"/>
<newznab:attr name="haspretime" value="0"/>
<newznab:attr name="nuked" value="0"/>
<newznab:attr name="subs" value=""/>
</item>
</channel>
</rss>

View File

@@ -0,0 +1,121 @@
using System.Collections.Generic;
using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Extras.Others;
using NzbDrone.Core.Housekeeping.Housekeepers;
using NzbDrone.Core.Languages;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
{
[TestFixture]
public class CleanupOrphanedExtraFilesFixture : DbTest<CleanupOrphanedExtraFiles, OtherExtraFile>
{
[Test]
public void should_delete_extra_files_that_dont_have_a_coresponding_series()
{
var episodeFile = Builder<EpisodeFile>.CreateNew()
.With(h => h.Quality = new QualityModel())
.With(h => h.Languages = new List<Language> { Language.English })
.BuildNew();
Db.Insert(episodeFile);
var extraFile = Builder<OtherExtraFile>.CreateNew()
.With(m => m.EpisodeFileId = episodeFile.Id)
.BuildNew();
Db.Insert(extraFile);
Subject.Clean();
AllStoredModels.Should().BeEmpty();
}
[Test]
public void should_not_delete_extra_files_that_have_a_coresponding_series()
{
var series = Builder<Series>.CreateNew()
.BuildNew();
var episodeFile = Builder<EpisodeFile>.CreateNew()
.With(h => h.Quality = new QualityModel())
.With(h => h.Languages = new List<Language> { Language.English })
.BuildNew();
Db.Insert(series);
Db.Insert(episodeFile);
var extraFile = Builder<OtherExtraFile>.CreateNew()
.With(m => m.SeriesId = series.Id)
.With(m => m.EpisodeFileId = episodeFile.Id)
.BuildNew();
Db.Insert(extraFile);
Subject.Clean();
AllStoredModels.Should().HaveCount(1);
}
[Test]
public void should_delete_extra_files_that_dont_have_a_coresponding_episode_file()
{
var series = Builder<Series>.CreateNew()
.BuildNew();
Db.Insert(series);
var extraFile = Builder<OtherExtraFile>.CreateNew()
.With(m => m.SeriesId = series.Id)
.With(m => m.EpisodeFileId = 10)
.BuildNew();
Db.Insert(extraFile);
Subject.Clean();
AllStoredModels.Should().BeEmpty();
}
[Test]
public void should_not_delete_extra_files_that_have_a_coresponding_episode_file()
{
var series = Builder<Series>.CreateNew()
.BuildNew();
var episodeFile = Builder<EpisodeFile>.CreateNew()
.With(h => h.Quality = new QualityModel())
.With(h => h.Languages = new List<Language> { Language.English })
.BuildNew();
Db.Insert(series);
Db.Insert(episodeFile);
var extraFile = Builder<OtherExtraFile>.CreateNew()
.With(m => m.SeriesId = series.Id)
.With(m => m.EpisodeFileId = episodeFile.Id)
.BuildNew();
Db.Insert(extraFile);
Subject.Clean();
AllStoredModels.Should().HaveCount(1);
}
[Test]
public void should_delete_extra_files_that_have_episodefileid_of_zero()
{
var series = Builder<Series>.CreateNew()
.BuildNew();
Db.Insert(series);
var extraFile = Builder<OtherExtraFile>.CreateNew()
.With(m => m.SeriesId = series.Id)
.With(m => m.EpisodeFileId = 0)
.BuildNew();
Db.Insert(extraFile);
Subject.Clean();
AllStoredModels.Should().HaveCount(0);
}
}
}

View File

@@ -0,0 +1,126 @@
using System.Collections.Generic;
using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Extras.Subtitles;
using NzbDrone.Core.Housekeeping.Housekeepers;
using NzbDrone.Core.Languages;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
{
[TestFixture]
public class CleanupOrphanedSubtitleFilesFixture : DbTest<CleanupOrphanedSubtitleFiles, SubtitleFile>
{
[Test]
public void should_delete_subtitle_files_that_dont_have_a_coresponding_series()
{
var episodeFile = Builder<EpisodeFile>.CreateNew()
.With(h => h.Quality = new QualityModel())
.With(h => h.Languages = new List<Language> { Language.English })
.BuildNew();
Db.Insert(episodeFile);
var subtitleFile = Builder<SubtitleFile>.CreateNew()
.With(m => m.EpisodeFileId = episodeFile.Id)
.With(m => m.Language = Language.English)
.BuildNew();
Db.Insert(subtitleFile);
Subject.Clean();
AllStoredModels.Should().BeEmpty();
}
[Test]
public void should_not_delete_subtitle_files_that_have_a_coresponding_series()
{
var series = Builder<Series>.CreateNew()
.BuildNew();
var episodeFile = Builder<EpisodeFile>.CreateNew()
.With(h => h.Quality = new QualityModel())
.With(h => h.Languages = new List<Language> { Language.English })
.BuildNew();
Db.Insert(series);
Db.Insert(episodeFile);
var subtitleFile = Builder<SubtitleFile>.CreateNew()
.With(m => m.SeriesId = series.Id)
.With(m => m.EpisodeFileId = episodeFile.Id)
.With(m => m.Language = Language.English)
.BuildNew();
Db.Insert(subtitleFile);
Subject.Clean();
AllStoredModels.Should().HaveCount(1);
}
[Test]
public void should_delete_subtitle_files_that_dont_have_a_coresponding_episode_file()
{
var series = Builder<Series>.CreateNew()
.BuildNew();
Db.Insert(series);
var subtitleFile = Builder<SubtitleFile>.CreateNew()
.With(m => m.SeriesId = series.Id)
.With(m => m.EpisodeFileId = 10)
.With(m => m.Language = Language.English)
.BuildNew();
Db.Insert(subtitleFile);
Subject.Clean();
AllStoredModels.Should().BeEmpty();
}
[Test]
public void should_not_delete_subtitle_files_that_have_a_coresponding_episode_file()
{
var series = Builder<Series>.CreateNew()
.BuildNew();
var episodeFile = Builder<EpisodeFile>.CreateNew()
.With(h => h.Quality = new QualityModel())
.With(h => h.Languages = new List<Language> { Language.English })
.BuildNew();
Db.Insert(series);
Db.Insert(episodeFile);
var subtitleFile = Builder<SubtitleFile>.CreateNew()
.With(m => m.SeriesId = series.Id)
.With(m => m.EpisodeFileId = episodeFile.Id)
.With(m => m.Language = Language.English)
.BuildNew();
Db.Insert(subtitleFile);
Subject.Clean();
AllStoredModels.Should().HaveCount(1);
}
[Test]
public void should_delete_subtitle_files_that_have_episodefileid_of_zero()
{
var series = Builder<Series>.CreateNew()
.BuildNew();
Db.Insert(series);
var subtitleFile = Builder<SubtitleFile>.CreateNew()
.With(m => m.SeriesId = series.Id)
.With(m => m.EpisodeFileId = 0)
.With(m => m.Language = Language.English)
.BuildNew();
Db.Insert(subtitleFile);
Subject.Clean();
AllStoredModels.Should().HaveCount(0);
}
}
}

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