1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-03-20 16:44:18 -04:00

Compare commits

...

300 Commits

Author SHA1 Message Date
Mark McDowall
2639c069bc Bumped package version to 3.0.10 2023-03-12 23:52:39 -07:00
Mark McDowall
d185783987 Fixed: Migrations on SQLite 3.41.0 2023-03-12 23:52:39 -07:00
Mark McDowall
e4c0e80e3e Bumped package version to 3.0.9 2022-08-06 15:36:38 -07:00
Qstick
46bc711558 Fixed: Releases Size filter has incorrect value type
Closes #5085
2022-07-29 18:37:21 -08:00
Mark McDowall
e35e24a4c2 Fixed broken unicode tests 2022-07-06 08:10:25 -07:00
Mark McDowall
7eb61fafa4 Fixed: Parsing of Chinese anime releases with character after episode number
Closes #5054
2022-06-25 23:24:23 -07:00
Mark McDowall
ab578566be Log replacement regex 2022-06-25 23:23:13 -07:00
Mark McDowall
35edef91c6 Fixed: Standard episode searches for anime
Closes #5066
2022-06-25 23:05:18 -07:00
Mark McDowall
78dc2a7e13 New: Include Series Match Type in grab event details 2022-06-20 23:23:48 -07:00
Mark McDowall
5d976ac657 LanguageProfileId for Language profile filtering 2022-06-12 15:17:47 -07:00
Qstick
d5fff15f32 New: Reset Quality Definitions to default 2022-06-12 09:49:07 -07:00
Qstick
7c98c2397a New: Instance name in System/Status API endpoint 2022-06-12 09:36:43 -07:00
Qstick
ad6081aec6 New: Instance name for Page Title 2022-06-12 09:36:43 -07:00
Robin Dadswell
1558929484 New: Instance Name used for Syslog 2022-06-12 09:36:43 -07:00
Robin Dadswell
4a2f120bc1 New: Set Instance Name 2022-06-12 09:36:43 -07:00
Robin Dadswell
6c0f22a11e New: Added UDP syslog support
(cherry picked from commit 8d856b2edb8bf46a2b516d5f7644ae3fa1151323)
2022-06-12 09:36:43 -07:00
Qstick
41a821352e New: Sonarr Sync on Language Profile 2022-06-12 09:34:50 -07:00
Qstick
0991cfe27e Fixed: Validate if equals or child for startup folder 2022-06-12 09:33:59 -07:00
bakerboy448
97925feed9 Fixed: Improved parsing WebDL Releases 2022-06-12 09:33:11 -07:00
Mark McDowall
93fc9abae9 Parse another additional Chinese anime release format 2022-05-29 10:16:14 -07:00
Mark McDowall
2ea7b477cb Fixed: Forgot password wiki link 2022-05-29 10:16:14 -07:00
Kathrin De Cecco
c9df12e6bc Fixed: Typo in add indexer modal 2022-05-28 12:59:12 -07:00
Mark McDowall
b542dd0ddd Fixed: Manual Import without selecting Import Mode
Closes #5036
2022-05-27 08:52:06 -07:00
Mark McDowall
c1e5b7f642 Fixed cutoff unmet integration tests 2022-05-23 20:52:27 -07:00
Mark McDowall
ccb88919b9 New: Increase TBA episode title delay to 48 hours (from 24) to deal with TheTVDB's API caching
Closes #4307
2022-05-22 17:20:24 -07:00
Mark McDowall
f9b2c2d843 Fixed: PRevent import if potential bulk season release doesn't have air date for all episodes
Closes #5021
2022-05-22 17:18:19 -07:00
Mark McDowall
d48950ec3c Fixed: Cutoff Unmet showing items above lowest accepted quality when upgrades are disabled 2022-05-22 16:44:12 -07:00
Mark McDowall
6a7d84f134 New: Parse some additional Chinese anime releases
Closes #5024
2022-05-15 15:13:56 -07:00
Mark McDowall
a81a80a00f Fixed: Parsing of single episode numbers
Closes #5022
2022-05-15 14:47:23 -07:00
Mark McDowall
c00cbb9a5a New: Add FAQ to error adding series with duplicate slug
Closes #5020
2022-05-15 14:41:25 -07:00
Mark McDowall
0d739cd26d New: Don't default manual import to move
Closes #5005
2022-05-09 22:23:30 -07:00
Mark McDowall
d01e6d32de Fixed: Original Filename/Title not being included when importing a new file
Closes #5010
2022-05-09 21:04:42 -07:00
Mark McDowall
704cf7aebe New: Rename Protocol to Preferred Protocol in Delay Profiles
Closes #4951
2022-05-09 21:00:58 -07:00
Jure Merhar
52d95fa632 Fixed: Bluray 576p parsing
Closes #5006
2022-04-30 14:32:57 -08:00
Mark McDowall
a71cc1081e Downgrade Ical.Net from 4.2 to 4.1.11 2022-04-30 15:10:47 -07:00
Mark McDowall
edf1167a37 Fixed: Mapping episode 0 to the parsed season instead of specials 2022-04-29 17:27:35 -07:00
Mark McDowall
8f2c4fe4d1 Fixed: Don't parse part # in brackets as mini series style naming
Closes #1265
2022-04-29 00:10:42 -07:00
Mark McDowall
cc9fc1e3c3 New: Use 45 minutes for runtime when episode aired within 24 hours of pilot episode 2022-04-28 23:55:07 -07:00
installemployee
9fb29f42c4 Fixed: iCal status values 2022-04-28 17:27:23 -07:00
Mark McDowall
9a1a320110 Fixed: Not including Original Title/Filename during rename when episode identifiers are missing
Closes #5003
2022-04-28 17:24:21 -07:00
Mark McDowall
f6664b8b42 New: Parse Spanish language using Castellano and Español
Closes #3579
2022-04-24 17:18:50 -07:00
Mark McDowall
82646db70d New: Added Malayalam and Ukrainian languages
Closes #4662
Closes #5000
2022-04-24 17:18:50 -07:00
Mark McDowall
97e40dc00a Fixed: Rename On Download to On Import when editing connections 2022-04-24 13:20:31 -07:00
Mark McDowall
ae0e23fc8e New: Added Mediainfo Video Dynamic Range column for episodes
Closes #4963
2022-04-24 13:18:48 -07:00
Mark McDowall
893a6744ac New: Add The TVDB link to library import search results
Closes #4996
2022-04-24 12:30:20 -07:00
Mark McDowall
fa4b80b86f Bumped package version to 3.0.8 2022-04-23 21:18:23 -07:00
Mark McDowall
18f7bcd212 Fixed: QBittorrent unknown download state: forcedMetaDL
Closes #4997
2022-04-23 21:14:06 -07:00
Mark McDowall
458c5cd0b3 Fixed: UI hiding search results with duplicate GUIDs
Closes #4994
2022-04-23 21:08:35 -07:00
Mark McDowall
c93f63cd20 Fixed: Interactive Search Filter not filtering multiple qualities in the same filter row 2022-04-18 18:46:54 -07:00
zodac
bc5a43bd92 Fixed: Typo in Connection Lost modal 2022-04-18 18:12:42 -07:00
Mark McDowall
a6a68b4cae Fixed: A potential issue when extra files for multiple series have the same relative path 2022-04-12 17:46:10 -07:00
Mark McDowall
9183c6b846 Fixed: Importing file from UNC shared folder without job folder
Closes #4943
2022-04-12 17:08:53 -07:00
Mark McDowall
d73ad3e27a Remove copy/pasta VOSTFR parsing as French 2022-04-11 20:25:52 -07:00
Mark McDowall
bd70fa5410 Fixed: Plex Library Updates
Closes #4914
2022-04-11 19:34:02 -07:00
Mark McDowall
481345226a Fixed: Skip extras in 'Extras' subfolder
Closes #4980
2022-04-11 18:18:02 -07:00
Mark McDowall
365c6a7741 New: Added additional terms for matching French language releases
Closes #4949
2022-04-09 22:03:59 -07:00
Mark McDowall
8fa6e5ec6d Fixed: Use Manage Episodes instead of Manual Import for title when managing episodes for a series 2022-04-09 21:52:46 -07:00
Mark McDowall
d376ae2f9f Fixed: Manage Episodes button grayed out if there are no episodes
Closes #4938
2022-04-09 21:51:10 -07:00
issueg2k4g34j2g
103d2751ee New: Update Monotorrent 2022-04-09 21:35:40 -07:00
Mark McDowall
bdd5865876 New: Natural Sorting Manual Import Relative Paths
Closes #4956
2022-04-09 21:19:17 -07:00
bakerboy448
f678775e5c Fixed: Escape Characters as needed for *znab queries
Closes #4788
2022-04-09 21:16:20 -07:00
Douglas R Andreani
5a08d5dc24 New: Add date picker for custom filter dates
Closes #4972
2022-04-09 21:15:02 -07:00
bakerboy448
bba4a5636e Fixed: Clarify Qbit Content Path Error 2022-04-06 17:53:52 -08:00
bakerboy448
8d83b1d8d6 Fixed: API error when sending payload without optional parameters
Co-authored-by: Qstick <qstick@gmail.com>
2022-04-05 17:23:33 -07:00
Marcelo Castagna
3be5d6c258 Fixed: Properly handle 119 error code from Synology Download Station 2022-04-05 17:22:37 -07:00
bakerboy448
40ecdbc12d New: Support for new Nyaa RSS Feed format
Closes #4614
2022-04-05 17:10:26 -07:00
Mark McDowall
6e271e9272 Fixed: Error when trying to import an empty Plex Watchlist 2022-04-05 17:08:58 -07:00
Mark McDowall
5c5b012ded Remove old, broken test 2022-04-05 17:08:28 -07:00
Mark McDowall
be1acfc2f9 Fixed: Re-assigning file that was mapped to multiple episodes to only one of those episodes
Closes #4946
2022-03-28 17:50:11 -07:00
Mark McDowall
ebb48a19cc Improve usage of Original Title renaming token
Fixed: Don't recursively add the current file name to new file name when '{Original Title}' is used in addition to other naming tokens
2022-03-28 17:34:19 -07:00
Mark McDowall
fa9136c4d1 Fixed: Validation when testing indexers, import lists, connections and download clients 2022-03-28 17:34:19 -07:00
Mark McDowall
e7ca98489e Fixed: Default sort key for wanted/missing API endpoint
Closes #4950
2022-03-28 17:34:19 -07:00
Mark McDowall
a3fd3c5e67 Fixed: Clean API request path before authentication 2022-03-28 17:34:19 -07:00
Nial McCallister
cc09f85212 Fixed: Twitter link 2022-03-24 13:50:07 -07:00
Stevie Robinson
581fb2cb3d New: Add optional Source Title column to history 2022-03-24 13:49:37 -07:00
Taloth Saldono
d899225509 Add response size to http responses 2022-03-20 22:19:06 +01:00
Mark McDowall
06464d720c Don't return early after re-running checks after startup grace period 2022-03-20 00:21:07 -07:00
Mark McDowall
d21e9753bc New: Support for parsing some releases from Spanish trackers 2022-03-19 13:04:40 -07:00
Mark McDowall
07f0db477a Fixed: Delay health check notifications on startup
Closes #4381
2022-03-19 13:00:15 -07:00
Mark McDowall
e280897bc7 Fixed: Automatic import of releases when file is not matched to series
Closes #4935
2022-03-19 12:57:00 -07:00
Mark McDowall
bb02fc4668 Fixed: Newznab requests for anime using season/episode numbers 2022-03-19 12:55:45 -07:00
Mark McDowall
e3aa92d09a Fixed: Scrolling in Firefox in small window (requires refresh)
Closes #4403
2022-03-19 12:55:06 -07:00
Mark McDowall
d02d1bbdfe Fixed: Releases without a job folder importing extra files from another release 2022-03-06 22:05:17 -08:00
Mark McDowall
5df6f13f36 Bumped package version 2022-03-04 18:45:36 -08:00
Mark McDowall
fc2023d67c Fixed: Parsing of 540p season packs
Closes #4928
2022-03-04 17:26:08 -08:00
Mark McDowall
2417c4afb2 Cleanup partial comment 2022-03-04 17:26:08 -08:00
Taloth Saldono
0521fc5681 Re-added xem scene number verification warning to calendar list view as well 2022-02-27 23:40:34 +01:00
bakerboy448
77412f2376 Fixed: Assume SABnzbd develop version is 3.0.0 if not specified
Fixes #4911
2022-02-27 22:08:09 +01:00
Stéphane Dupont
4bfcd0de1d New: Import subtitles from sub folders
Closes #2513
2022-02-27 12:37:23 -08:00
Mark McDowall
c10677dfe7 Option to show audio/subtitle language on series details
New: Option to show audio/subtitle language on series details (first two unique languages will be shown)

Closes #3189
2022-02-27 01:38:56 -08:00
Mark McDowall
56b3acddc9 Fixed: Clearing logs not updating UI once complete 2022-02-27 01:38:56 -08:00
Mark McDowall
b24bea415b New: TvdbId added to episode response from API
Closes #4921
2022-02-27 01:38:56 -08:00
Marcelo Castagna
a8cb7784f2 Update Synology error codes 2022-02-27 01:37:38 -08:00
Daniel Martin Gonzalez
f1d07f74ee New: Option to show release group column on series list 2022-02-27 01:18:13 -08:00
Mark McDowall
acdf02d569 Fixed: Negative preferred word scores being trumped by 0 scores without any matches 2022-02-24 19:11:09 -08:00
Mark McDowall
66af08a830 Fixed: On Import notifications for webhooks
Closes #4913
2022-02-24 17:49:31 -08:00
Mark McDowall
61e68b02ed Fixed: favion.ico request does not require AuthenticationService
Closes #4910
2022-02-24 17:48:11 -08:00
Mark McDowall
80d36a06c8 Fixed: Filter indicator in interactive search 2022-02-24 17:44:51 -08:00
Mark McDowall
2a45b615ae Fixed: Don't automatically import if release title doesn't match series title
Closes #4826
2022-02-21 22:59:07 -08:00
Mark McDowall
f6b08f697b New: Use TVDB's Episode ID as default uniqueid for Kodi metadata
Closes #4792
2022-02-21 21:28:24 -08:00
Mark McDowall
9e82014454 Fixed: Parsing of quality in DP.WEB releases
Closes #4829
2022-02-21 21:09:18 -08:00
Mark McDowall
341e8023af Fixed: Send download client name instead of type for grab events
Closes #4836
2022-02-21 20:07:18 -08:00
Mark McDowall
7a0090c7a2 Fixed: Schedule refresh and process monitored download tasks at high priority 2022-02-21 19:47:33 -08:00
Ajax
66be23a7c4 New: Update Cert Validation Help Text 2022-02-21 14:20:12 -08:00
Mark McDowall
590f306e5f Fix linting for Backup/BackupRow 2022-02-21 13:31:59 -08:00
Mark McDowall
715711e6d7 New: Auto focus input when editing release group during manual import
Closes #4906
2022-02-21 13:31:14 -08:00
Mark McDowall
9e1b799fb7 Fixed: Recycle bin log message
Closes #4908
2022-02-21 13:31:14 -08:00
Mark McDowall
8946b401cf New: Schedule refresh and process monitored download tasks at high priority
Closes #4907
2022-02-21 13:31:14 -08:00
Mathis
87f03e1f38 New: Add qBittorrent sequential order and first and last piece priority options(#4870) 2022-02-21 12:20:58 -08:00
bakerboy448
d18751eff2 Fixed: Improve help text for download client Category 2022-02-21 12:14:44 -08:00
Marcos
d71c50b634 New: Add more groups that do not follow -Group format 2022-02-21 12:12:31 -08:00
Zack Eckersley Pallett
78aeda1a2c New: Add backup size information
Closes #4830
2022-02-21 12:11:12 -08:00
Ajax
c7427f8df8 Fixed: IPv4 instead of IP4 2022-02-21 12:07:54 -08:00
Mark McDowall
79436149eb Fixed: Sorting of some titles with acronyms or common words at the start
Closes #4839
2022-02-16 18:25:42 -08:00
Mark McDowall
210768d7d6 Fixed: Profiles with upgrades disabled incorrectly allowing upgrades in some cases
Closes #4898
2022-02-16 18:25:41 -08:00
Mark McDowall
b3d90d903a Fixed: Parsing of 4-digit absolute episode number with Exx in CRC
Closes #4858
2022-02-16 18:25:41 -08:00
Robin Dadswell
1bf87bf873 New: End Jackett 'all' endpoint support
(cherry picked from commit 54c914d48fefa730728518d50fc9e49032d0947b)

Closes #4437
2022-02-05 19:27:37 -08:00
PearsonFlyer
69ccb96a36 Fixed: Updated ruTorrent stopped state helptext 2022-02-05 19:26:06 -08:00
Mark McDowall
a36397452d Fixed: Parsing episodes when full series does not have XEM mappings
Fixed #4849
2022-02-05 19:13:02 -08:00
Stevie Robinson
853f4d1e29 Fixed: Help text for Release Profile tags 2022-02-05 18:34:57 -08:00
Alan Collins
52c6bc5549 Adjusted the Windows LongPath support check for valid segment lengths 2022-02-02 18:28:09 +01:00
Mark McDowall
5a9df521ad Fixed: Manual import not removing unparseable items from queue
Fixed #4831
2022-01-23 15:41:07 -08:00
Mark McDowall
6debc77408 New: Show filter indicator when filter is applied to a view
Closes #4863
2022-01-23 14:28:06 -08:00
Mark McDowall
1ee40215e7 New: Add Release group to history for all events
Closes #4865
2022-01-23 14:16:26 -08:00
Mark McDowall
ccc378fd0c Update comment for TorznabSettings MinimumSeeders 2022-01-23 14:06:16 -08:00
bakerboy448
71dba904a1 New: OnApplicationUpdate Notifications
Closes #4810
Co-authored-by: Qstick <qstick@gmail.com>
2022-01-23 13:24:34 -08:00
Stéphane (Bakeneko) Dupont
cee17483d9 New: Add option to search for anime using standard episode numbers
Closes #4153
2022-01-23 13:11:43 -08:00
bakerboy448
05b1581b7d Fixed: Improved Indexer test failure message when no results are returned 2022-01-23 13:05:24 -08:00
bakerboy448
36395decc7 New: Support various Anime Release Groups with spaces and dashes 2022-01-23 13:00:59 -08:00
bakerboy448
113bb6ad4d Fixed: Don't parse -EN, -ES, -CAT as Group
(based on radarr 81966e621ef1c5ea8014c7d4fcb7f32d0f93581b)
2022-01-23 13:00:59 -08:00
bakerboy448
e6210aede6 Fixed: Better Cleansing of Tracker Announce Keys
Fixes: #4623
2022-01-23 13:00:59 -08:00
bakerboy448
6de8bdf331 Fix bad HashRelease test 2022-01-23 13:00:48 -08:00
bakerboy448
c67718d81e New: Correctly Parse Groups D-Z0N3 and Fight-BB
New: Parse Select Groups that do not follow -Group Format

Fixes #4623
- formatting
2022-01-23 13:00:26 -08:00
bakerboy448
faa510eb09 Fixed: Better Parsing of 4k Releases
Fixed: Parse 4k H265 releases as 4k
Fixed: Parse 3840x2160 as 4k

(based on radarr commit fa6b7ad2877a78182823e8bd831d01a70c16f033)
2022-01-23 13:00:26 -08:00
bakerboy448
ac9a98e498 New: Parse Release Groups with a - in their name
(based on radarr commit ea801665e4f3b7c505e2c2f37a4ef941a15e2c2d)
2022-01-23 13:00:26 -08:00
Qiming Chen
13aaa20f1b New: Link indexer to specific download client
Closes #1215
Co-authored-by: Qstick <qstick@gmail.com>
2022-01-23 12:58:23 -08:00
pixxon
a27984c032 New: Implement OnDelete notification for Emby/Jellyfin
Closes #4743
2022-01-23 12:56:39 -08:00
Stevie Robinson
9afcec8b1f Fixed: Mass Editor Footer on Smaller Screens
Closes #4860
2022-01-23 12:47:43 -08:00
bakerboy448
f4dbda1318 Fixed: Clarify Indexer Priority Helptext 2022-01-23 12:46:19 -08:00
bejhan
f67e11d477 Fixed: Parsing of numeric only titles that include a year
Closes #4850
2022-01-23 12:45:47 -08:00
PearsonFlyer
d4d4bf8784 Fixed: Avoid download path check false positives for Flood
Closes #4825
2022-01-23 12:44:19 -08:00
Qstick
6106362f6c Fix more unit tests after filter paths change 2022-01-22 11:42:09 -06:00
Mark McDowall
86658b05ca Fix unit tests after filter paths change 2022-01-19 23:54:26 -08:00
Qstick
041689e904 New: Add AppName to system status response 2022-01-18 23:19:03 -06:00
Mark McDowall
0cb8d93069 Fixed: Jump bar on series page not showing when window is made wider
Closes #4837
2022-01-13 17:54:04 -08:00
Qstick
6131a99497 Fixed: Skip Flat Extra Files (Plex Naming) on Import
Fixes #3914
2022-01-13 17:08:07 -08:00
Qstick
4be626a44c Fixed: Skip various Extras directories during scan 2022-01-13 17:08:07 -08:00
Qstick
9889ab7b48 Update MonoTorrent from nuget
Co-authored-by: ta264 <ta264@users.noreply.github.com>
2021-12-30 09:41:59 -08:00
bakerboy448
d284c29b6f Fixed: Add missing MediaInfo VideoDynamicRangeType token to help modal 2021-12-30 09:36:09 -08:00
Qstick
87c65932f0 Clean TraktService.cs 2021-12-28 19:51:43 -06:00
Taloth Saldono
5c8f2518ba Added BDLight to quality parser 2021-12-27 01:51:35 +01:00
Taloth Saldono
d7c10a4d4d Fixed: Re-added xem scene number verification warning to calendar. 2021-12-27 01:18:11 +01:00
Taloth Saldono
f2c7e235af Parse specials directly in ParsingService rather than callee. 2021-12-25 01:06:53 +01:00
Taloth Saldono
e3be3ef91e New: Gracefully handle broken release profiles when ppl swapped to older sonarr versions and back again 2021-12-25 00:59:42 +01:00
bakerboy448
ec866082d4 Fixed: Cleanse Notifiarr secret from URL in logs 2021-12-24 13:06:31 -08:00
Qstick
60b4e14522 Add test to ensure Identity is maintained on Id column during migration 2021-12-24 13:05:05 -08:00
tsubus
385a756a7e Use mono for MachOConverter in build.sh 2021-12-24 13:03:44 -08:00
Qstick
de528fff42 Don't set ReleaseGroup twice in while building LocalEpisode 2021-12-23 18:20:17 -06:00
Mark McDowall
b90e25f652 Fixed: Overflowing release profile terms
Closes #4667
2021-12-20 12:29:17 -08:00
Stevie Robinson
d80565f6a9 Fixed: Convert Trakt list name to URL slug 2021-12-20 12:26:51 -08:00
Mark McDowall
245a033ab3 Fixed: Don't show absolute episode number in Manual Import if not available 2021-12-19 10:09:32 -08:00
Mark McDowall
8621ecfec1 Fixed: Some manually imported episodes not being marked as imported and remove from queue
Closes #4803
2021-12-19 10:09:32 -08:00
Mark McDowall
52d8f87c66 Fixed: Sorting series without a Previous Airing
Closes #4809
2021-12-19 10:09:32 -08:00
Mark McDowall
b869ebeac2 Fixed: Parsing of 2160p BDRips
Closes #4807
2021-12-19 10:09:32 -08:00
Mark McDowall
3f66eeba4d Fixed: Download client name in history details
Closes #4801
2021-12-19 10:09:32 -08:00
hugepants
d5b91f81ef Fixed: Grammar in tooltip of download button 2021-12-17 19:46:22 -08:00
Yaroslav Veremenko
39192a6622 New: Added "Season Pack" to Interactive Search custom filters
Closes #4780
2021-12-17 19:45:45 -08:00
bakerboy448
f948a59f6f Fixed: Bad wiki fragment for mono health check 2021-12-17 19:42:24 -08:00
bakerboy448
2d67247234 Fixed: Improved messaging when episode file was detected as deleted from disk 2021-12-17 19:41:14 -08:00
bakerboy448
cfa93c0a92 Fixed: Improve WEBDL detection of Netflix Rips 2021-12-17 19:39:48 -08:00
bakerboy448
96a8991ba3 Clarify Trace logs in bug reports 2021-12-17 19:39:00 -08:00
Martin
e2b16adec6 Use pattern matching instead of as expression type checking 2021-12-17 19:16:00 -08:00
Taloth Saldono
201004113e New: Updated MediaInfo to 21.09, Sqlite to 3.32.1.0 and added support for mac osx arm64 arch. Deprecated osx x86. 2021-12-16 15:54:19 +01:00
Phillip R. Jaenke
3a8bd451a9 Update RestSharp to 106.15.0, addresses CVE-2021-27293 2021-12-15 15:36:59 -08:00
Qstick
aa463e0af1 New: Use https for thexem.info 2021-12-15 15:34:02 -08:00
Mark McDowall
81ff4791ac Update open collective links 2021-12-13 15:52:51 -08:00
Mark McDowall
ec62884649 Fixed: Report certificate validation failures when configuring Plex Media Server connection
Closes #4781
2021-12-07 08:20:14 -08:00
bakerboy448
0ac0a6223a New: Display Unknown Items in Activity Queue by default
Closes #4783
2021-12-07 08:12:38 -08:00
Alan Collins
7b694ea71d New: Add {MediaInfo VideoDynamicRangeType} token for renaming 2021-12-05 10:41:27 -08:00
Stevie Robinson
7f079ac8f2 New: Updated naming examples 2021-12-05 10:03:07 -08:00
Mark McDowall
1603512ad6 Remove message from ping response 2021-12-04 13:29:45 -08:00
Mark McDowall
75fc550a3f New: /ping endpoint for verifying that Sonarr is running and able to access it's DB
Closes #4766
2021-12-03 17:41:30 -08:00
Mark McDowall
d51cd4bbe7 Fixed: Parsing of even more poorly named full season regexes
Closes #4771
2021-12-03 17:12:29 -08:00
Mark McDowall
70456410a7 Fixed: Deleting from Manage Episodes modal 2021-12-03 17:09:21 -08:00
Mark McDowall
9e5d173900 Fixed: Parsing of 4 digit episode number with 4 digit season number 2021-12-03 17:08:46 -08:00
Mark McDowall
f08f5cecdc Fixed: Parsing of some multi-season release names 2021-11-28 18:26:26 -08:00
Mark McDowall
226d94b050 New: Show genres on series details page
Closes #4734
2021-11-28 18:26:26 -08:00
Mark McDowall
2d0541c03b Fixed: Increase width and truncate long titles on Import List Exclusions
Closes #4528
2021-11-28 15:46:06 -08:00
Mark McDowall
ae328c1d84 Fixed: Parsing of Ger.Dub releases as German
Closes #4538
2021-11-28 15:13:38 -08:00
Mark McDowall
61633ab074 Fixed: Parsing of more anime releases with Chinese and English titles
Closes #4531
2021-11-28 15:03:04 -08:00
Mark McDowall
d600e2e3fb Fixed: More restrictive repeated multi-episode parsing to avoid false positives
Closes #4716
2021-11-28 14:08:17 -08:00
bakerboy448
7b7da9c1b2 New: Handle missingFiles status from qBit 2021-11-26 19:49:24 -08:00
Mark McDowall
ded3f59d2f Fixed: Don't attempt to import downloads that cannot be parsed
Closes #4758
2021-11-26 19:41:38 -08:00
Mark McDowall
2601a68cd4 New: Parse 960p as 720p instead of 480p
Closes #4757
2021-11-26 18:56:43 -08:00
Mark McDowall
1d188d32b6 Fixed broken disk scan tests 2021-11-26 09:10:04 -08:00
Mark McDowall
73429a0823 Fixed: Parsing of poorly named anime above 1000 episodes
Closes #4755
2021-11-23 16:50:52 -08:00
Mark McDowall
f26540cdc7 Reanalyze media files if file size changes
Closes #3366
2021-11-23 16:50:52 -08:00
Mark McDowall
2bf1ce1763 New: Manage episodes through Manual Import modal 2021-11-23 16:50:52 -08:00
Mark McDowall
b184e62fa7 Fixed: Get full path for download station instead of shared folder
Closes #4751
2021-11-21 11:39:39 -08:00
geogolem
86fa6036d0 New: Trakt Connection
Closes #3906
2021-11-21 10:08:56 -08:00
bakerboy448
5267e15c17 Fixed: Images for Connections using old branch name 2021-11-21 10:06:42 -08:00
bakerboy448
a30ec0eb52 Add missing remux quality parsing tests 2021-11-21 10:05:58 -08:00
bakerboy448
dd3b57da27 Update bug report template 2021-11-21 09:07:13 -08:00
Robin Dadswell
3f9f8b3e2d New: Time column is first column on events page 2021-11-21 09:06:42 -08:00
Mark McDowall
d11c691a73 Fixed: Only blocklist pending releases when option is checked 2021-11-17 07:46:01 -08:00
Taloth Saldono
44a160fa06 Clarified Aria2 RPC Path field.
closes #4738
2021-11-06 20:17:20 +01:00
Taloth Saldono
272f8e6136 Restored backward compat of Release Profile POST api 2021-11-05 18:48:13 +01:00
Qstick
6378e7afef Fix broken test 2021-11-04 22:54:55 -05:00
Mark McDowall
b70ef368db Fixed: api/v3/history/series "includeSeries" and "includeEpisode" query parameters
Closes #4727
2021-11-03 17:21:03 -07:00
Mark McDowall
19510c4932 Fixed: Quality Profile and Tag Import List filtering
Closes #4736
2021-11-03 16:50:12 -07:00
Mark McDowall
4e28c7d190 Fixed: Error message when RSS feed items lack a pubData element 2021-11-03 16:30:07 -07:00
Mark McDowall
51d45247d0 Fixed prowl tests 2021-11-03 15:57:02 -07:00
Robin Dadswell
85a143a1b6 Fixed: Prowl notifications priority
Closes #4719
2021-11-01 20:29:18 -07:00
Mark McDowall
f58a2389bd New: Increase width of preferred word keys when editing
Closes #4689
2021-10-22 17:44:50 -07:00
Mark McDowall
a97e83ea4d Fixed: Parsing of special episodes that lack a series title
Closes #4688
2021-10-17 21:39:17 -07:00
Mark McDowall
a83ed3bcce New: Add logging is release is rejected because no download URL is available 2021-10-17 21:39:17 -07:00
Mark McDowall
30aa5f9070 New: Release group column on series details 2021-10-17 21:39:17 -07:00
Mark McDowall
4bba820e5a New: Set release group during Manual Import
Closes #3811
2021-10-17 21:39:17 -07:00
BubbleRep
cd30175308 Fixed: On Delete events for Custom Scripts not being executed
Closes #4688
2021-10-17 21:39:08 -07:00
Mark McDowall
2e2733a2e1 Fixed: Saving release profile when removing an item from Must (Not) Contain 2021-10-05 13:42:37 -07:00
Mark McDowall
553aa22b29 Remove unused split 2021-10-05 13:12:28 -07:00
Mark McDowall
08359e264c New: Show preferred words in release profiles for tag details 2021-10-05 12:30:38 -07:00
Mark McDowall
979219f7df Fixed: Viewing tag details in settings
Closes #4683
2021-10-05 12:30:38 -07:00
Mark McDowall
bb89fb0867 Fixed: Use unmodified titles when searching Nyaa
Towards #1225
2021-10-05 12:30:38 -07:00
Mark McDowall
747a4164e2 Rename QueryTitles to CleanSceneTitles in SearchCriteriaBase 2021-10-05 12:30:38 -07:00
Mark McDowall
eb4a9f624e Rename NzbSearchService to ReleaseSearchService 2021-10-05 12:30:38 -07:00
Alan Collins
4bdb7fe795 New: Use filename without extension if SceneName is unavailable for preferred words
Closes #4675
2021-10-05 12:10:53 -07:00
Mark McDowall
0abd52d6be Fixed: Release profiles not saving if Must (Not) Contain is empty
Closes #4681
2021-10-05 09:19:47 -07:00
Mark McDowall
af654e245c Fixed: Bad migration resulting Must Not Contain being overwritten by Must Contain 2021-10-04 08:06:48 -07:00
Mark McDowall
deed85d2f9 Handle null values while migrating release profiles 2021-10-03 15:49:20 -07:00
Mark McDowall
89b1d58b86 Fix broken tests 2021-10-03 12:45:01 -07:00
Mark McDowall
f4c71fc56f Remove unused split from ReleaseProfile 2021-10-03 11:51:01 -07:00
Mark McDowall
7e175bf95e Fixed: Qbit torrents treated as failed after error
Closes #4674
2021-10-03 00:44:38 -07:00
ta264
99843d2876 Fixed: Restoring scroll position when going back to index page 2021-10-03 00:44:38 -07:00
ta264
7c74d19515 Revert "Updated react-virtualized"
This reverts commit 47f3886d7a.
2021-10-02 20:38:16 -07:00
ta264
076ad5fe6d Revert "Fixed scrolling via jump list on series index"
This reverts commit 5d316ad7dc.
2021-10-02 20:38:16 -07:00
Qstick
519a5ca75c New: Allow Sonarr List Sync by Source Tag
Fixes #3966
2021-10-02 20:36:58 -07:00
Mark McDowall
07c95f06d3 New: Log which DB is being migrated
Closes #4669
2021-10-02 19:00:25 -07:00
Mark McDowall
ad9f242b5c Fixed: Parsing of Plex DVR date-based recordings
Closes #4671
2021-10-02 18:44:53 -07:00
Mark McDowall
ada01a1116 Fixed: Commas in Must (Not) Contain regex
Closes #4672
2021-10-02 18:44:53 -07:00
justin vanderhooft
f6fbd3cfee New: Import from Plex Watchlist 2021-10-01 13:35:59 -07:00
Mark McDowall
5923b4ae0d New: Show limited matching for aliases that are limited to specific formats/release groups 2021-10-01 10:48:30 -07:00
Taloth Saldono
0d99c87d87 Fixed: Bumped mono workaround version from 6.10 to 7.x for btls trust chain coz they still haven't fixed it after over a year 2021-09-30 21:12:02 +02:00
Mark McDowall
9111799f46 Not a real series title 2021-09-30 11:22:35 -07:00
Stevie Robinson
943a3d80c4 New: Disable autocomplete of port number 2021-09-30 11:11:52 -07:00
Kevin Lau
d9e9b72a89 New: Change Today color in calendar for better visibility 2021-09-30 11:11:14 -07:00
bakerboy448
5bf7228658 Update CONTRIBUTING.md
- wiki links
- irc link
- GitHub link 
- branches
2021-09-30 11:10:43 -07:00
Mark McDowall
8ad5e5dd13 Fixed: Parsing of quality when release group contains Remux
Closes #4594
2021-09-30 10:59:45 -07:00
Taloth Saldono
c0a961bb94 New: Updated TheXEM server url on xem admin request 2021-09-28 22:21:36 +02:00
Mark McDowall
6994ca720a Fixed: Parsing multi-episode file name with number in episode title
Closes #4613
2021-09-05 21:45:05 -07:00
Mark McDowall
1d8b711eda Aria2 fixes
Fixed: Removing completed downloads from Aria2
Fixed: Return correct path for Aria2 downloads in a job folder
Fixed: Seeding torrents in Aria2 are treated as finished downloading
Closes #4648
2021-09-03 21:41:52 -07:00
Mark McDowall
77fdebc366 Do not unmonitor episodes after using Manual File Import 2021-08-30 17:29:13 -07:00
Mark McDowall
dd3899806b Fixed: Parsing of some anime with standard numbers
Closes #4573
2021-08-29 20:40:46 -07:00
Mark McDowall
574f05e296 Fixed: Parsing of some multi-episode titles
Closes #4612
2021-08-29 20:15:27 -07:00
Mark McDowall
5c4687e0d9 Fixed: Unmonitor episodes after using Manual File Import
Closes #4638
2021-08-29 19:54:05 -07:00
Mark McDowall
e19d4cf85b Fixed: Log active indexers instead of implying all indexers are searched
Closes #4642
2021-08-29 19:25:41 -07:00
Mark McDowall
6b84da614b Fixed: Queue conflicts with the same download in multiple clients 2021-08-29 19:23:48 -07:00
LoV432
31833253dd Fixed: Erai-Raws Batches Parsing 2021-08-29 19:13:49 -07:00
Qstick
dbd140d4ec Update QualityDefinitionsConnector.js 2021-08-20 21:02:09 -07:00
Qstick
e9a49941c9 Fixed: Help message when adding download clients 2021-08-20 21:01:43 -07:00
Qstick
0fe436b952 Aria2 formatting cleanup 2021-08-20 21:01:23 -07:00
Qstick
22f044844c Fix Phantom branch reference in Join proxy 2021-08-15 20:14:51 +02:00
Taloth Saldono
20306a38e1 Fixed: Release Push api broken when no indexer id is specified 2021-08-11 13:48:12 +02:00
Winter
0d03dba6ea Fix Feature Request issue template 2021-08-07 09:22:19 +02:00
Winter
1c6863dd27 Fix Bug Report issue template
The `title` key is optional, but must not be empty if specified.
2021-08-07 09:22:19 +02:00
Mark McDowall
3f60e28c42 Fixed: Blocklisting pending releases
Closes #4598
2021-08-05 17:17:40 -07:00
Robin Dadswell
ead1371846 New: Renamed Blacklist to Blocklist 2021-08-05 16:55:23 -07:00
Taloth Saldono
2f6409226a Fixed: Cleanup of unused tags for Import lists.
Fixes #4610
2021-08-06 01:54:12 +02:00
bakerboy448
3fb5f65f08 Use YML Github templates 2021-08-05 16:16:57 -07:00
Mark McDowall
38feeefea3 Fixed: Improve More Info links for Indexers, download clients, etc 2021-08-03 19:13:08 -07:00
Mark McDowall
a7a3c546e5 Fix broken test 2021-08-03 18:55:45 -07:00
Taloth Saldono
f107ea5678 Simplified regex a bit. 2021-08-03 21:59:30 +02:00
Taloth Saldono
59409a7e72 Fixed: Parse endpoint response when title failed to parse
Closes #4591
2021-08-03 20:55:27 +02:00
Mark McDowall
dc7f46027a Fixed: Prevent conflicts with reserved device names
Closes #4595
2021-08-01 16:45:23 -07:00
Mark McDowall
2031da05f6 Updated more wiki links 2021-08-01 16:12:30 -07:00
Mark McDowall
92b9f46399 Fixed HealthCheckFixture test 2021-08-01 16:05:36 -07:00
Mark McDowall
0a30735f34 Fixed: Updated supported wiki links 2021-08-01 16:00:52 -07:00
Mark McDowall
021fd4afa7 Fixed: Updated wiki links 2021-08-01 15:34:44 -07:00
siankatabg
57e3bd8b4d New: Bulgarian Language 2021-08-01 13:32:49 -07:00
bakerboy448
3bbec2ff5d Fixed: Incorrectly Parsing [PublicHD] as Release Group 2021-08-01 13:30:33 -07:00
Zippy79
155dbd4dd5 New: Add TVDB URL in Kodi metadata 2021-08-01 13:29:25 -07:00
Mark McDowall
4bf3ab1511 Improve default path for Synology Download Station
Fixes: Missing default path for Download Station
Fixes: Error when getting destination path for Synology Download Station in health check
Closes #4562
2021-08-01 13:25:50 -07:00
LLeny
6fd31613c2 New: Aria2 Torrent Client
Closes #1374
2021-08-01 12:49:36 -07:00
Alanoll
d4cd4a9549 New: Named Release Profile preferred word renaming tokens 2021-08-01 12:45:26 -07:00
Mark McDowall
6596d0b4da Fixed: Show error if adding root folder fails
Closes #4570
2021-08-01 12:09:02 -07:00
Mark McDowall
dca2cfcecd Fixed: Peers filtering in Interactive Search results
Closes #4583
2021-08-01 12:09:01 -07:00
Mark McDowall
076c293942 Fixed: Monitoring pilot episode will not monitor first season
Closes #4597
2021-08-01 12:09:01 -07:00
Alex Thomson
94417402d8 Remove duplicate call to DeleteTorrent 2021-07-27 16:17:04 +02:00
bakerboy448
4659a8366d Update bug report template 2021-07-13 10:23:31 -07:00
6cUbi57z
c3d54b312e New: Add tag support to indexers
Closes #487
2021-07-04 11:17:57 -07:00
Yukine
14b551b027 Fixed: Parsing of some anime releases with year in title 2021-07-04 10:19:55 -07:00
Nathaniel Peiffer
43cd103248 New: Add Size column to Activity: Queue
Closes #4527
2021-07-04 10:14:42 -07:00
bakerboy448
bd4624c0ab Add wiki link for logs in bug report template 2021-07-04 10:13:20 -07:00
bakerboy448
b9539cc1f7 Update Contributing.md 2021-06-22 07:46:47 -07:00
Minamiya Natsuki
fc8bbf29d1 Fixed: Parsing of some Chinese anime releases without brackets 2021-06-21 20:42:32 -07:00
Robin Dadswell
98e5442f24 New: Added Running Years into the shows details 2021-06-21 20:39:10 -07:00
bakerboy448
c30ce3580a Fixed: Incorrectly parsing RePACKPOST as Group 2021-06-21 20:38:50 -07:00
Nathaniel Peiffer
2ddf131e1a New: Activity Queue: Rename Timeleft column to Time Left 2021-06-21 20:37:00 -07:00
Jake Soenneker
2f366bc3b7 New: Manual Import rejection column is sortable 2021-06-21 20:35:23 -07:00
TwentyNine78
49e90463e5 Fixed: Compatibility with the new Download Station API
Fixes #4388
2021-06-21 20:29:50 -07:00
634 changed files with 11748 additions and 20679 deletions

View File

@@ -1,37 +0,0 @@
---
name: Bug Report
about: Support Requests will be closed immediately, if you are not 100% certain this is a bug please go to our Reddit, Discord, Forums, or IRC first. Exceptions do not mean you found a bug!
title: ''
labels: ''
assignees: ''
---
<!-- Support Requests will be closed immediately, if you are not 100% certain this is a bug please go to our Reddit, Discord, Forums, or IRC first. Exceptions do not mean you found a bug! -->
**Describe the bug**
<!-- A clear and concise description of what the bug is. -->
**To Reproduce**
<!-- Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error -->
**Expected behavior**
<!-- A clear and concise description of what you expected to happen.-->
**Screenshots**
<!-- If applicable, add screenshots to help explain your problem.-->
**Platform Information (please complete the following information):**
- OS: <!-- [e.g. Windows 10 2004 / Ubuntu 20.04] -->
- Docker: <!-- [Yes/No] -->
- .net Framework (Windows) or mono (macOS/Linux) (System -> Status): <!--[e.g. Mono 5.8, Mono 6.2, .net 4.5] -->
- Browser and Version (Only needed for UI issues): <!--[e.g. chrome 86.0.4240.198] -->
- Sonarr Version: <!--[e.g. 2.0.0.5344 , 3.0.4.1077]-->
- Sonarr Branch: <!--[e.g. master, develop , phantom-develop]-->
**Trace Logs**
Turn on Trace logs under Settings -> General and wait for the bug to occur again.
**Upload the full log file here (or another site (e.g. pastebin) and link it). Issues will be closed, if they do not include this!**
<!-- Trace logs are named Sonarr.trace.txt or Sonarr.trace.#.txt and will contain "trace" in them-->

82
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,82 @@
name: Bug Report
description: 'Support Requests will be closed immediately, if you are not 100% certain this is a bug please go to our Reddit, Discord, Forums, or IRC first. Exceptions do not mean you found a bug!'
labels: ['needs-triage']
body:
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the bug you encountered.
options:
- label: I have searched the existing issues
required: true
- type: textarea
attributes:
label: Current Behavior
description: A concise description of what you're experiencing.
validations:
required: true
- type: textarea
attributes:
label: Expected Behavior
description: A concise description of what you expected to happen.
validations:
required: true
- type: textarea
attributes:
label: Steps To Reproduce
description: Steps to reproduce the behavior.
placeholder: |
1. In this environment...
2. With this config...
3. Run '...'
4. See error...
validations:
required: false
- type: textarea
attributes:
label: Environment
description: |
examples:
- **OS**: Ubuntu 20.04
- **Sonarr**: Sonarr 3.0.6.1265
- **Docker Install**: Yes
- **Using Reverse Proxy**: No
- **Browser**: Firefox 90 (If UI related)
value: |
- OS:
- Sonarr:
- Docker Install:
- Using Reverse Proxy:
- Browser:
render: markdown
validations:
required: true
- type: dropdown
attributes:
label: What branch are you running?
options:
- Main
- Develop
- Other (This issue will be closed)
validations:
required: true
- type: textarea
attributes:
label: Trace Logs?
description: |
Trace Logs (https://wiki.servarr.com/sonarr/troubleshooting#logging-and-log-files)
***Generally speaking, all bug reports must have trace logs provided.***
*** Info Logs are not trace logs. If the logs do not say trace and are not from a file like `*.trace.*.txt` they are not trace logs.***
validations:
required: true
- type: textarea
attributes:
label: Anything else?
description: |
Links? Screenshots? References? Anything that will give us more context about the issue you are encountering!
***Generally speaking, all bug reports must have trace logs provided.***
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
validations:
required: true
required: true

View File

@@ -10,5 +10,5 @@ contact_links:
url: https://forums.sonarr.tv/
about: Discuss and search through support topics.
- name: Support via IRC
url: http://webchat.freenode.net/?channels=#sonarr
url: https://web.libera.chat/?channels=#sonarr
about: Chat with users and devs on support and setup related topics.

View File

@@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for Sonarr
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
**Describe the solution you'd like**
<!-- A clear and concise description of what you want to happen. -->
**Describe alternatives you've considered**
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
**Additional context**
<!-- Add any other context or screenshots about the feature request here. -->

View File

@@ -0,0 +1,38 @@
name: Feature Request
description: 'Suggest an idea for Sonarr'
labels: ['needs-triage']
body:
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the feature you are requesting.
options:
- label: I have searched the existing issues
required: true
- type: textarea
attributes:
label: Is your feature request related to a problem? Please describe
description: A clear and concise description of what the problem is.
validations:
required: true
- type: textarea
attributes:
label: Describe the solution you'd like
description: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
attributes:
label: Describe alternatives you've considered
description: A clear and concise description of any alternative solutions or features you've considered.
validations:
required: true
- type: textarea
attributes:
label: Anything else?
description: |
Links? References? Mockups? Anything that will give us more context about the feature you are encountering!
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
validations:
required: true

6
.github/SUPPORT.md vendored
View File

@@ -1,7 +1,7 @@
## Support
There are a number of frequently asked questions that have been answered in our [FAQ](https://wiki.servarr.com/Sonarr_FAQ)
There are a number of frequently asked questions that have been answered in our [FAQ](https://wiki.servarr.com/sonarr/faq)
The [wiki](https://wiki.servarr.com/Sonarr) contains other information and guides
The [wiki](https://wiki.servarr.com/sonarr) contains other information and guides
Please use one of the support channels: [forums](https://forums.sonarr.tv/), [subreddit](https://www.reddit.com/r/sonarr/), [discord ](https://discord.gg/M6BvZn5), or [IRC ](http://webchat.freenode.net/?channels=#sonarr)for support/questions.
Please use one of the support channels: [forums](https://forums.sonarr.tv/), [subreddit](https://www.reddit.com/r/sonarr/), [discord ](https://discord.gg/M6BvZn5), or [IRC](https://web.libera.chat/?channels=#sonarr)for support/questions.

View File

@@ -3,7 +3,7 @@
We're always looking for people to help make Sonarr even better, there are a number of ways to contribute.
## Documentation ##
Setup guides, [FAQ](https://wiki.servarr.com/Sonarr_FAQ), the more information we have on the [wiki](https://wiki.servarr.com/Sonarr) the better.
Setup guides, [FAQ](https://wiki.servarr.com/sonarr/faq), the more information we have on the [wiki](https://wiki.servarr.com/sonarr) the better.
## Development ##
@@ -17,26 +17,26 @@ Setup guides, [FAQ](https://wiki.servarr.com/Sonarr_FAQ), the more information w
### Getting started ###
1. Fork Sonarr
2. Clone the repository into your development machine. [*info*](https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository-from-github)
2. Clone the repository into your development machine. [*info*](https://docs.github.com/en/get-started/quickstart/fork-a-repo)
3. Install the required Node Packages `yarn install`
4. Start gulp to monitor your dev environment for any changes that need post processing using `yarn start` command.
4. Start webpack to monitor your dev environment for any frontend changes that need post processing using `yarn start` command.
5. Build the project in Visual Studio, Setting startup project to `Sonarr.Console` and framework to `x86`
6. Debug the project in Visual Studio
7. Open http://localhost:8989
### Contributing Code ###
- If you're adding a new, already requested feature, please comment on [Github Issues](https://github.com/Sonarr/Sonarr/issues "Github Issues") so work is not duplicated (If you want to add something not already on there, please talk to us first)
- Rebase from Sonarr's develop (currently phantom-develop) branch, don't merge
- Rebase from Sonarr's `develop` branch, don't merge
- Make meaningful commits, or squash them
- Feel free to make a pull request before work is complete, this will let us see where its at and make comments/suggest improvements
- Reach out to us on our [forums](https://forums.sonarr.tv/), [subreddit](https://www.reddit.com/r/sonarr/), [discord](https://discord.gg/Ex7FmFK), or [IRC](http://webchat.freenode.net/?channels=#sonarr) if you have any questions
- Reach out to us on our [forums](https://forums.sonarr.tv/), [subreddit](https://www.reddit.com/r/sonarr/), [discord](https://discord.gg/Ex7FmFK), or [IRC](https://web.libera.chat/?channels=#sonarr) if you have any questions
- Add tests (unit/integration)
- Commit with *nix line endings for consistency (We checkout Windows and commit *nix)
- One feature/bug fix per pull request to keep things clean and easy to understand
- Use 4 spaces instead of tabs, this should be the default for VS 2019 and WebStorm
### Pull Requesting ###
- Only make pull requests to develop (currently phantom-develop), never master, if you make a PR to master we'll comment on it and close it
- Only make pull requests to develop (currently `develop`), never `main`, if you make a PR to master we'll comment on it and close it
- You're probably going to get some comments or questions from us, they will be to ensure consistency and maintainability
- We'll try to respond to pull requests as soon as possible, if its been a day or two, please reach out to us, we may have missed it
- Each PR should come from its own [feature branch](http://martinfowler.com/bliki/FeatureBranch.html) not develop in your fork, it should have a meaningful branch name (what is being added/fixed)

View File

@@ -5,7 +5,7 @@ Sonarr is a PVR for Usenet and BitTorrent users. It can monitor multiple RSS fee
## Getting Started
- [Download/Installation](https://sonarr.tv/#downloads-v3)
- [FAQ](https://wiki.servarr.com/Sonarr_FAQ)
- [FAQ](https://wiki.servarr.com/sonarr/faq)
- [Wiki](https://wiki.servarr.com/Sonarr)
- [(WIP) API Documentation](https://github.com/Sonarr/Sonarr/wiki/API)
- [Donate](https://sonarr.tv/donate)
@@ -16,9 +16,9 @@ Note: GitHub Issues are for Bugs and Feature Requests Only
- [Forums](https://forums.sonarr.tv/)
- [Discord](https://discord.gg/M6BvZn5)
- [GitHub - Bugs and Feature Requests Only](https://github.com/Sonarr/Sonarr/issues)
- [IRC ](http://webchat.freenode.net/?channels=#sonarr)
- [IRC](https://web.libera.chat/?channels=#sonarr)
- [Reddit](https://www.reddit.com/r/sonarr)
- [Wiki](https://wiki.servarr.com/Sonarr)
- [Wiki](https://wiki.servarr.com/sonarr)
@@ -42,6 +42,7 @@ Note: GitHub Issues are for Bugs and Feature Requests Only
### Development
This project exists thanks to all the people who contribute. [Contribute](CONTRIBUTING.md).
<a href="https://github.com/Sonarr/Sonarr/graphs/contributors"><img src="https://opencollective.com/Sonarr/contributors.svg?width=890&button=false" /></a>
### Supporters
@@ -49,17 +50,17 @@ This project exists thanks to all the people who contribute. [Contribute](CONTRI
This project would not be possible without the support of our users and software providers.
[**Become a sponsor or backer**](https://opencollective.com/sonarr) to help us out!
#### Mega Sponsors
[![Sponsors](https://opencollective.com/sonarr/tiers/mega-sponsor.svg?width=890)](https://opencollective.com/sonarr/contribute/mega-sponsor-21443/checkout)
#### Sponsors
[![Sponsors](https://opencollective.com/sonarr/tiers/sponsor.svg)](https://opencollective.com/sonarr/contribute/sponsor-21443/checkout)
#### Flexible Sponsors
[![Flexible Sponsors](https://opencollective.com/sonarr/tiers/flexible-sponsor.svg?avatarHeight=54)](https://opencollective.com/sonarr/contribute/flexible-sponsor-21457/checkout)
[![Flexible Sponsors](https://opencollective.com/sonarr/sponsors.svg?width=890)](https://opencollective.com/sonarr/contribute/sponsor-21457/checkout)
#### Backers
[![Backers](https://opencollective.com/sonarr/tiers/backer.svg?avatarHeight=48)](https://opencollective.com/sonarr/contribute/backer-21442/checkout)
[![Backers](https://opencollective.com/sonarr/backers.svg?width=890)](https://opencollective.com/sonarr/contribute/backer-21442/checkout)
#### JetBrains

View File

@@ -12,8 +12,11 @@ sourceFolder='./src'
slnFile=$sourceFolder/Sonarr.sln
updateSubFolder=Sonarr.Update
sqlitePackageDir="$HOME/.nuget/packages/system.data.sqlite.core.servarr/1.0.115.5-18"
nuget='tools/nuget/nuget.exe';
vswhere='tools/vswhere/vswhere.exe';
macho='tools/macho/MachOConverter.exe';
. ./version.sh
@@ -135,6 +138,9 @@ Build()
CleanFolder $outputFolder false
echo "Removing Sonarr.Update/sqlite3.dll"
rm $outputFolder/Sonarr.Update/sqlite3.dll
echo "Removing Mono.Posix.dll"
rm $outputFolder/Mono.Posix.dll
@@ -234,6 +240,7 @@ PackageMono()
echo "Removing native windows binaries Sqlite, MediaInfo"
rm -f $outputFolderLinux/sqlite3.*
rm -f $outputFolderLinux/Sonarr.Update/sqlite3.*
rm -f $outputFolderLinux/MediaInfo.*
PatchMono $outputFolderLinux
@@ -278,17 +285,22 @@ PackageMacOS()
chmod +x $outputFolderMacOS/Sonarr
echo "Adding Sonarr.Update Launcher"
cp ./distribution/osx/Launcher/dist/Launcher $outputFolderMacOS/Sonarr.Update/
mv $outputFolderMacOS/Sonarr.Update/Sonarr.Update.exe $outputFolderMacOS/Sonarr.Update/Sonarr.Update.exe.bak
mv $outputFolderMacOS/Sonarr.Update/Launcher $outputFolderMacOS/Sonarr.Update/Sonarr.Update
mv $outputFolderMacOS/Sonarr.Update/Sonarr.Update.exe.bak $outputFolderMacOS/Sonarr.Update/Sonarr.Update.exe
CheckExitCode cp ./distribution/osx/Launcher/dist/Launcher $outputFolderMacOS/Sonarr.Update/
CheckExitCode mv $outputFolderMacOS/Sonarr.Update/Sonarr.Update.exe $outputFolderMacOS/Sonarr.Update/Sonarr.Update.exe.bak
CheckExitCode mv $outputFolderMacOS/Sonarr.Update/Launcher $outputFolderMacOS/Sonarr.Update/Sonarr.Update
CheckExitCode mv $outputFolderMacOS/Sonarr.Update/Sonarr.Update.exe.bak $outputFolderMacOS/Sonarr.Update/Sonarr.Update.exe
chmod +x $outputFolderMacOS/Sonarr.Update/Sonarr.Update
echo "Adding sqlite dylibs"
cp $sourceFolder/Libraries/Sqlite/*.dylib $outputFolderMacOS
echo "Adding sqlite dylib"
CheckExitCode cp "$sqlitePackageDir/runtimes/osx-x64/native/net46"/*.config $outputFolderMacOS/
if [ $runtime = "dotnet" ] ; then
CheckExitCode $macho merge $outputFolderMacOS "$sqlitePackageDir/runtimes/osx-x64/native/net46"/*.dylib "$sqlitePackageDir/runtimes/osx-arm64/native/net46"/*.dylib
else
CheckExitCode mono $macho merge $outputFolderMacOS "$sqlitePackageDir/runtimes/osx-x64/native/net46"/*.dylib "$sqlitePackageDir/runtimes/osx-arm64/native/net46"/*.dylib
fi
echo "Adding MediaInfo dylib"
cp $sourceFolder/Libraries/MediaInfo/*.dylib $outputFolderMacOS
CheckExitCode cp $sourceFolder/Libraries/MediaInfo/x64/*.dylib $outputFolderMacOS/
ProgressEnd 'Creating MacOS Package'
}
@@ -297,29 +309,37 @@ PackageMacOSApp()
{
ProgressStart 'Creating macOS App Package'
outputFolderMacOSAppBase=$outputFolderMacOSApp/Sonarr.app/Contents/MacOS
outputFolderMacOSAppBin=$outputFolderMacOSAppBase/bin
rm -rf $outputFolderMacOSApp
mkdir $outputFolderMacOSApp
cp -r ./distribution/osx/Sonarr.app $outputFolderMacOSApp
mkdir -p $outputFolderMacOSApp/Sonarr.app/Contents/MacOS
mkdir -p $outputFolderMacOSAppBase
echo "Adding Sonarr Launcher"
cp ./distribution/osx/Launcher/dist/Launcher $outputFolderMacOSApp/Sonarr.app/Contents/MacOS/
mv $outputFolderMacOSApp/Sonarr.app/Contents/MacOS/Launcher $outputFolderMacOSApp/Sonarr.app/Contents/MacOS/Sonarr
chmod +x $outputFolderMacOSApp/Sonarr.app/Contents/MacOS/Sonarr
CheckExitCode cp ./distribution/osx/Launcher/dist/Launcher $outputFolderMacOSAppBase/
CheckExitCode mv $outputFolderMacOSAppBase/Launcher $outputFolderMacOSAppBase/Sonarr
chmod +x $outputFolderMacOSAppBase/Sonarr
echo "Copying Binaries"
mkdir -p $outputFolderMacOSApp/Sonarr.app/Contents/MacOS/bin
cp -r $outputFolderLinux/* $outputFolderMacOSApp/Sonarr.app/Contents/MacOS/bin/
mkdir -p $outputFolderMacOSAppBin
CheckExitCode cp -r $outputFolderLinux/* $outputFolderMacOSAppBin
echo "Adding sqlite dylib"
if [ $runtime = "dotnet" ] ; then
CheckExitCode $macho merge $outputFolderMacOSAppBin "$sqlitePackageDir/runtimes/osx-x64/native/net46" "$sqlitePackageDir/runtimes/osx-arm64/native/net46"
else
CheckExitCode mono $macho merge $outputFolderMacOSAppBin "$sqlitePackageDir/runtimes/osx-x64/native/net46" "$sqlitePackageDir/runtimes/osx-arm64/native/net46"
fi
echo "Adding sqlite dylibs"
cp $sourceFolder/Libraries/Sqlite/*.dylib $outputFolderMacOSApp/Sonarr.app/Contents/MacOS/bin/
echo "Adding MediaInfo dylib"
cp $sourceFolder/Libraries/MediaInfo/*.dylib $outputFolderMacOSApp/Sonarr.app/Contents/MacOS/bin/
CheckExitCode cp $sourceFolder/Libraries/MediaInfo/x64/*.dylib $outputFolderMacOSAppBin/
echo "Removing Update Folder"
rm -r $outputFolderMacOSApp/Sonarr.app/Contents/MacOS/bin/Sonarr.Update
echo "# Do Not Edit\nPackageVersion=${BUILD_NUMBER}\nPackageAuthor=[Team Sonarr](https://sonarr.tv)\nReleaseVersion=${BUILD_NUMBER}\nUpdateMethod=$PackageUpdater\nBranch=${Branch:-master}" > $outputFolderMacOSApp/Sonarr.app/Contents/MacOS/package_info
rm -r $outputFolderMacOSAppBin/Sonarr.Update
echo "# Do Not Edit\nPackageVersion=${BUILD_NUMBER}\nPackageAuthor=[Team Sonarr](https://sonarr.tv)\nReleaseVersion=${BUILD_NUMBER}\nUpdateMethod=$PackageUpdater\nBranch=${Branch:-master}" > $outputFolderMacOSAppBase/package_info
ProgressEnd 'Creating macOS App Package'
}

View File

@@ -44,7 +44,8 @@ module.exports = (env) => {
'node_modules'
],
alias: {
jquery: 'jquery/src/jquery'
jquery: 'jquery/src/jquery',
'react-middle-truncate': 'react-middle-truncate/lib/react-middle-truncate'
}
},

View File

@@ -18,9 +18,9 @@ import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import BlacklistRowConnector from './BlacklistRowConnector';
import BlocklistRowConnector from './BlocklistRowConnector';
class Blacklist extends Component {
class Blocklist extends Component {
//
// Lifecycle
@@ -100,8 +100,8 @@ class Blacklist extends Component {
columns,
totalRecords,
isRemoving,
isClearingBlacklistExecuting,
onClearBlacklistPress,
isClearingBlocklistExecuting,
onClearBlocklistPress,
...otherProps
} = this.props;
@@ -115,7 +115,7 @@ class Blacklist extends Component {
const selectedIds = this.getSelectedIds();
return (
<PageContent title="Blacklist">
<PageContent title="Blocklist">
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
@@ -129,8 +129,8 @@ class Blacklist extends Component {
<PageToolbarButton
label="Clear"
iconName={icons.CLEAR}
isSpinning={isClearingBlacklistExecuting}
onPress={onClearBlacklistPress}
isSpinning={isClearingBlocklistExecuting}
onPress={onClearBlocklistPress}
/>
</PageToolbarSection>
@@ -155,13 +155,13 @@ class Blacklist extends Component {
{
!isFetching && !!error &&
<div>Unable to load blacklist</div>
<div>Unable to load blocklist</div>
}
{
isPopulated && !error && !items.length &&
<div>
No history blacklist
No history blocklist
</div>
}
@@ -180,7 +180,7 @@ class Blacklist extends Component {
{
items.map((item) => {
return (
<BlacklistRowConnector
<BlocklistRowConnector
key={item.id}
isSelected={selectedState[item.id] || false}
columns={columns}
@@ -206,7 +206,7 @@ class Blacklist extends Component {
isOpen={isConfirmRemoveModalOpen}
kind={kinds.DANGER}
title="Remove Selected"
message={'Are you sure you want to remove the selected items from the blacklist?'}
message={'Are you sure you want to remove the selected items from the blocklist?'}
confirmLabel="Remove Selected"
onConfirm={this.onRemoveSelectedConfirmed}
onCancel={this.onConfirmRemoveModalClose}
@@ -216,7 +216,7 @@ class Blacklist extends Component {
}
}
Blacklist.propTypes = {
Blocklist.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
@@ -224,9 +224,9 @@ Blacklist.propTypes = {
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
totalRecords: PropTypes.number,
isRemoving: PropTypes.bool.isRequired,
isClearingBlacklistExecuting: PropTypes.bool.isRequired,
isClearingBlocklistExecuting: PropTypes.bool.isRequired,
onRemoveSelected: PropTypes.func.isRequired,
onClearBlacklistPress: PropTypes.func.isRequired
onClearBlocklistPress: PropTypes.func.isRequired
};
export default Blacklist;
export default Blocklist;

View File

@@ -5,30 +5,30 @@ import { createSelector } from 'reselect';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import withCurrentPage from 'Components/withCurrentPage';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import * as blacklistActions from 'Store/Actions/blacklistActions';
import * as blocklistActions from 'Store/Actions/blocklistActions';
import { executeCommand } from 'Store/Actions/commandActions';
import * as commandNames from 'Commands/commandNames';
import Blacklist from './Blacklist';
import Blocklist from './Blocklist';
function createMapStateToProps() {
return createSelector(
(state) => state.blacklist,
createCommandExecutingSelector(commandNames.CLEAR_BLACKLIST),
(blacklist, isClearingBlacklistExecuting) => {
(state) => state.blocklist,
createCommandExecutingSelector(commandNames.CLEAR_BLOCKLIST),
(blocklist, isClearingBlocklistExecuting) => {
return {
isClearingBlacklistExecuting,
...blacklist
isClearingBlocklistExecuting,
...blocklist
};
}
);
}
const mapDispatchToProps = {
...blacklistActions,
...blocklistActions,
executeCommand
};
class BlacklistConnector extends Component {
class BlocklistConnector extends Component {
//
// Lifecycle
@@ -36,27 +36,27 @@ class BlacklistConnector extends Component {
componentDidMount() {
const {
useCurrentPage,
fetchBlacklist,
gotoBlacklistFirstPage
fetchBlocklist,
gotoBlocklistFirstPage
} = this.props;
registerPagePopulator(this.repopulate);
if (useCurrentPage) {
fetchBlacklist();
fetchBlocklist();
} else {
gotoBlacklistFirstPage();
gotoBlocklistFirstPage();
}
}
componentDidUpdate(prevProps) {
if (prevProps.isClearingBlacklistExecuting && !this.props.isClearingBlacklistExecuting) {
this.props.gotoBlacklistFirstPage();
if (prevProps.isClearingBlocklistExecuting && !this.props.isClearingBlocklistExecuting) {
this.props.gotoBlocklistFirstPage();
}
}
componentWillUnmount() {
this.props.clearBlacklist();
this.props.clearBlocklist();
unregisterPagePopulator(this.repopulate);
}
@@ -64,56 +64,56 @@ class BlacklistConnector extends Component {
// Control
repopulate = () => {
this.props.fetchBlacklist();
this.props.fetchBlocklist();
}
//
// Listeners
onFirstPagePress = () => {
this.props.gotoBlacklistFirstPage();
this.props.gotoBlocklistFirstPage();
}
onPreviousPagePress = () => {
this.props.gotoBlacklistPreviousPage();
this.props.gotoBlocklistPreviousPage();
}
onNextPagePress = () => {
this.props.gotoBlacklistNextPage();
this.props.gotoBlocklistNextPage();
}
onLastPagePress = () => {
this.props.gotoBlacklistLastPage();
this.props.gotoBlocklistLastPage();
}
onPageSelect = (page) => {
this.props.gotoBlacklistPage({ page });
this.props.gotoBlocklistPage({ page });
}
onRemoveSelected = (ids) => {
this.props.removeBlacklistItems({ ids });
this.props.removeBlocklistItems({ ids });
}
onSortPress = (sortKey) => {
this.props.setBlacklistSort({ sortKey });
this.props.setBlocklistSort({ sortKey });
}
onTableOptionChange = (payload) => {
this.props.setBlacklistTableOption(payload);
this.props.setBlocklistTableOption(payload);
if (payload.pageSize) {
this.props.gotoBlacklistFirstPage();
this.props.gotoBlocklistFirstPage();
}
}
onClearBlacklistPress = () => {
this.props.executeCommand({ name: commandNames.CLEAR_BLACKLIST });
onClearBlocklistPress = () => {
this.props.executeCommand({ name: commandNames.CLEAR_BLOCKLIST });
}
onTableOptionChange = (payload) => {
this.props.setBlacklistTableOption(payload);
this.props.setBlocklistTableOption(payload);
if (payload.pageSize) {
this.props.gotoBlacklistFirstPage();
this.props.gotoBlocklistFirstPage();
}
}
@@ -122,7 +122,7 @@ class BlacklistConnector extends Component {
render() {
return (
<Blacklist
<Blocklist
onFirstPagePress={this.onFirstPagePress}
onPreviousPagePress={this.onPreviousPagePress}
onNextPagePress={this.onNextPagePress}
@@ -131,30 +131,30 @@ class BlacklistConnector extends Component {
onRemoveSelected={this.onRemoveSelected}
onSortPress={this.onSortPress}
onTableOptionChange={this.onTableOptionChange}
onClearBlacklistPress={this.onClearBlacklistPress}
onClearBlocklistPress={this.onClearBlocklistPress}
{...this.props}
/>
);
}
}
BlacklistConnector.propTypes = {
BlocklistConnector.propTypes = {
useCurrentPage: PropTypes.bool.isRequired,
isClearingBlacklistExecuting: PropTypes.bool.isRequired,
isClearingBlocklistExecuting: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchBlacklist: PropTypes.func.isRequired,
gotoBlacklistFirstPage: PropTypes.func.isRequired,
gotoBlacklistPreviousPage: PropTypes.func.isRequired,
gotoBlacklistNextPage: PropTypes.func.isRequired,
gotoBlacklistLastPage: PropTypes.func.isRequired,
gotoBlacklistPage: PropTypes.func.isRequired,
removeBlacklistItems: PropTypes.func.isRequired,
setBlacklistSort: PropTypes.func.isRequired,
setBlacklistTableOption: PropTypes.func.isRequired,
clearBlacklist: PropTypes.func.isRequired,
fetchBlocklist: PropTypes.func.isRequired,
gotoBlocklistFirstPage: PropTypes.func.isRequired,
gotoBlocklistPreviousPage: PropTypes.func.isRequired,
gotoBlocklistNextPage: PropTypes.func.isRequired,
gotoBlocklistLastPage: PropTypes.func.isRequired,
gotoBlocklistPage: PropTypes.func.isRequired,
removeBlocklistItems: PropTypes.func.isRequired,
setBlocklistSort: PropTypes.func.isRequired,
setBlocklistTableOption: PropTypes.func.isRequired,
clearBlocklist: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired
};
export default withCurrentPage(
connect(createMapStateToProps, mapDispatchToProps)(BlacklistConnector)
connect(createMapStateToProps, mapDispatchToProps)(BlocklistConnector)
);

View File

@@ -9,7 +9,7 @@ import ModalHeader from 'Components/Modal/ModalHeader';
import ModalBody from 'Components/Modal/ModalBody';
import ModalFooter from 'Components/Modal/ModalFooter';
class BlacklistDetailsModal extends Component {
class BlocklistDetailsModal extends Component {
//
// Render
@@ -77,7 +77,7 @@ class BlacklistDetailsModal extends Component {
}
}
BlacklistDetailsModal.propTypes = {
BlocklistDetailsModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
sourceTitle: PropTypes.string.isRequired,
protocol: PropTypes.string.isRequired,
@@ -86,4 +86,4 @@ BlacklistDetailsModal.propTypes = {
onModalClose: PropTypes.func.isRequired
};
export default BlacklistDetailsModal;
export default BlocklistDetailsModal;

View File

@@ -9,10 +9,10 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
import EpisodeLanguage from 'Episode/EpisodeLanguage';
import EpisodeQuality from 'Episode/EpisodeQuality';
import SeriesTitleLink from 'Series/SeriesTitleLink';
import BlacklistDetailsModal from './BlacklistDetailsModal';
import styles from './BlacklistRow.css';
import BlocklistDetailsModal from './BlocklistDetailsModal';
import styles from './BlocklistRow.css';
class BlacklistRow extends Component {
class BlocklistRow extends Component {
//
// Lifecycle
@@ -152,7 +152,7 @@ class BlacklistRow extends Component {
/>
<IconButton
title="Remove from blacklist"
title="Remove from blocklist"
name={icons.REMOVE}
kind={kinds.DANGER}
onPress={onRemovePress}
@@ -165,7 +165,7 @@ class BlacklistRow extends Component {
})
}
<BlacklistDetailsModal
<BlocklistDetailsModal
isOpen={this.state.isDetailsModalOpen}
sourceTitle={sourceTitle}
protocol={protocol}
@@ -179,7 +179,7 @@ class BlacklistRow extends Component {
}
BlacklistRow.propTypes = {
BlocklistRow.propTypes = {
id: PropTypes.number.isRequired,
series: PropTypes.object.isRequired,
sourceTitle: PropTypes.string.isRequired,
@@ -195,4 +195,4 @@ BlacklistRow.propTypes = {
onRemovePress: PropTypes.func.isRequired
};
export default BlacklistRow;
export default BlocklistRow;

View File

@@ -1,8 +1,8 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { removeBlacklistItem } from 'Store/Actions/blacklistActions';
import { removeBlocklistItem } from 'Store/Actions/blocklistActions';
import createSeriesSelector from 'Store/Selectors/createSeriesSelector';
import BlacklistRow from './BlacklistRow';
import BlocklistRow from './BlocklistRow';
function createMapStateToProps() {
return createSelector(
@@ -18,9 +18,9 @@ function createMapStateToProps() {
function createMapDispatchToProps(dispatch, props) {
return {
onRemovePress() {
dispatch(removeBlacklistItem({ id: props.id }));
dispatch(removeBlocklistItem({ id: props.id }));
}
};
}
export default connect(createMapStateToProps, createMapDispatchToProps)(BlacklistRow);
export default connect(createMapStateToProps, createMapDispatchToProps)(BlocklistRow);

View File

@@ -24,8 +24,10 @@ function HistoryDetails(props) {
indexer,
releaseGroup,
preferredWordScore,
seriesMatchType,
nzbInfoUrl,
downloadClient,
downloadClientName,
downloadId,
age,
ageHours,
@@ -33,6 +35,8 @@ function HistoryDetails(props) {
publishedDate
} = data;
const downloadClientNameInfo = downloadClientName ?? downloadClient;
return (
<DescriptionList>
<DescriptionListItem
@@ -69,6 +73,16 @@ function HistoryDetails(props) {
null
}
{
seriesMatchType ?
<DescriptionListItem
descriptionClassName={styles.description}
title="Series Match Type"
data={seriesMatchType}
/> :
null
}
{
nzbInfoUrl ?
<span>
@@ -84,10 +98,10 @@ function HistoryDetails(props) {
}
{
downloadClient ?
downloadClientNameInfo ?
<DescriptionListItem
title="Download Client"
data={downloadClient}
data={downloadClientNameInfo}
/> :
null
}
@@ -207,7 +221,7 @@ function HistoryDetails(props) {
reasonMessage = 'File was deleted by via UI';
break;
case 'MissingFromDisk':
reasonMessage = 'Sonarr was unable to find the file on disk so it was removed';
reasonMessage = 'Sonarr was unable to find the file on disk so the file was unlinked from the episode in the database';
break;
case 'Upgrade':
reasonMessage = 'File was deleted to import an upgrade';

View File

@@ -217,6 +217,16 @@ class HistoryRow extends Component {
);
}
if (name === 'sourceTitle') {
return (
<TableRowCell
key={name}
>
{sourceTitle}
</TableRowCell>
);
}
if (name === 'details') {
return (
<TableRowCell

View File

@@ -279,6 +279,17 @@ class Queue extends Component {
return !!(item && item.seriesId && item.episodeId);
})
)}
allPending={isConfirmRemoveModalOpen && (
selectedIds.every((id) => {
const item = items.find((i) => i.id === id);
if (!item) {
return false;
}
return item.status === 'delay' || item.status === 'downloadClientUnavailable';
})
)}
onRemovePress={this.onRemoveSelectedConfirmed}
onModalClose={this.onConfirmRemoveModalClose}
/>

View File

@@ -19,6 +19,7 @@ import QueueStatusCell from './QueueStatusCell';
import TimeleftCell from './TimeleftCell';
import RemoveQueueItemModal from './RemoveQueueItemModal';
import styles from './QueueRow.css';
import formatBytes from 'Utilities/Number/formatBytes';
class QueueRow extends Component {
@@ -41,14 +42,14 @@ class QueueRow extends Component {
this.setState({ isRemoveQueueItemModalOpen: true });
}
onRemoveQueueItemModalConfirmed = (blacklist) => {
onRemoveQueueItemModalConfirmed = (blocklist) => {
const {
onRemoveQueueItemPress,
onQueueRowModalOpenOrClose
} = this.props;
onQueueRowModalOpenOrClose(false);
onRemoveQueueItemPress(blacklist);
onRemoveQueueItemPress(blocklist);
this.setState({ isRemoveQueueItemModalOpen: false });
}
@@ -280,6 +281,12 @@ class QueueRow extends Component {
);
}
if (name === 'size') {
return (
<TableRowCell key={name}>{formatBytes(size)}</TableRowCell>
);
}
if (name === 'outputPath') {
return (
<TableRowCell key={name}>
@@ -370,6 +377,7 @@ class QueueRow extends Component {
isOpen={isRemoveQueueItemModalOpen}
sourceTitle={title}
canIgnore={!!series}
isPending={isPending}
onRemovePress={this.onRemoveQueueItemModalConfirmed}
onModalClose={this.onRemoveQueueItemModalClose}
/>

View File

@@ -21,7 +21,7 @@ class RemoveQueueItemModal extends Component {
this.state = {
remove: true,
blacklist: false
blocklist: false
};
}
@@ -31,7 +31,7 @@ class RemoveQueueItemModal extends Component {
resetState = function() {
this.setState({
remove: true,
blacklist: false
blocklist: false
});
}
@@ -42,8 +42,8 @@ class RemoveQueueItemModal extends Component {
this.setState({ remove: value });
}
onBlacklistChange = ({ value }) => {
this.setState({ blacklist: value });
onBlocklistChange = ({ value }) => {
this.setState({ blocklist: value });
}
onRemoveConfirmed = () => {
@@ -65,10 +65,11 @@ class RemoveQueueItemModal extends Component {
const {
isOpen,
sourceTitle,
canIgnore
canIgnore,
isPending
} = this.props;
const { remove, blacklist } = this.state;
const { remove, blocklist } = this.state;
return (
<Modal
@@ -88,28 +89,32 @@ class RemoveQueueItemModal extends Component {
Are you sure you want to remove '{sourceTitle}' from the queue?
</div>
{
isPending ?
null :
<FormGroup>
<FormLabel>Remove From Download Client</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="remove"
value={remove}
helpTextWarning="Removing will remove the download and the file(s) from the download client."
isDisabled={!canIgnore}
onChange={this.onRemoveChange}
/>
</FormGroup>
}
<FormGroup>
<FormLabel>Remove From Download Client</FormLabel>
<FormLabel>Add Release To Blocklist</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="remove"
value={remove}
helpTextWarning="Removing will remove the download and the file(s) from the download client."
isDisabled={!canIgnore}
onChange={this.onRemoveChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>Blacklist Release</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="blacklist"
value={blacklist}
name="blocklist"
value={blocklist}
helpText="Starts a search for this episode again and prevents this release from being grabbed again"
onChange={this.onBlacklistChange}
onChange={this.onBlocklistChange}
/>
</FormGroup>
@@ -137,6 +142,7 @@ RemoveQueueItemModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
sourceTitle: PropTypes.string.isRequired,
canIgnore: PropTypes.bool.isRequired,
isPending: PropTypes.bool.isRequired,
onRemovePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

View File

@@ -22,7 +22,7 @@ class RemoveQueueItemsModal extends Component {
this.state = {
remove: true,
blacklist: false
blocklist: false
};
}
@@ -32,7 +32,7 @@ class RemoveQueueItemsModal extends Component {
resetState = function() {
this.setState({
remove: true,
blacklist: false
blocklist: false
});
}
@@ -43,8 +43,8 @@ class RemoveQueueItemsModal extends Component {
this.setState({ remove: value });
}
onBlacklistChange = ({ value }) => {
this.setState({ blacklist: value });
onBlocklistChange = ({ value }) => {
this.setState({ blocklist: value });
}
onRemoveConfirmed = () => {
@@ -66,10 +66,11 @@ class RemoveQueueItemsModal extends Component {
const {
isOpen,
selectedCount,
canIgnore
canIgnore,
allPending
} = this.props;
const { remove, blacklist } = this.state;
const { remove, blocklist } = this.state;
return (
<Modal
@@ -89,30 +90,34 @@ class RemoveQueueItemsModal extends Component {
Are you sure you want to remove {selectedCount} item{selectedCount > 1 ? 's' : ''} from the queue?
</div>
<FormGroup>
<FormLabel>Remove From Download Client</FormLabel>
{
allPending ?
null :
<FormGroup>
<FormLabel>Remove From Download Client</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="remove"
value={remove}
helpTextWarning="Removing will remove the download and the file(s) from the download client."
isDisabled={!canIgnore}
onChange={this.onRemoveChange}
/>
</FormGroup>
<FormInputGroup
type={inputTypes.CHECK}
name="remove"
value={remove}
helpTextWarning="Removing will remove the download and the file(s) from the download client."
isDisabled={!canIgnore}
onChange={this.onRemoveChange}
/>
</FormGroup>
}
<FormGroup>
<FormLabel>
Blacklist Release{selectedCount > 1 ? 's' : ''}
Add Release{selectedCount > 1 ? 's' : ''} To Blocklist
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="blacklist"
value={blacklist}
name="blocklist"
value={blocklist}
helpText="Prevents Sonarr from automatically grabbing this episode again"
onChange={this.onBlacklistChange}
onChange={this.onBlocklistChange}
/>
</FormGroup>
@@ -140,6 +145,7 @@ RemoveQueueItemsModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
selectedCount: PropTypes.number.isRequired,
canIgnore: PropTypes.bool.isRequired,
allPending: PropTypes.bool.isRequired,
onRemovePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

View File

@@ -154,7 +154,7 @@ class AddNewSeries extends Component {
<div className={styles.noResults}>Couldn't find any results for '{term}'</div>
<div>You can also search using TVDB ID of a show. eg. tvdb:71663</div>
<div>
<Link to="https://wiki.servarr.com/Sonarr_FAQ#Why_cant_I_add_a_new_series_when_I_know_the_TVDB_ID">
<Link to="https://wiki.servarr.com/sonarr/faq#why-cant-i-add-a-new-series-when-i-know-the-tvdb-id">
Why can't I find my show?
</Link>
</div>

View File

@@ -1,4 +1,5 @@
.series {
.container {
display: flex;
padding: 10px 20px;
width: 100%;
@@ -6,3 +7,19 @@
background-color: $menuItemHoverBackgroundColor;
}
}
.series {
flex: 1 0 0;
overflow: hidden;
}
.tvdbLink {
composes: link from '~Components/Link/Link.css';
margin-left: auto;
color: $textColor;
}
.tvdbLinkIcon {
margin-left: 10px;
}

View File

@@ -1,33 +1,28 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import React, { useCallback } from 'react';
import { icons } from 'Helpers/Props';
import Link from 'Components/Link/Link';
import Icon from 'Components/Icon';
import ImportSeriesTitle from './ImportSeriesTitle';
import styles from './ImportSeriesSearchResult.css';
class ImportSeriesSearchResult extends Component {
function ImportSeriesSearchResult(props) {
const {
tvdbId,
title,
year,
network,
isExistingSeries,
onPress
} = props;
//
// Listeners
const onPressCallback = useCallback(() => onPress(tvdbId), [tvdbId, onPress]);
onPress = () => {
this.props.onPress(this.props.tvdbId);
}
//
// Render
render() {
const {
title,
year,
network,
isExistingSeries
} = this.props;
return (
return (
<div className={styles.container}>
<Link
className={styles.series}
onPress={this.onPress}
onPress={onPressCallback}
>
<ImportSeriesTitle
title={title}
@@ -36,8 +31,19 @@ class ImportSeriesSearchResult extends Component {
isExistingSeries={isExistingSeries}
/>
</Link>
);
}
<Link
className={styles.tvdbLink}
to={`http://www.thetvdb.com/?tab=series&id=${tvdbId}`}
>
<Icon
className={styles.tvdbLinkIcon}
name={icons.EXTERNAL_LINK}
size={16}
/>
</Link>
</div>
);
}
ImportSeriesSearchResult.propTypes = {

View File

@@ -20,24 +20,27 @@ function ImportSeriesTitle(props) {
{
!title.contains(year) &&
year > 0 &&
year > 0 ?
<span className={styles.year}>
({year})
</span>
</span> :
null
}
{
!!network &&
<Label>{network}</Label>
network ?
<Label>{network}</Label> :
null
}
{
isExistingSeries &&
isExistingSeries ?
<Label
kind={kinds.WARNING}
>
Existing
</Label>
</Label> :
null
}
</div>
);

View File

@@ -30,3 +30,9 @@
.importButtonIcon {
margin-right: 8px;
}
.addErrorAlert {
composes: alert from '~Components/Alert.css';
margin: 20px 0;
}

View File

@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { icons, kinds, sizes } from 'Helpers/Props';
import Button from 'Components/Link/Button';
import Alert from 'Components/Alert';
import Icon from 'Components/Icon';
import FieldSet from 'Components/FieldSet';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
@@ -47,21 +48,27 @@ class ImportSeriesSelectFolder extends Component {
isWindows,
isFetching,
isPopulated,
isSaving,
error,
saveError,
items
} = this.props;
const hasRootFolders = items.length > 0;
return (
<PageContent title="Import Series">
<PageContentBody>
{
isFetching && !isPopulated &&
<LoadingIndicator />
isFetching && !isPopulated ?
<LoadingIndicator /> :
null
}
{
!isFetching && !!error &&
<div>Unable to load root folders</div>
!isFetching && error ?
<div>Unable to load root folders</div> :
null
}
{
@@ -87,7 +94,7 @@ class ImportSeriesSelectFolder extends Component {
</div>
{
items.length > 0 ?
hasRootFolders ?
<div className={styles.recentFolders}>
<FieldSet legend="Root Folders">
<RootFolders
@@ -97,35 +104,51 @@ class ImportSeriesSelectFolder extends Component {
items={items}
/>
</FieldSet>
<Button
kind={kinds.PRIMARY}
size={sizes.LARGE}
onPress={this.onAddNewRootFolderPress}
>
<Icon
className={styles.importButtonIcon}
name={icons.DRIVE}
/>
Choose another folder
</Button>
</div> :
<div className={styles.startImport}>
<Button
kind={kinds.PRIMARY}
size={sizes.LARGE}
onPress={this.onAddNewRootFolderPress}
>
<Icon
className={styles.importButtonIcon}
name={icons.DRIVE}
/>
Start Import
</Button>
</div>
null
}
{
!isSaving && saveError ?
<Alert
className={styles.addErrorAlert}
kind={kinds.DANGER}
>
Unable to add root folder
<ul>
{
saveError.responseJSON.map((e, index) => {
return (
<li key={index}>
{e.errorMessage}
</li>
);
})
}
</ul>
</Alert> :
null
}
<div className={hasRootFolders ? undefined : styles.startImport}>
<Button
kind={kinds.PRIMARY}
size={sizes.LARGE}
onPress={this.onAddNewRootFolderPress}
>
<Icon
className={styles.importButtonIcon}
name={icons.DRIVE}
/>
{
hasRootFolders ?
'Choose another folder' :
'Start Import'
}
</Button>
</div>
<FileBrowserModal
isOpen={this.state.isAddNewRootFolderModalOpen}
name="rootFolderPath"
@@ -145,7 +168,9 @@ ImportSeriesSelectFolder.propTypes = {
isWindows: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired,
error: PropTypes.object,
saveError: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
onNewRootFolderSelect: PropTypes.func.isRequired
};

View File

@@ -8,7 +8,7 @@ import AppRoutes from './AppRoutes';
function App({ store, history }) {
return (
<DocumentTitle title="Sonarr">
<DocumentTitle title={window.Sonarr.instanceName}>
<Provider store={store}>
<ConnectedRouter history={history}>
<PageConnector>

View File

@@ -13,13 +13,13 @@ import SeriesDetailsPageConnector from 'Series/Details/SeriesDetailsPageConnecto
import CalendarPageConnector from 'Calendar/CalendarPageConnector';
import HistoryConnector from 'Activity/History/HistoryConnector';
import QueueConnector from 'Activity/Queue/QueueConnector';
import BlacklistConnector from 'Activity/Blacklist/BlacklistConnector';
import BlocklistConnector from 'Activity/Blocklist/BlocklistConnector';
import MissingConnector from 'Wanted/Missing/MissingConnector';
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
import Settings from 'Settings/Settings';
import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementConnector';
import Profiles from 'Settings/Profiles/Profiles';
import Quality from 'Settings/Quality/Quality';
import QualityConnector from 'Settings/Quality/QualityConnector';
import IndexerSettingsConnector from 'Settings/Indexers/IndexerSettingsConnector';
import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector';
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
@@ -118,8 +118,8 @@ function AppRoutes(props) {
/>
<Route
path="/activity/blacklist"
component={BlacklistConnector}
path="/activity/blocklist"
component={BlocklistConnector}
/>
{/*
@@ -158,7 +158,7 @@ function AppRoutes(props) {
<Route
path="/settings/quality"
component={Quality}
component={QualityConnector}
/>
<Route

View File

@@ -27,7 +27,7 @@ function ConnectionLostModal(props) {
<ModalBody>
<div>
Sonarr has lost it's connection to the backend and will need to be reloaded to restore functionality.
Sonarr has lost its connection to the backend and will need to be reloaded to restore functionality.
</div>
<div className={styles.automatic}>

View File

@@ -50,6 +50,7 @@ class AgendaEvent extends Component {
absoluteEpisodeNumber,
airDateUtc,
monitored,
unverifiedSceneNumbering,
hasFile,
grabbed,
queueItem,
@@ -70,7 +71,7 @@ class AgendaEvent extends Component {
const statusStyle = getStatusStyle(hasFile, downloading, startTime, endTime, isMonitored);
const missingAbsoluteNumber = series.seriesType === 'anime' && seasonNumber > 0 && !absoluteEpisodeNumber;
const season = series.seasons.find((s) => s.seasonNumber === seasonNumber);
const seasonStatistics = season.statistics || {};
const seasonStatistics = season?.statistics || {};
return (
<div>
@@ -131,6 +132,16 @@ class AgendaEvent extends Component {
/>
}
{
unverifiedSceneNumbering && !missingAbsoluteNumber ?
<Icon
className={styles.statusIcon}
name={icons.WARNING}
title="Scene number hasn't been verified yet"
/> :
null
}
{
!!queueItem &&
<span className={styles.statusIcon}>
@@ -237,6 +248,7 @@ AgendaEvent.propTypes = {
absoluteEpisodeNumber: PropTypes.number,
airDateUtc: PropTypes.string.isRequired,
monitored: PropTypes.bool.isRequired,
unverifiedSceneNumbering: PropTypes.bool,
hasFile: PropTypes.bool.isRequired,
grabbed: PropTypes.bool,
queueItem: PropTypes.object,

View File

@@ -55,6 +55,7 @@ class CalendarEvent extends Component {
absoluteEpisodeNumber,
airDateUtc,
monitored,
unverifiedSceneNumbering,
hasFile,
grabbed,
queueItem,
@@ -78,7 +79,7 @@ class CalendarEvent extends Component {
const statusStyle = getStatusStyle(hasFile, isDownloading, startTime, endTime, isMonitored);
const missingAbsoluteNumber = series.seriesType === 'anime' && seasonNumber > 0 && !absoluteEpisodeNumber;
const season = series.seasons.find((s) => s.seasonNumber === seasonNumber);
const seasonStatistics = season.statistics || {};
const seasonStatistics = season?.statistics || {};
return (
<Fragment>
@@ -108,6 +109,16 @@ class CalendarEvent extends Component {
null
}
{
unverifiedSceneNumbering && !missingAbsoluteNumber ?
<Icon
className={styles.statusIcon}
name={icons.WARNING}
title="Scene number hasn't been verified yet"
/> :
null
}
{
queueItem ?
<span className={styles.statusIcon}>
@@ -244,6 +255,7 @@ CalendarEvent.propTypes = {
absoluteEpisodeNumber: PropTypes.number,
airDateUtc: PropTypes.string.isRequired,
monitored: PropTypes.bool.isRequired,
unverifiedSceneNumbering: PropTypes.bool,
hasFile: PropTypes.bool.isRequired,
grabbed: PropTypes.bool,
queueItem: PropTypes.object,

View File

@@ -1,7 +1,7 @@
export const APPLICATION_UPDATE = 'ApplicationUpdate';
export const BACKUP = 'Backup';
export const REFRESH_MONITORED_DOWNLOADS = 'RefreshMonitoredDownloads';
export const CLEAR_BLACKLIST = 'ClearBlacklist';
export const CLEAR_BLOCKLIST = 'ClearBlocklist';
export const CLEAR_LOGS = 'ClearLog';
export const CUTOFF_UNMET_EPISODE_SEARCH = 'CutoffUnmetEpisodeSearch';
export const DELETE_LOG_FILES = 'DeleteLogFiles';
@@ -15,6 +15,7 @@ export const REFRESH_SERIES = 'RefreshSeries';
export const RENAME_FILES = 'RenameFiles';
export const RENAME_SERIES = 'RenameSeries';
export const RESET_API_KEY = 'ResetApiKey';
export const RESET_QUALITY_DEFINITIONS = 'ResetQualityDefinitions';
export const RSS_SYNC = 'RssSync';
export const SEASON_SEARCH = 'SeasonSearch';
export const SERIES_SEARCH = 'SeriesSearch';

View File

@@ -128,7 +128,7 @@ class FileBrowserModalContent extends Component {
className={styles.mappedDrivesWarning}
kind={kinds.WARNING}
>
Mapped network drives are not available when running as a Windows Service, see the <Link className={styles.faqLink} to="https://wiki.servarr.com/Sonarr_FAQ#Why_cant_Sonarr_see_my_files_on_a_remote_server">FAQ</Link> for more information.
Mapped network drives are not available when running as a Windows Service, see the <Link className={styles.faqLink} to="https://wiki.servarr.com/sonarr/faq#why-cant-sonarr-see-my-files-on-a-remote-server">FAQ</Link> for more information.
</Alert>
}

View File

@@ -160,6 +160,7 @@ class DateFilterBuilderRowValue extends Component {
<TextInput
name={NAME}
value={filterValue}
type="date"
placeholder="yyyy-mm-dd"
onChange={this.onValueChange}
/>

View File

@@ -0,0 +1,100 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchDownloadClients } from 'Store/Actions/settingsActions';
import sortByName from 'Utilities/Array/sortByName';
import EnhancedSelectInput from './EnhancedSelectInput';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.downloadClients,
(state, { includeAny }) => includeAny,
(state, { protocol }) => protocol,
(downloadClients, includeAny, protocolFilter) => {
const {
isFetching,
isPopulated,
error,
items
} = downloadClients;
const filteredItems = items.filter((item) => item.protocol === protocolFilter);
const values = _.map(filteredItems.sort(sortByName), (downloadClient) => {
return {
key: downloadClient.id,
value: downloadClient.name
};
});
if (includeAny) {
values.unshift({
key: 0,
value: '(Any)'
});
}
return {
isFetching,
isPopulated,
error,
values
};
}
);
}
const mapDispatchToProps = {
dispatchFetchDownloadClients: fetchDownloadClients
};
class DownloadClientSelectInputConnector extends Component {
//
// Lifecycle
componentDidMount() {
if (!this.props.isPopulated) {
this.props.dispatchFetchDownloadClients();
}
}
//
// Listeners
onChange = ({ name, value }) => {
this.props.onChange({ name, value: parseInt(value) });
}
//
// Render
render() {
return (
<EnhancedSelectInput
{...this.props}
onChange={this.onChange}
/>
);
}
}
DownloadClientSelectInputConnector.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
includeAny: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired,
dispatchFetchDownloadClients: PropTypes.func.isRequired
};
DownloadClientSelectInputConnector.defaultProps = {
includeAny: false,
protocol: 'torrent'
};
export default connect(createMapStateToProps, mapDispatchToProps)(DownloadClientSelectInputConnector);

View File

@@ -4,7 +4,7 @@ import React, { Component } from 'react';
import { Manager, Popper, Reference } from 'react-popper';
import classNames from 'classnames';
import getUniqueElememtId from 'Utilities/getUniqueElementId';
import { isMobile as isMobileUtil } from 'Utilities/mobile';
import { isMobile as isMobileUtil } from 'Utilities/browser';
import * as keyCodes from 'Utilities/Constants/keyCodes';
import { icons, sizes, scrollDirections } from 'Helpers/Props';
import Icon from 'Components/Icon';

View File

@@ -6,6 +6,7 @@ import AutoCompleteInput from './AutoCompleteInput';
import CaptchaInputConnector from './CaptchaInputConnector';
import CheckInput from './CheckInput';
import DeviceInputConnector from './DeviceInputConnector';
import DownloadClientSelectInputConnector from './DownloadClientSelectInputConnector';
import KeyValueListInput from './KeyValueListInput';
import MonitorEpisodesSelectInput from './MonitorEpisodesSelectInput';
import NumberInput from './NumberInput';
@@ -68,6 +69,9 @@ function getComponent(type) {
case inputTypes.INDEXER_SELECT:
return IndexerSelectInputConnector;
case inputTypes.DOWNLOAD_CLIENT_SELECT:
return DownloadClientSelectInputConnector;
case inputTypes.ROOT_FOLDER_SELECT:
return RootFolderSelectInputConnector;

View File

@@ -8,8 +8,13 @@
}
}
.inputWrapper {
.keyInputWrapper {
flex: 6 0 0;
}
.valueInputWrapper {
flex: 1 0 0;
min-width: 40px;
}
.buttonWrapper {

View File

@@ -63,7 +63,7 @@ class KeyValueListInputItem extends Component {
return (
<div className={styles.itemContainer}>
<div className={styles.inputWrapper}>
<div className={styles.keyInputWrapper}>
<TextInput
className={styles.keyInput}
name="key"
@@ -75,7 +75,7 @@ class KeyValueListInputItem extends Component {
/>
</div>
<div className={styles.inputWrapper}>
<div className={styles.valueInputWrapper}>
<TextInput
className={styles.valueInput}
name="value"

View File

@@ -2,9 +2,18 @@
display: flex;
justify-content: center;
flex-direction: column;
max-width: 100%;
height: 31px;
}
.link {
max-width: 100%;
}
.linkWithEdit {
max-width: calc(100% - 9px - 4px - 2px);
}
.editContainer {
display: inline-block;
margin-left: 4px;
@@ -15,5 +24,11 @@
.editButton {
composes: button from '~Components/Link/IconButton.css';
width: auto;
width: 9px;
}
.label {
composes: label from '~Components/Label.css';
max-width: 100%;
}

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import MiddleTruncate from 'react-middle-truncate';
import { icons, kinds } from 'Helpers/Props';
import tagShape from 'Helpers/Props/Shapes/tagShape';
import Label from 'Components/Label';
@@ -48,20 +49,26 @@ class TagInputTag extends Component {
kind,
canEdit
} = this.props;
return (
<div
className={styles.tag}
tabIndex={-1}
>
<Label
className={styles.label}
kind={kind}
>
<Link
className={canEdit ? styles.linkWithEdit : styles.link}
tabIndex={-1}
onPress={this.onDelete}
>
{tag.name}
<MiddleTruncate
text={tag.name}
start={10}
end={10}
/>
</Link>
{

View File

@@ -46,13 +46,13 @@ class TextTagInputConnector extends Component {
// to oddities with restrictions (as an example).
const newValue = [...valueArray];
const newTags = split(tag.name);
const newTags = tag.name.startsWith('/') ? [tag.name] : split(tag.name);
newTags.forEach((newTag) => {
newValue.push(newTag.trim());
});
onChange({ name, value: newValue.join(',') });
onChange({ name, value: newValue });
}
onTagDelete = ({ index }) => {
@@ -67,7 +67,7 @@ class TextTagInputConnector extends Component {
onChange({
name,
value: newValue.join(',')
value: newValue
});
}

View File

@@ -2,3 +2,7 @@
margin-right: 5px;
color: $themeRed;
}
.rating {
margin-right: 15px;
}

View File

@@ -6,7 +6,7 @@ import styles from './HeartRating.css';
function HeartRating({ rating, iconSize }) {
return (
<span>
<span className={styles.rating}>
<Icon
className={styles.heart}
name={icons.HEART}

View File

@@ -57,6 +57,7 @@ class FilterMenu extends Component {
>
<ButtonComponent
iconName={icons.FILTER}
showIndicator={selectedFilterKey !== 'all'}
text="Filter"
isDisabled={isDisabled}
/>

View File

@@ -1,11 +1,19 @@
.menuButton {
composes: menuButton from '~./MenuButton.css';
position: relative;
&:hover {
color: #666;
}
}
.indicatorContainer {
position: absolute;
top: 10px;
left: 10px;
}
.label {
margin-left: 5px;
}

View File

@@ -1,12 +1,15 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
import MenuButton from 'Components/Menu/MenuButton';
import { icons } from 'Helpers/Props';
import styles from './PageMenuButton.css';
function PageMenuButton(props) {
const {
iconName,
showIndicator,
text,
...otherProps
} = props;
@@ -21,6 +24,22 @@ function PageMenuButton(props) {
size={18}
/>
{
showIndicator ?
<span
className={classNames(
styles.indicatorContainer,
'fa-layers fa-fw'
)}
>
<Icon
name={icons.CIRCLE}
size={9}
/>
</span> :
null
}
<div className={styles.label}>
{text}
</div>
@@ -30,7 +49,12 @@ function PageMenuButton(props) {
PageMenuButton.propTypes = {
iconName: PropTypes.object.isRequired,
showIndicator: PropTypes.bool.isRequired,
text: PropTypes.string
};
PageMenuButton.defaultProps = {
showIndicator: false
};
export default PageMenuButton;

View File

@@ -7,6 +7,12 @@
text-align: center;
}
.indicatorContainer {
position: absolute;
top: 10px;
right: 12px;
}
.labelContainer {
composes: labelContainer from '~Components/Page/Toolbar/PageToolbarButton.css';
}

View File

@@ -1,12 +1,15 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
import MenuButton from 'Components/Menu/MenuButton';
import { icons } from 'Helpers/Props';
import styles from './ToolbarMenuButton.css';
function ToolbarMenuButton(props) {
const {
iconName,
showIndicator,
text,
...otherProps
} = props;
@@ -22,6 +25,22 @@ function ToolbarMenuButton(props) {
size={21}
/>
{
showIndicator ?
<span
className={classNames(
styles.indicatorContainer,
'fa-layers fa-fw'
)}
>
<Icon
name={icons.CIRCLE}
size={9}
/>
</span> :
null
}
<div className={styles.labelContainer}>
<div className={styles.label}>
{text}
@@ -34,7 +53,12 @@ function ToolbarMenuButton(props) {
ToolbarMenuButton.propTypes = {
iconName: PropTypes.object.isRequired,
showIndicator: PropTypes.bool.isRequired,
text: PropTypes.string
};
ToolbarMenuButton.defaultProps = {
showIndicator: false
};
export default ToolbarMenuButton;

View File

@@ -5,7 +5,7 @@ import FocusLock from 'react-focus-lock';
import classNames from 'classnames';
import elementClass from 'element-class';
import getUniqueElememtId from 'Utilities/getUniqueElementId';
import { isIOS } from 'Utilities/mobile';
import { isIOS } from 'Utilities/browser';
import { setScrollLock } from 'Utilities/scrollLock';
import * as keyCodes from 'Utilities/Constants/keyCodes';
import { sizes } from 'Helpers/Props';

View File

@@ -14,7 +14,7 @@ function PageContent(props) {
return (
<ErrorBoundary errorComponent={PageContentError}>
<DocumentTitle title={title ? `${title} - Sonarr` : 'Sonarr'}>
<DocumentTitle title={title ? `${title} - ${window.Sonarr.instanceName}` : window.Sonarr.instanceName}>
<div className={className}>
{children}
</div>

View File

@@ -1,6 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { isMobile as isMobileUtil } from 'Utilities/mobile';
import { isMobile, isFirefox } from 'Utilities/browser';
import { isLocked } from 'Utilities/scrollLock';
import { scrollDirections } from 'Helpers/Props';
import OverlayScroller from 'Components/Scroller/OverlayScroller';
@@ -15,7 +15,8 @@ class PageContentBody extends Component {
constructor(props, context) {
super(props, context);
this._isMobile = isMobileUtil();
this._isMobile = isMobile();
this._isSmallScreenFirefox = isFirefox && window.innerWidth < 768;
}
//
@@ -41,7 +42,9 @@ class PageContentBody extends Component {
...otherProps
} = this.props;
const ScrollerComponent = this._isMobile ? Scroller : OverlayScroller;
const ScrollerComponent = this._isMobile || this._isSmallScreenFirefox ?
Scroller :
OverlayScroller;
return (
<ScrollerComponent

View File

@@ -19,7 +19,7 @@
}
}
@media only screen and (max-width: $breakpointLarge) {
@media only screen and (max-width: $breakpointExtraLarge) {
.contentFooter {
flex-wrap: wrap;
}

View File

@@ -100,7 +100,9 @@ class PageJumpBar extends Component {
// Listeners
onMeasure = ({ height }) => {
this.setState({ height });
if (height > 0) {
this.setState({ height });
}
}
//

View File

@@ -64,8 +64,8 @@ const links = [
to: '/activity/history'
},
{
title: 'Blacklist',
to: '/activity/blacklist'
title: 'Blocklist',
to: '/activity/blocklist'
}
]
},

View File

@@ -14,6 +14,7 @@ import { fetchHealth } from 'Store/Actions/systemActions';
import { fetchQueue, fetchQueueDetails } from 'Store/Actions/queueActions';
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
import { fetchTags, fetchTagDetails } from 'Store/Actions/tagActions';
import { fetchQualityDefinitions } from 'Store/Actions/settingsActions';
function getState(status) {
switch (status) {
@@ -70,6 +71,7 @@ const mapDispatchToProps = {
dispatchUpdateItem: updateItem,
dispatchRemoveItem: removeItem,
dispatchFetchHealth: fetchHealth,
dispatchFetchQualityDefinitions: fetchQualityDefinitions,
dispatchFetchQueue: fetchQueue,
dispatchFetchQueueDetails: fetchQueueDetails,
dispatchFetchRootFolders: fetchRootFolders,
@@ -221,6 +223,10 @@ class SignalRConnector extends Component {
}
}
handleQualitydefinition = () => {
this.props.dispatchFetchQualityDefinitions();
}
handleQueue = () => {
if (this.props.isQueuePopulated) {
this.props.dispatchFetchQueue();
@@ -377,6 +383,7 @@ SignalRConnector.propTypes = {
dispatchUpdateItem: PropTypes.func.isRequired,
dispatchRemoveItem: PropTypes.func.isRequired,
dispatchFetchHealth: PropTypes.func.isRequired,
dispatchFetchQualityDefinitions: PropTypes.func.isRequired,
dispatchFetchQueue: PropTypes.func.isRequired,
dispatchFetchQueueDetails: PropTypes.func.isRequired,
dispatchFetchRootFolders: PropTypes.func.isRequired,

View File

@@ -7,6 +7,8 @@ import { WindowScroller, Grid } from 'react-virtualized';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
import styles from './VirtualTable.css';
const ROW_HEIGHT = 38;
function overscanIndicesGetter(options) {
const {
cellCount,
@@ -37,7 +39,8 @@ class VirtualTable extends Component {
super(props, context);
this.state = {
width: 0
width: 0,
scrollRestored: false
};
this._grid = null;
@@ -45,17 +48,32 @@ class VirtualTable extends Component {
componentDidUpdate(prevProps, prevState) {
const {
items
items,
scrollIndex,
scrollTop
} = this.props;
const {
width
width,
scrollRestored
} = this.state;
if (this._grid && (prevState.width !== width || hasDifferentItemsOrOrder(prevProps.items, items))) {
// recomputeGridSize also forces Grid to discard its cache of rendered cells
this._grid.recomputeGridSize();
}
if (this._grid && scrollTop !== undefined && scrollTop !== 0 && !scrollRestored) {
this.setState({ scrollRestored: true });
this._grid.scrollToPosition({ scrollTop });
}
if (scrollIndex != null && scrollIndex !== prevProps.scrollIndex) {
this._grid.scrollToCell({
rowIndex: scrollIndex,
columnIndex: 0
});
}
}
//
@@ -85,9 +103,8 @@ class VirtualTable extends Component {
scroller,
header,
headerHeight,
rowRenderer,
rowHeight,
scrollIndex,
rowRenderer,
...otherProps
} = this.props;
@@ -117,11 +134,6 @@ class VirtualTable extends Component {
if (!height) {
return null;
}
const finalScrollTop = scrollIndex == null ?
scrollTop :
scrollIndex * rowHeight;
return (
<Measure
whitelist={['width']}
@@ -134,6 +146,7 @@ class VirtualTable extends Component {
{header}
<div ref={registerChild}>
<Grid
{...otherProps}
ref={this.setGridRef}
autoContainerWidth={true}
autoHeight={true}
@@ -145,7 +158,7 @@ class VirtualTable extends Component {
rowCount={items.length}
columnCount={1}
columnWidth={width}
scrollTop={finalScrollTop}
scrollTop={scrollTop}
onScroll={onChildScroll}
overscanRowCount={2}
cellRenderer={rowRenderer}
@@ -155,7 +168,6 @@ class VirtualTable extends Component {
className={styles.tableBodyContainer}
style={gridStyle}
containerStyle={containerStyle}
{...otherProps}
/>
</div>
</Scroller>
@@ -173,6 +185,7 @@ VirtualTable.propTypes = {
className: PropTypes.string.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
scrollIndex: PropTypes.number,
scrollTop: PropTypes.number,
scroller: PropTypes.instanceOf(Element).isRequired,
header: PropTypes.node.isRequired,
headerHeight: PropTypes.number.isRequired,
@@ -182,7 +195,8 @@ VirtualTable.propTypes = {
VirtualTable.defaultProps = {
className: styles.tableContainer,
headerHeight: 38
headerHeight: 38,
rowHeight: ROW_HEIGHT
};
export default VirtualTable;

View File

@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Manager, Popper, Reference } from 'react-popper';
import classNames from 'classnames';
import { isMobile as isMobileUtil } from 'Utilities/mobile';
import { isMobile as isMobileUtil } from 'Utilities/browser';
import { kinds, tooltipPositions } from 'Helpers/Props';
import Portal from 'Components/Portal';
import dimensions from 'Styles/Variables/dimensions';

View File

@@ -8,7 +8,7 @@ function withScrollPosition(WrappedComponent, scrollPositionKey) {
history
} = props;
const scrollTop = history.action === 'POP' ?
const scrollTop = history.action === 'POP' || (history.location.state && history.location.state.restoreScrollPosition) ?
scrollPositions[scrollPositionKey] :
0;

View File

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

View File

@@ -1,8 +0,0 @@
.actions {
display: flex;
margin-right: auto;
}
.selectInput {
margin-left: 10px;
}

View File

@@ -1,310 +0,0 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import { kinds } from 'Helpers/Props';
import SelectInput from 'Components/Form/SelectInput';
import Button from 'Components/Link/Button';
import SpinnerButton from 'Components/Link/SpinnerButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import ModalContent from 'Components/Modal/ModalContent';
import ModalHeader from 'Components/Modal/ModalHeader';
import ModalBody from 'Components/Modal/ModalBody';
import ModalFooter from 'Components/Modal/ModalFooter';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import SeasonNumber from 'Season/SeasonNumber';
import EpisodeFileEditorRow from './EpisodeFileEditorRow';
import styles from './EpisodeFileEditorModalContent.css';
const columns = [
{
name: 'episodeNumber',
label: 'Episode',
isVisible: true
},
{
name: 'relativePath',
label: 'Relative Path',
isVisible: true
},
{
name: 'airDateUtc',
label: 'Air Date',
isVisible: true
},
{
name: 'language',
label: 'Language',
isVisible: true
},
{
name: 'quality',
label: 'Quality',
isVisible: true
}
];
class EpisodeFileEditorModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {},
isConfirmDeleteModalOpen: false
};
}
componentDidUpdate(prevProps) {
if (hasDifferentItems(prevProps.items, this.props.items)) {
this.setState((state) => {
return removeOldSelectedState(state, prevProps.items);
});
}
}
//
// Control
getSelectedIds = () => {
const selectedIds = getSelectedIds(this.state.selectedState);
return selectedIds.reduce((acc, id) => {
const matchingItem = this.props.items.find((item) => item.id === id);
if (matchingItem && !acc.includes(matchingItem.episodeFileId)) {
acc.push(matchingItem.episodeFileId);
}
return acc;
}, []);
}
//
// Listeners
onSelectAllChange = ({ value }) => {
this.setState(selectAll(this.state.selectedState, value));
}
onSelectedChange = ({ id, value, shiftKey = false }) => {
this.setState((state) => {
return toggleSelected(state, this.props.items, id, value, shiftKey);
});
}
onDeletePress = () => {
this.setState({ isConfirmDeleteModalOpen: true });
}
onConfirmDelete = () => {
this.setState({ isConfirmDeleteModalOpen: false });
this.props.onDeletePress(this.getSelectedIds());
}
onConfirmDeleteModalClose = () => {
this.setState({ isConfirmDeleteModalOpen: false });
}
onLanguageChange = ({ value }) => {
const selectedIds = this.getSelectedIds();
if (!selectedIds.length) {
return;
}
this.props.onLanguageChange(selectedIds, parseInt(value));
}
onQualityChange = ({ value }) => {
const selectedIds = this.getSelectedIds();
if (!selectedIds.length) {
return;
}
this.props.onQualityChange(selectedIds, parseInt(value));
}
//
// Render
render() {
const {
seasonNumber,
isDeleting,
isFetching,
isPopulated,
error,
items,
languages,
qualities,
seriesType,
onModalClose
} = this.props;
const {
allSelected,
allUnselected,
selectedState,
isConfirmDeleteModalOpen
} = this.state;
const languageOptions = _.reduceRight(languages, (acc, language) => {
acc.push({
key: language.id,
value: language.name
});
return acc;
}, [{ key: 'selectLanguage', value: 'Select Language', disabled: true }]);
const qualityOptions = _.reduceRight(qualities, (acc, quality) => {
acc.push({
key: quality.id,
value: quality.name
});
return acc;
}, [{ key: 'selectQuality', value: 'Select Quality', disabled: true }]);
const hasSelectedFiles = this.getSelectedIds().length > 0;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Manage Episodes {seasonNumber != null && <SeasonNumber seasonNumber={seasonNumber} />}
</ModalHeader>
<ModalBody>
{
isFetching && !isPopulated ?
<LoadingIndicator /> :
null
}
{
!isFetching && error ?
<div>{error}</div> :
null
}
{
isPopulated && !items.length ?
<div>
No episode files to manage.
</div>:
null
}
{
isPopulated && items.length ?
<Table
columns={columns}
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
onSelectAllChange={this.onSelectAllChange}
>
<TableBody>
{
items.map((item) => {
return (
<EpisodeFileEditorRow
key={item.id}
seriesType={seriesType}
isSelected={selectedState[item.id]}
{...item}
onSelectedChange={this.onSelectedChange}
/>
);
})
}
</TableBody>
</Table> :
null
}
</ModalBody>
<ModalFooter>
<div className={styles.actions}>
<SpinnerButton
kind={kinds.DANGER}
isSpinning={isDeleting}
isDisabled={!hasSelectedFiles}
onPress={this.onDeletePress}
>
Delete
</SpinnerButton>
<div className={styles.selectInput}>
<SelectInput
name="language"
value="selectLanguage"
values={languageOptions}
isDisabled={!hasSelectedFiles}
onChange={this.onLanguageChange}
/>
</div>
<div className={styles.selectInput}>
<SelectInput
name="quality"
value="selectQuality"
values={qualityOptions}
isDisabled={!hasSelectedFiles}
onChange={this.onQualityChange}
/>
</div>
</div>
<Button
onPress={onModalClose}
>
Close
</Button>
</ModalFooter>
<ConfirmModal
isOpen={isConfirmDeleteModalOpen}
kind={kinds.DANGER}
title="Delete Selected Episode Files"
message={'Are you sure you want to delete the selected episode files?'}
confirmLabel="Delete"
onConfirm={this.onConfirmDelete}
onCancel={this.onConfirmDeleteModalClose}
/>
</ModalContent>
);
}
}
EpisodeFileEditorModalContent.propTypes = {
seasonNumber: PropTypes.number,
isDeleting: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
qualities: PropTypes.arrayOf(PropTypes.object).isRequired,
seriesType: PropTypes.string.isRequired,
onDeletePress: PropTypes.func.isRequired,
onLanguageChange: PropTypes.func.isRequired,
onQualityChange: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default EpisodeFileEditorModalContent;

View File

@@ -1,174 +0,0 @@
/* eslint max-params: 0 */
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import getQualities from 'Utilities/Quality/getQualities';
import createSeriesSelector from 'Store/Selectors/createSeriesSelector';
import { deleteEpisodeFiles, updateEpisodeFiles } from 'Store/Actions/episodeFileActions';
import { fetchLanguageProfileSchema, fetchQualityProfileSchema } from 'Store/Actions/settingsActions';
import EpisodeFileEditorModalContent from './EpisodeFileEditorModalContent';
function createSchemaSelector() {
return createSelector(
(state) => state.settings.languageProfiles,
(state) => state.settings.qualityProfiles,
(languageProfiles, qualityProfiles) => {
const languages = _.map(languageProfiles.schema.languages, 'language');
const qualities = getQualities(qualityProfiles.schema.items);
let error = null;
if (languageProfiles.schemaError) {
error = 'Unable to load languages';
} else if (qualityProfiles.schemaError) {
error = 'Unable to load qualities';
}
return {
isFetching: languageProfiles.isSchemaFetching || qualityProfiles.isSchemaFetching,
isPopulated: languageProfiles.isSchemaPopulated && qualityProfiles.isSchemaPopulated,
error,
languages,
qualities
};
}
);
}
function createMapStateToProps() {
return createSelector(
(state, { seasonNumber }) => seasonNumber,
(state) => state.episodes,
(state) => state.episodeFiles,
createSchemaSelector(),
createSeriesSelector(),
(
seasonNumber,
episodes,
episodeFiles,
schema,
series
) => {
const filtered = _.filter(episodes.items, (episode) => {
if (seasonNumber >= 0 && episode.seasonNumber !== seasonNumber) {
return false;
}
if (!episode.episodeFileId) {
return false;
}
return _.some(episodeFiles.items, { id: episode.episodeFileId });
});
const sorted = _.orderBy(filtered, ['seasonNumber', 'episodeNumber'], ['desc', 'desc']);
const items = _.map(sorted, (episode) => {
const episodeFile = _.find(episodeFiles.items, { id: episode.episodeFileId });
return {
relativePath: episodeFile.relativePath,
language: episodeFile.language,
quality: episodeFile.quality,
languageCutoffNotMet: episodeFile.languageCutoffNotMet,
qualityCutoffNotMet: episodeFile.qualityCutoffNotMet,
...episode
};
});
return {
...schema,
items,
seriesType: series.seriesType,
isDeleting: episodeFiles.isDeleting,
isSaving: episodeFiles.isSaving
};
}
);
}
function createMapDispatchToProps(dispatch, props) {
return {
dispatchFetchLanguageProfileSchema(name, path) {
dispatch(fetchLanguageProfileSchema());
},
dispatchFetchQualityProfileSchema(name, path) {
dispatch(fetchQualityProfileSchema());
},
dispatchUpdateEpisodeFiles(updateProps) {
dispatch(updateEpisodeFiles(updateProps));
},
onDeletePress(episodeFileIds) {
dispatch(deleteEpisodeFiles({ episodeFileIds }));
}
};
}
class EpisodeFileEditorModalContentConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.dispatchFetchLanguageProfileSchema();
this.props.dispatchFetchQualityProfileSchema();
}
//
// Listeners
onLanguageChange = (episodeFileIds, languageId) => {
const language = _.find(this.props.languages, { id: languageId });
this.props.dispatchUpdateEpisodeFiles({ episodeFileIds, language });
}
onQualityChange = (episodeFileIds, qualityId) => {
const quality = {
quality: _.find(this.props.qualities, { id: qualityId }),
revision: {
version: 1,
real: 0
}
};
this.props.dispatchUpdateEpisodeFiles({ episodeFileIds, quality });
}
//
// Render
render() {
const {
dispatchFetchLanguageProfileSchema,
dispatchFetchQualityProfileSchema,
dispatchUpdateEpisodeFiles,
...otherProps
} = this.props;
return (
<EpisodeFileEditorModalContent
{...otherProps}
onLanguageChange={this.onLanguageChange}
onQualityChange={this.onQualityChange}
/>
);
}
}
EpisodeFileEditorModalContentConnector.propTypes = {
seriesId: PropTypes.number.isRequired,
seasonNumber: PropTypes.number,
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
qualities: PropTypes.arrayOf(PropTypes.object).isRequired,
dispatchFetchLanguageProfileSchema: PropTypes.func.isRequired,
dispatchFetchQualityProfileSchema: PropTypes.func.isRequired,
dispatchUpdateEpisodeFiles: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, createMapDispatchToProps)(EpisodeFileEditorModalContentConnector);

View File

@@ -1,3 +0,0 @@
.absoluteEpisodeNumber {
margin-left: 5px;
}

View File

@@ -1,89 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import padNumber from 'Utilities/Number/padNumber';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import TableRow from 'Components/Table/TableRow';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import EpisodeLanguage from 'Episode/EpisodeLanguage';
import EpisodeQuality from 'Episode/EpisodeQuality';
import styles from './EpisodeFileEditorRow';
function EpisodeFileEditorRow(props) {
const {
id,
seriesType,
seasonNumber,
episodeNumber,
absoluteEpisodeNumber,
relativePath,
airDateUtc,
language,
quality,
qualityCutoffNotMet,
languageCutoffNotMet,
isSelected,
onSelectedChange
} = props;
return (
<TableRow>
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChange}
/>
<TableRowCell>
{seasonNumber}x{padNumber(episodeNumber, 2)}
{
seriesType === 'anime' && !!absoluteEpisodeNumber &&
<span className={styles.absoluteEpisodeNumber}>
({absoluteEpisodeNumber})
</span>
}
</TableRowCell>
<TableRowCell>
{relativePath}
</TableRowCell>
<RelativeDateCellConnector
date={airDateUtc}
/>
<TableRowCell>
<EpisodeLanguage
language={language}
isCutoffNotMet={languageCutoffNotMet}
/>
</TableRowCell>
<TableRowCell>
<EpisodeQuality
quality={quality}
isCutoffNotMet={qualityCutoffNotMet}
/>
</TableRowCell>
</TableRow>
);
}
EpisodeFileEditorRow.propTypes = {
id: PropTypes.number.isRequired,
seriesType: PropTypes.string.isRequired,
seasonNumber: PropTypes.number.isRequired,
episodeNumber: PropTypes.number.isRequired,
absoluteEpisodeNumber: PropTypes.number,
relativePath: PropTypes.string.isRequired,
airDateUtc: PropTypes.string.isRequired,
language: PropTypes.object.isRequired,
quality: PropTypes.object.isRequired,
qualityCutoffNotMet: PropTypes.bool.isRequired,
languageCutoffNotMet: PropTypes.bool.isRequired,
isSelected: PropTypes.bool,
onSelectedChange: PropTypes.func.isRequired
};
export default EpisodeFileEditorRow;

View File

@@ -1,13 +1,39 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React from 'react';
import * as mediaInfoTypes from './mediaInfoTypes';
function formatLanguages(languages) {
if (!languages) {
return null;
}
const splitLanguages = _.uniq(languages.split(' / '));
if (splitLanguages.length > 3) {
return (
<span title={splitLanguages.join(', ')}>
{splitLanguages.slice(0, 2).join(', ')}, {splitLanguages.length - 2} more
</span>
);
}
return (
<span>
{splitLanguages.join(', ')}
</span>
);
}
function MediaInfo(props) {
const {
type,
audioChannels,
audioCodec,
videoCodec
audioLanguages,
subtitles,
videoCodec,
videoDynamicRangeType
} = props;
if (type === mediaInfoTypes.AUDIO) {
@@ -31,6 +57,14 @@ function MediaInfo(props) {
);
}
if (type === mediaInfoTypes.AUDIO_LANGUAGES) {
return formatLanguages(audioLanguages);
}
if (type === mediaInfoTypes.SUBTITLES) {
return formatLanguages(subtitles);
}
if (type === mediaInfoTypes.VIDEO) {
return (
<span>
@@ -39,6 +73,14 @@ function MediaInfo(props) {
);
}
if (type === mediaInfoTypes.VIDEO_DYNAMIC_RANGE_TYPE) {
return (
<span>
{videoDynamicRangeType}
</span>
);
}
return null;
}
@@ -46,7 +88,10 @@ MediaInfo.propTypes = {
type: PropTypes.string.isRequired,
audioChannels: PropTypes.number,
audioCodec: PropTypes.string,
videoCodec: PropTypes.string
audioLanguages: PropTypes.string,
subtitles: PropTypes.string,
videoCodec: PropTypes.string,
videoDynamicRangeType: PropTypes.string
};
export default MediaInfo;

View File

@@ -1,2 +1,5 @@
export const AUDIO = 'audio';
export const AUDIO_LANGUAGES = 'audioLanguages';
export const SUBTITLES = 'subtitles';
export const VIDEO = 'video';
export const VIDEO_DYNAMIC_RANGE_TYPE = 'videoDynamicRangeType';

View File

@@ -11,6 +11,7 @@ export const PATH = 'path';
export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect';
export const LANGUAGE_PROFILE_SELECT = 'languageProfileSelect';
export const INDEXER_SELECT = 'indexerSelect';
export const DOWNLOAD_CLIENT_SELECT = 'downloadClientSelect';
export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
export const SELECT = 'select';
export const DYNAMIC_SELECT = 'dynamicSelect';
@@ -35,6 +36,7 @@ export const all = [
QUALITY_PROFILE_SELECT,
LANGUAGE_PROFILE_SELECT,
INDEXER_SELECT,
DOWNLOAD_CLIENT_SELECT,
ROOT_FOLDER_SELECT,
SELECT,
DYNAMIC_SELECT,

View File

@@ -96,6 +96,7 @@ class SelectEpisodeModalContent extends Component {
isAnime,
sortKey,
sortDirection,
modalTitle,
onSortPress,
onModalClose
} = this.props;
@@ -121,7 +122,7 @@ class SelectEpisodeModalContent extends Component {
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
<div className={styles.header}>
Manual Import - Select Episode(s)
{modalTitle} - Select Episode(s)
</div>
</ModalHeader>
@@ -235,6 +236,7 @@ SelectEpisodeModalContent.propTypes = {
isAnime: PropTypes.bool.isRequired,
sortKey: PropTypes.string,
sortDirection: PropTypes.string,
modalTitle: PropTypes.string,
onSortPress: PropTypes.func.isRequired,
onEpisodesSelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired

View File

@@ -67,6 +67,7 @@ class InteractiveImportSelectFolderModalContent extends Component {
const {
recentFolders,
onRemoveRecentFolderPress,
modalTitle,
onModalClose
} = this.props;
@@ -75,7 +76,7 @@ class InteractiveImportSelectFolderModalContent extends Component {
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Manual Import - Select Folder
{modalTitle} - Select Folder
</ModalHeader>
<ModalBody>
@@ -159,6 +160,7 @@ class InteractiveImportSelectFolderModalContent extends Component {
InteractiveImportSelectFolderModalContent.propTypes = {
recentFolders: PropTypes.arrayOf(PropTypes.object).isRequired,
modalTitle: PropTypes.string.isRequired,
onQuickImportPress: PropTypes.func.isRequired,
onInteractiveImportPress: PropTypes.func.isRequired,
onRemoveRecentFolderPress: PropTypes.func.isRequired,

View File

@@ -26,6 +26,12 @@
justify-content: flex-end;
}
.deleteButton {
composes: button from '~Components/Link/Button.css';
margin-right: 10px;
}
.importMode,
.bulkSelect {
composes: select from '~Components/Form/SelectInput.css';

View File

@@ -7,6 +7,7 @@ import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import { align, icons, kinds, scrollDirections } from 'Helpers/Props';
import Button from 'Components/Link/Button';
import SpinnerButton from 'Components/Link/SpinnerButton';
import Icon from 'Components/Icon';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import SelectInput from 'Components/Form/SelectInput';
@@ -14,6 +15,7 @@ import Menu from 'Components/Menu/Menu';
import MenuButton from 'Components/Menu/MenuButton';
import MenuContent from 'Components/Menu/MenuContent';
import SelectedMenuItem from 'Components/Menu/SelectedMenuItem';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import ModalContent from 'Components/Modal/ModalContent';
import ModalHeader from 'Components/Modal/ModalHeader';
import ModalBody from 'Components/Modal/ModalBody';
@@ -25,6 +27,7 @@ import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal'
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
import SelectSeriesModal from 'InteractiveImport/Series/SelectSeriesModal';
import SelectSeasonModal from 'InteractiveImport/Season/SelectSeasonModal';
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
import InteractiveImportRow from './InteractiveImportRow';
import styles from './InteractiveImportModalContent.css';
@@ -51,6 +54,11 @@ const columns = [
label: 'Episode(s)',
isVisible: true
},
{
name: 'releaseGroup',
label: 'Release Group',
isVisible: true
},
{
name: 'quality',
label: 'Quality',
@@ -75,6 +83,7 @@ const columns = [
name: icons.DANGER,
kind: kinds.DANGER
}),
isSortable: true,
isVisible: true
}
];
@@ -85,6 +94,7 @@ const filterExistingFilesOptions = {
};
const importModeOptions = [
{ key: 'chooseImportMode', value: 'Choose Import Mode', disabled: true },
{ key: 'move', value: 'Move Files' },
{ key: 'copy', value: 'Hardlink/Copy Files' }
];
@@ -93,8 +103,9 @@ const SELECT = 'select';
const SERIES = 'series';
const SEASON = 'season';
const EPISODE = 'episode';
const LANGUAGE = 'language';
const RELEASE_GROUP = 'releaseGroup';
const QUALITY = 'quality';
const LANGUAGE = 'language';
class InteractiveImportModalContent extends Component {
@@ -104,16 +115,37 @@ class InteractiveImportModalContent extends Component {
constructor(props, context) {
super(props, context);
const instanceColumns = _.cloneDeep(columns);
if (!props.showSeries) {
instanceColumns.find((c) => c.name === 'series').isVisible = false;
}
this.state = {
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {},
invalidRowsSelected: [],
selectModalOpen: null
withoutEpisodeFileIdRowsSelected: [],
selectModalOpen: null,
columns: instanceColumns,
isConfirmDeleteModalOpen: false
};
}
componentDidUpdate(prevProps) {
const {
isDeleting,
deleteError,
onModalClose
} = this.props;
if (!isDeleting && prevProps.isDeleting && !deleteError) {
onModalClose();
}
}
//
// Control
@@ -128,9 +160,14 @@ class InteractiveImportModalContent extends Component {
this.setState(selectAll(this.state.selectedState, value));
}
onSelectedChange = ({ id, value, shiftKey = false }) => {
onSelectedChange = ({ id, value, hasEpisodeFileId, shiftKey = false }) => {
this.setState((state) => {
return toggleSelected(state, this.props.items, id, value, shiftKey);
return {
...toggleSelected(state, this.props.items, id, value, shiftKey),
withoutEpisodeFileIdRowsSelected: hasEpisodeFileId || !value ?
_.without(state.withoutEpisodeFileIdRowsSelected, id) :
[...state.withoutEpisodeFileIdRowsSelected, id]
};
});
}
@@ -148,6 +185,19 @@ class InteractiveImportModalContent extends Component {
});
}
onDeleteSelectedPress = () => {
this.setState({ isConfirmDeleteModalOpen: true });
}
onConfirmDelete = () => {
this.setState({ isConfirmDeleteModalOpen: false });
this.props.onDeleteSelectedPress(this.getSelectedIds());
}
onConfirmDeleteModalClose = () => {
this.setState({ isConfirmDeleteModalOpen: false });
}
onImportSelectedPress = () => {
const {
downloadId,
@@ -185,7 +235,9 @@ class InteractiveImportModalContent extends Component {
const {
downloadId,
allowSeriesChange,
autoSelectRow,
showFilterExistingFiles,
showDelete,
showImportMode,
filterExistingFiles,
title,
@@ -198,6 +250,8 @@ class InteractiveImportModalContent extends Component {
sortDirection,
importMode,
interactiveImportErrorMessage,
isDeleting,
modalTitle,
onSortPress,
onModalClose
} = this.props;
@@ -207,7 +261,9 @@ class InteractiveImportModalContent extends Component {
allUnselected,
selectedState,
invalidRowsSelected,
selectModalOpen
withoutEpisodeFileIdRowsSelected,
selectModalOpen,
isConfirmDeleteModalOpen
} = this.state;
const selectedIds = this.getSelectedIds();
@@ -230,8 +286,9 @@ class InteractiveImportModalContent extends Component {
{ key: SELECT, value: 'Select...', disabled: true },
{ key: SEASON, value: 'Select Season' },
{ key: EPISODE, value: 'Select Episode(s)' },
{ key: LANGUAGE, value: 'Select Language' },
{ key: QUALITY, value: 'Select Quality' }
{ key: QUALITY, value: 'Select Quality' },
{ key: RELEASE_GROUP, value: 'Select Release Group' },
{ key: LANGUAGE, value: 'Select Language' }
];
if (allowSeriesChange) {
@@ -244,7 +301,7 @@ class InteractiveImportModalContent extends Component {
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Manual Import - {title || folder}
{modalTitle} - {title || folder}
</ModalHeader>
<ModalBody scrollDirection={scrollDirections.BOTH}>
@@ -299,7 +356,7 @@ class InteractiveImportModalContent extends Component {
{
isPopulated && !!items.length && !isFetching && !isFetching &&
<Table
columns={columns}
columns={this.state.columns}
horizontalScroll={true}
selectAll={true}
allSelected={allSelected}
@@ -318,6 +375,9 @@ class InteractiveImportModalContent extends Component {
isSelected={selectedState[item.id]}
{...item}
allowSeriesChange={allowSeriesChange}
autoSelectRow={autoSelectRow}
columns={this.state.columns}
modalTitle={modalTitle}
onSelectedChange={this.onSelectedChange}
onValidRowChange={this.onValidRowChange}
/>
@@ -336,6 +396,20 @@ class InteractiveImportModalContent extends Component {
<ModalFooter className={styles.footer}>
<div className={styles.leftButtons}>
{
showDelete ?
<SpinnerButton
className={styles.deleteButton}
kind={kinds.DANGER}
isSpinning={isDeleting}
isDisabled={!selectedIds.length || !!withoutEpisodeFileIdRowsSelected.length}
onPress={this.onDeleteSelectedPress}
>
Delete
</SpinnerButton> :
null
}
{
!downloadId && showImportMode ?
<SelectInput
@@ -381,6 +455,7 @@ class InteractiveImportModalContent extends Component {
<SelectSeriesModal
isOpen={selectModalOpen === SERIES}
ids={selectedIds}
modalTitle={modalTitle}
onModalClose={this.onSelectModalClose}
/>
@@ -388,6 +463,7 @@ class InteractiveImportModalContent extends Component {
isOpen={selectModalOpen === SEASON}
ids={selectedIds}
seriesId={selectedItem && selectedItem.series && selectedItem.series.id}
modalTitle={modalTitle}
onModalClose={this.onSelectModalClose}
/>
@@ -396,6 +472,15 @@ class InteractiveImportModalContent extends Component {
ids={orderedSelectedIds}
seriesId={selectedItem && selectedItem.series && selectedItem.series.id}
seasonNumber={selectedItem && selectedItem.seasonNumber}
modalTitle={modalTitle}
onModalClose={this.onSelectModalClose}
/>
<SelectReleaseGroupModal
isOpen={selectModalOpen === RELEASE_GROUP}
ids={selectedIds}
releaseGroup=""
modalTitle={modalTitle}
onModalClose={this.onSelectModalClose}
/>
@@ -403,6 +488,7 @@ class InteractiveImportModalContent extends Component {
isOpen={selectModalOpen === LANGUAGE}
ids={selectedIds}
languageId={0}
modalTitle={modalTitle}
onModalClose={this.onSelectModalClose}
/>
@@ -412,8 +498,19 @@ class InteractiveImportModalContent extends Component {
qualityId={0}
proper={false}
real={false}
modalTitle={modalTitle}
onModalClose={this.onSelectModalClose}
/>
<ConfirmModal
isOpen={isConfirmDeleteModalOpen}
kind={kinds.DANGER}
title="Delete Selected Episode Files"
message={'Are you sure you want to delete the selected episode files?'}
confirmLabel="Delete"
onConfirm={this.onConfirmDelete}
onCancel={this.onConfirmDeleteModalClose}
/>
</ModalContent>
);
}
@@ -421,7 +518,10 @@ class InteractiveImportModalContent extends Component {
InteractiveImportModalContent.propTypes = {
downloadId: PropTypes.string,
showSeries: PropTypes.bool.isRequired,
allowSeriesChange: PropTypes.bool.isRequired,
autoSelectRow: PropTypes.bool.isRequired,
showDelete: PropTypes.bool.isRequired,
showImportMode: PropTypes.bool.isRequired,
showFilterExistingFiles: PropTypes.bool.isRequired,
filterExistingFiles: PropTypes.bool.isRequired,
@@ -435,16 +535,23 @@ InteractiveImportModalContent.propTypes = {
sortKey: PropTypes.string,
sortDirection: PropTypes.string,
interactiveImportErrorMessage: PropTypes.string,
isDeleting: PropTypes.bool.isRequired,
deleteError: PropTypes.object,
modalTitle: PropTypes.string.isRequired,
onSortPress: PropTypes.func.isRequired,
onFilterExistingFilesChange: PropTypes.func.isRequired,
onImportModeChange: PropTypes.func.isRequired,
onDeleteSelectedPress: PropTypes.func.isRequired,
onImportSelectedPress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
InteractiveImportModalContent.defaultProps = {
showSeries: true,
allowSeriesChange: true,
autoSelectRow: true,
showFilterExistingFiles: false,
showDelete: false,
showImportMode: true,
importMode: 'move'
};

View File

@@ -1,19 +1,49 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import { sortDirections } from 'Helpers/Props';
import { fetchInteractiveImportItems, setInteractiveImportSort, clearInteractiveImport, setInteractiveImportMode } from 'Store/Actions/interactiveImportActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import { executeCommand } from 'Store/Actions/commandActions';
import { updateEpisodeFiles, deleteEpisodeFiles } from 'Store/Actions/episodeFileActions';
import * as commandNames from 'Commands/commandNames';
import InteractiveImportModalContent from './InteractiveImportModalContent';
function isSameEpisodeFile(file, originalFile) {
const {
series,
seasonNumber,
episodes
} = file;
if (!originalFile) {
return false;
}
if (!originalFile.series || series.id !== originalFile.series.id) {
return false;
}
if (seasonNumber !== originalFile.seasonNumber) {
return false;
}
return !hasDifferentItems(originalFile.episodes, episodes);
}
function createMapStateToProps() {
return createSelector(
createClientSideCollectionSelector('interactiveImport'),
(interactiveImport) => {
return interactiveImport;
(state) => state.episodeFiles.isDeleting,
(state) => state.episodeFiles.deleteError,
(interactiveImport, isDeleting, deleteError) => {
return {
...interactiveImport,
isDeleting,
deleteError
};
}
);
}
@@ -23,6 +53,8 @@ const mapDispatchToProps = {
dispatchSetInteractiveImportSort: setInteractiveImportSort,
dispatchSetInteractiveImportMode: setInteractiveImportMode,
dispatchClearInteractiveImport: clearInteractiveImport,
dispatchUpdateEpisodeFiles: updateEpisodeFiles,
dispatchDeleteEpisodeFiles: deleteEpisodeFiles,
dispatchExecuteCommand: executeCommand
};
@@ -44,16 +76,34 @@ class InteractiveImportModalContentConnector extends Component {
const {
downloadId,
seriesId,
folder
seasonNumber,
folder,
initialSortKey,
initialSortDirection,
dispatchSetInteractiveImportSort,
dispatchFetchInteractiveImportItems
} = this.props;
const {
filterExistingFiles
} = this.state;
this.props.dispatchFetchInteractiveImportItems({
if (initialSortKey) {
const sortProps = {
sortKey: initialSortKey
};
if (initialSortDirection) {
sortProps.sortDirection = initialSortDirection;
}
dispatchSetInteractiveImportSort(sortProps);
}
dispatchFetchInteractiveImportItems({
downloadId,
seriesId,
seasonNumber,
folder,
filterExistingFiles
});
@@ -99,10 +149,41 @@ class InteractiveImportModalContentConnector extends Component {
this.props.dispatchSetInteractiveImportMode({ importMode });
}
onDeleteSelectedPress = (selected) => {
const {
items,
dispatchDeleteEpisodeFiles
} = this.props;
const episodeFileIds = items.reduce((acc, item) => {
if (selected.indexOf(item.id) > -1 && item.episodeFileId) {
acc.push(item.episodeFileId);
}
return acc;
}, []);
dispatchDeleteEpisodeFiles({ episodeFileIds });
}
onImportSelectedPress = (selected, importMode) => {
const {
items,
originalItems,
dispatchUpdateEpisodeFiles,
dispatchExecuteCommand,
onModalClose
} = this.props;
const existingFiles = [];
const files = [];
_.forEach(this.props.items, (item) => {
if (importMode === 'chooseImportMode') {
this.setState({ interactiveImportErrorMessage: 'An import mode must be selected' });
return;
}
items.forEach((item) => {
const isSelected = selected.indexOf(item.id) > -1;
if (isSelected) {
@@ -110,33 +191,50 @@ class InteractiveImportModalContentConnector extends Component {
series,
seasonNumber,
episodes,
releaseGroup,
quality,
language
language,
episodeFileId
} = item;
if (!series) {
this.setState({ interactiveImportErrorMessage: 'Series must be chosen for each selected file' });
return false;
return;
}
if (isNaN(seasonNumber)) {
this.setState({ interactiveImportErrorMessage: 'Season must be chosen for each selected file' });
return false;
return;
}
if (!episodes || !episodes.length) {
this.setState({ interactiveImportErrorMessage: 'One or more episodes must be chosen for each selected file' });
return false;
return;
}
if (!quality) {
this.setState({ interactiveImportErrorMessage: 'Quality must be chosen for each selected file' });
return false;
return;
}
if (!language) {
this.setState({ interactiveImportErrorMessage: 'Language must be chosen for each selected file' });
return false;
return;
}
if (episodeFileId) {
const originalItem = originalItems.find((i) => i.id === item.id);
if (isSameEpisodeFile(item, originalItem)) {
existingFiles.push({
id: episodeFileId,
releaseGroup,
quality,
language
});
return;
}
}
files.push({
@@ -144,24 +242,38 @@ class InteractiveImportModalContentConnector extends Component {
folderName: item.folderName,
seriesId: series.id,
episodeIds: episodes.map((e) => e.id),
releaseGroup,
quality,
language,
downloadId: this.props.downloadId
downloadId: this.props.downloadId,
episodeFileId
});
}
});
if (!files.length) {
return;
let shouldClose = false;
if (existingFiles.length) {
dispatchUpdateEpisodeFiles({
files: existingFiles
});
shouldClose = true;
}
this.props.dispatchExecuteCommand({
name: commandNames.INTERACTIVE_IMPORT,
files,
importMode
});
if (files.length) {
dispatchExecuteCommand({
name: commandNames.INTERACTIVE_IMPORT,
files,
importMode
});
this.props.onModalClose();
shouldClose = true;
}
if (shouldClose) {
onModalClose();
}
}
//
@@ -181,6 +293,7 @@ class InteractiveImportModalContentConnector extends Component {
onSortPress={this.onSortPress}
onFilterExistingFilesChange={this.onFilterExistingFilesChange}
onImportModeChange={this.onImportModeChange}
onDeleteSelectedPress={this.onDeleteSelectedPress}
onImportSelectedPress={this.onImportSelectedPress}
/>
);
@@ -190,13 +303,19 @@ class InteractiveImportModalContentConnector extends Component {
InteractiveImportModalContentConnector.propTypes = {
downloadId: PropTypes.string,
seriesId: PropTypes.number,
seasonNumber: PropTypes.number,
folder: PropTypes.string,
filterExistingFiles: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
initialSortKey: PropTypes.string,
initialSortDirection: PropTypes.oneOf(sortDirections.all),
originalItems: PropTypes.arrayOf(PropTypes.object).isRequired,
dispatchFetchInteractiveImportItems: PropTypes.func.isRequired,
dispatchSetInteractiveImportSort: PropTypes.func.isRequired,
dispatchSetInteractiveImportMode: PropTypes.func.isRequired,
dispatchClearInteractiveImport: PropTypes.func.isRequired,
dispatchUpdateEpisodeFiles: PropTypes.func.isRequired,
dispatchDeleteEpisodeFiles: PropTypes.func.isRequired,
dispatchExecuteCommand: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

View File

@@ -16,6 +16,7 @@ import SelectSeriesModal from 'InteractiveImport/Series/SelectSeriesModal';
import SelectSeasonModal from 'InteractiveImport/Season/SelectSeasonModal';
import SelectEpisodeModal from 'InteractiveImport/Episode/SelectEpisodeModal';
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
import InteractiveImportRowCellPlaceholder from './InteractiveImportRowCellPlaceholder';
import styles from './InteractiveImportRow.css';
@@ -32,6 +33,7 @@ class InteractiveImportRow extends Component {
isSelectSeriesModalOpen: false,
isSelectSeasonModalOpen: false,
isSelectEpisodeModalOpen: false,
isSelectReleaseGroupModalOpen: false,
isSelectQualityModalOpen: false,
isSelectLanguageModalOpen: false
};
@@ -39,23 +41,35 @@ class InteractiveImportRow extends Component {
componentDidMount() {
const {
allowSeriesChange,
id,
series,
seasonNumber,
episodes,
quality,
language
language,
episodeFileId,
columns
} = this.props;
if (
allowSeriesChange &&
series &&
seasonNumber != null &&
episodes.length &&
quality &&
language
) {
this.props.onSelectedChange({ id, value: true });
this.props.onSelectedChange({
id,
hasEpisodeFileId: !!episodeFileId,
value: true
});
}
this.setState({
isSeriesColumnVisible: columns.find((c) => c.name === 'series').isVisible
});
}
componentDidUpdate(prevProps) {
@@ -102,17 +116,34 @@ class InteractiveImportRow extends Component {
selectRowAfterChange = (value) => {
const {
id,
episodeFileId,
isSelected
} = this.props;
if (!isSelected && value === true) {
this.props.onSelectedChange({ id, value });
this.props.onSelectedChange({
id,
hasEpisodeFileId: !!episodeFileId,
value
});
}
}
//
// Listeners
onSelectedChange = (result) => {
const {
episodeFileId,
onSelectedChange
} = this.props;
onSelectedChange({
...result,
hasEpisodeFileId: !!episodeFileId
});
}
onSelectSeriesPress = () => {
this.setState({ isSelectSeriesModalOpen: true });
}
@@ -125,6 +156,10 @@ class InteractiveImportRow extends Component {
this.setState({ isSelectEpisodeModalOpen: true });
}
onSelectReleaseGroupPress = () => {
this.setState({ isSelectReleaseGroupModalOpen: true });
}
onSelectQualityPress = () => {
this.setState({ isSelectQualityModalOpen: true });
}
@@ -148,6 +183,11 @@ class InteractiveImportRow extends Component {
this.selectRowAfterChange(changed);
}
onSelectReleaseGroupModalClose = (changed) => {
this.setState({ isSelectReleaseGroupModalOpen: false });
this.selectRowAfterChange(changed);
}
onSelectQualityModalClose = (changed) => {
this.setState({ isSelectQualityModalOpen: false });
this.selectRowAfterChange(changed);
@@ -171,17 +211,19 @@ class InteractiveImportRow extends Component {
episodes,
quality,
language,
releaseGroup,
size,
rejections,
isReprocessing,
isSelected,
onSelectedChange
modalTitle
} = this.props;
const {
isSelectSeriesModalOpen,
isSelectSeasonModalOpen,
isSelectEpisodeModalOpen,
isSelectReleaseGroupModalOpen,
isSelectQualityModalOpen,
isSelectLanguageModalOpen
} = this.state;
@@ -193,7 +235,13 @@ class InteractiveImportRow extends Component {
return (
<div key={episode.id}>
{episode.episodeNumber}
{isAnime ? ` (${episode.absoluteEpisodeNumber})` : ''}
{
isAnime && episode.absoluteEpisodeNumber != null ?
` (${episode.absoluteEpisodeNumber})` :
''
}
{` - ${episode.title}`}
</div>
);
@@ -202,6 +250,7 @@ class InteractiveImportRow extends Component {
const showSeriesPlaceholder = isSelected && !series;
const showSeasonNumberPlaceholder = isSelected && !!series && isNaN(seasonNumber) && !isReprocessing;
const showEpisodeNumbersPlaceholder = isSelected && Number.isInteger(seasonNumber) && !episodes.length;
const showReleaseGroupPlaceholder = isSelected && !releaseGroup;
const showQualityPlaceholder = isSelected && !quality;
const showLanguagePlaceholder = isSelected && !language;
@@ -210,7 +259,7 @@ class InteractiveImportRow extends Component {
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChange}
onSelectedChange={this.onSelectedChange}
/>
<TableRowCell
@@ -220,15 +269,19 @@ class InteractiveImportRow extends Component {
{relativePath}
</TableRowCell>
<TableRowCellButton
isDisabled={!allowSeriesChange}
title={allowSeriesChange ? 'Click to change series' : undefined}
onPress={this.onSelectSeriesPress}
>
{
showSeriesPlaceholder ? <InteractiveImportRowCellPlaceholder /> : seriesTitle
}
</TableRowCellButton>
{
this.state.isSeriesColumnVisible ?
<TableRowCellButton
isDisabled={!allowSeriesChange}
title={allowSeriesChange ? 'Click to change series' : undefined}
onPress={this.onSelectSeriesPress}
>
{
showSeriesPlaceholder ? <InteractiveImportRowCellPlaceholder /> : seriesTitle
}
</TableRowCellButton> :
null
}
<TableRowCellButton
isDisabled={!series}
@@ -246,7 +299,6 @@ class InteractiveImportRow extends Component {
/> : null
}
</TableRowCellButton>
<TableRowCellButton
@@ -259,6 +311,17 @@ class InteractiveImportRow extends Component {
}
</TableRowCellButton>
<TableRowCellButton
title="Click to change release group"
onPress={this.onSelectReleaseGroupPress}
>
{
showReleaseGroupPlaceholder ?
<InteractiveImportRowCellPlaceholder /> :
releaseGroup
}
</TableRowCellButton>
<TableRowCellButton
className={styles.quality}
title="Click to change quality"
@@ -334,6 +397,7 @@ class InteractiveImportRow extends Component {
<SelectSeriesModal
isOpen={isSelectSeriesModalOpen}
ids={[id]}
modalTitle={modalTitle}
onModalClose={this.onSelectSeriesModalClose}
/>
@@ -341,6 +405,7 @@ class InteractiveImportRow extends Component {
isOpen={isSelectSeasonModalOpen}
ids={[id]}
seriesId={series && series.id}
modalTitle={modalTitle}
onModalClose={this.onSelectSeasonModalClose}
/>
@@ -351,15 +416,25 @@ class InteractiveImportRow extends Component {
isAnime={isAnime}
seasonNumber={seasonNumber}
relativePath={relativePath}
modalTitle={modalTitle}
onModalClose={this.onSelectEpisodeModalClose}
/>
<SelectReleaseGroupModal
isOpen={isSelectReleaseGroupModalOpen}
ids={[id]}
releaseGroup={releaseGroup ?? ''}
modalTitle={modalTitle}
onModalClose={this.onSelectReleaseGroupModalClose}
/>
<SelectQualityModal
isOpen={isSelectQualityModalOpen}
ids={[id]}
qualityId={quality ? quality.quality.id : 0}
proper={quality ? quality.revision.version > 1 : false}
real={quality ? quality.revision.real > 0 : false}
modalTitle={modalTitle}
onModalClose={this.onSelectQualityModalClose}
/>
@@ -367,6 +442,7 @@ class InteractiveImportRow extends Component {
isOpen={isSelectLanguageModalOpen}
ids={[id]}
languageId={language ? language.id : 0}
modalTitle={modalTitle}
onModalClose={this.onSelectLanguageModalClose}
/>
</TableRow>
@@ -382,12 +458,16 @@ InteractiveImportRow.propTypes = {
series: PropTypes.object,
seasonNumber: PropTypes.number,
episodes: PropTypes.arrayOf(PropTypes.object).isRequired,
releaseGroup: PropTypes.string,
quality: PropTypes.object,
language: PropTypes.object,
size: PropTypes.number.isRequired,
rejections: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
episodeFileId: PropTypes.number,
isReprocessing: PropTypes.bool,
isSelected: PropTypes.bool,
modalTitle: PropTypes.string.isRequired,
onSelectedChange: PropTypes.func.isRequired,
onValidRowChange: PropTypes.func.isRequired
};

View File

@@ -75,7 +75,12 @@ InteractiveImportModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
folder: PropTypes.string,
downloadId: PropTypes.string,
modalTitle: PropTypes.string.isRequired,
onModalClose: PropTypes.func.isRequired
};
InteractiveImportModal.defaultProps = {
modalTitle: 'Manual Import'
};
export default InteractiveImportModal;

View File

@@ -19,6 +19,7 @@ function SelectLanguageModalContent(props) {
isPopulated,
error,
items,
modalTitle,
onModalClose,
onLanguageSelect
} = props;
@@ -33,7 +34,7 @@ function SelectLanguageModalContent(props) {
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Manual Import - Select Language
{modalTitle} - Select Language
</ModalHeader>
<ModalBody>
@@ -80,6 +81,7 @@ SelectLanguageModalContent.propTypes = {
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
modalTitle: PropTypes.string.isRequired,
onLanguageSelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

View File

@@ -61,6 +61,7 @@ class SelectQualityModalContent extends Component {
isPopulated,
error,
items,
modalTitle,
onModalClose
} = this.props;
@@ -80,7 +81,7 @@ class SelectQualityModalContent extends Component {
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Manual Import - Select Quality
{modalTitle} - Select Quality
</ModalHeader>
<ModalBody>
@@ -159,6 +160,7 @@ SelectQualityModalContent.propTypes = {
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
modalTitle: PropTypes.string.isRequired,
onQualitySelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

View File

@@ -0,0 +1,37 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Modal from 'Components/Modal/Modal';
import SelectReleaseGroupModalContentConnector from './SelectReleaseGroupModalContentConnector';
class SelectReleaseGroupModal extends Component {
//
// Render
render() {
const {
isOpen,
onModalClose,
...otherProps
} = this.props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<SelectReleaseGroupModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
}
SelectReleaseGroupModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default SelectReleaseGroupModal;

View File

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

View File

@@ -0,0 +1,105 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { inputTypes, kinds, scrollDirections } from 'Helpers/Props';
import Button from 'Components/Link/Button';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormLabel from 'Components/Form/FormLabel';
import FormInputGroup from 'Components/Form/FormInputGroup';
import ModalContent from 'Components/Modal/ModalContent';
import ModalHeader from 'Components/Modal/ModalHeader';
import ModalBody from 'Components/Modal/ModalBody';
import ModalFooter from 'Components/Modal/ModalFooter';
import styles from './SelectReleaseGroupModalContent.css';
class SelectReleaseGroupModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
const {
releaseGroup
} = props;
this.state = {
releaseGroup
};
}
//
// Listeners
onReleaseGroupChange = ({ value }) => {
this.setState({ releaseGroup: value });
}
onReleaseGroupSelect = () => {
this.props.onReleaseGroupSelect(this.state);
}
//
// Render
render() {
const {
modalTitle,
onModalClose
} = this.props;
const {
releaseGroup
} = this.state;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{modalTitle} - Set Release Group
</ModalHeader>
<ModalBody
className={styles.modalBody}
scrollDirection={scrollDirections.NONE}
>
<Form>
<FormGroup>
<FormLabel>Release Group</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="releaseGroup"
value={releaseGroup}
autoFocus={true}
onChange={this.onReleaseGroupChange}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
Cancel
</Button>
<Button
kind={kinds.SUCCESS}
onPress={this.onReleaseGroupSelect}
>
Set Release Group
</Button>
</ModalFooter>
</ModalContent>
);
}
}
SelectReleaseGroupModalContent.propTypes = {
releaseGroup: PropTypes.string.isRequired,
modalTitle: PropTypes.string.isRequired,
onReleaseGroupSelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default SelectReleaseGroupModalContent;

View File

@@ -0,0 +1,54 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { updateInteractiveImportItems, reprocessInteractiveImportItems } from 'Store/Actions/interactiveImportActions';
import SelectReleaseGroupModalContent from './SelectReleaseGroupModalContent';
const mapDispatchToProps = {
dispatchUpdateInteractiveImportItems: updateInteractiveImportItems,
dispatchReprocessInteractiveImportItems: reprocessInteractiveImportItems
};
class SelectReleaseGroupModalContentConnector extends Component {
//
// Listeners
onReleaseGroupSelect = ({ releaseGroup }) => {
const {
ids,
dispatchUpdateInteractiveImportItems,
dispatchReprocessInteractiveImportItems
} = this.props;
dispatchUpdateInteractiveImportItems({
ids,
releaseGroup
});
dispatchReprocessInteractiveImportItems({ ids });
this.props.onModalClose(true);
}
//
// Render
render() {
return (
<SelectReleaseGroupModalContent
{...this.props}
onReleaseGroupSelect={this.onReleaseGroupSelect}
/>
);
}
}
SelectReleaseGroupModalContentConnector.propTypes = {
ids: PropTypes.arrayOf(PropTypes.number).isRequired,
dispatchUpdateInteractiveImportItems: PropTypes.func.isRequired,
dispatchReprocessInteractiveImportItems: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(null, mapDispatchToProps)(SelectReleaseGroupModalContentConnector);

View File

@@ -15,6 +15,7 @@ class SelectSeasonModalContent extends Component {
render() {
const {
items,
modalTitle,
onSeasonSelect,
onModalClose
} = this.props;
@@ -22,7 +23,7 @@ class SelectSeasonModalContent extends Component {
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Manual Import - Select Season
{modalTitle} - Select Season
</ModalHeader>
<ModalBody>
@@ -51,6 +52,7 @@ class SelectSeasonModalContent extends Component {
SelectSeasonModalContent.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired,
modalTitle: PropTypes.string.isRequired,
onSeasonSelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

View File

@@ -37,6 +37,7 @@ class SelectSeriesModalContent extends Component {
render() {
const {
items,
modalTitle,
onSeriesSelect,
onModalClose
} = this.props;
@@ -47,7 +48,7 @@ class SelectSeriesModalContent extends Component {
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Manual Import - Select Series
{modalTitle} - Select Series
</ModalHeader>
<ModalBody
@@ -96,6 +97,7 @@ class SelectSeriesModalContent extends Component {
SelectSeriesModalContent.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired,
modalTitle: PropTypes.string.isRequired,
onSeriesSelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

View File

@@ -175,7 +175,7 @@ function InteractiveSearch(props) {
items.map((item) => {
return (
<InteractiveSearchRow
key={item.guid}
key={`${item.indexerId}-${item.guid}`}
{...item}
searchPayload={searchPayload}
longDateFormat={longDateFormat}

View File

@@ -35,12 +35,12 @@ function getDownloadTooltip(isGrabbing, isGrabbed, grabError) {
if (isGrabbing) {
return '';
} else if (isGrabbed) {
return 'Added to downloaded queue';
return 'Added to download queue';
} else if (grabError) {
return grabError;
}
return 'Add to downloaded queue';
return 'Add to download queue';
}
class InteractiveSearchRow extends Component {

View File

@@ -192,7 +192,7 @@ OrganizePreviewModalContent.propTypes = {
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
seasonNumber: PropTypes.string.isRequired,
seasonNumber: PropTypes.number,
path: PropTypes.string.isRequired,
renameEpisodes: PropTypes.bool,
episodeFormat: PropTypes.string,

View File

@@ -36,3 +36,17 @@
width: 100px;
}
.audioLanguages,
.videoDynamicRangeType,
.subtitles {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 165px;
}
.releaseGroup {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 120px;
}

View File

@@ -68,6 +68,7 @@ class EpisodeRow extends Component {
episodeFilePath,
episodeFileRelativePath,
episodeFileSize,
releaseGroup,
alternateTitles,
columns
} = this.props;
@@ -195,6 +196,34 @@ class EpisodeRow extends Component {
);
}
if (name === 'audioLanguages') {
return (
<TableRowCell
key={name}
className={styles.audioLanguages}
>
<MediaInfoConnector
type={mediaInfoTypes.AUDIO_LANGUAGES}
episodeFileId={episodeFileId}
/>
</TableRowCell>
);
}
if (name === 'subtitleLanguages') {
return (
<TableRowCell
key={name}
className={styles.subtitles}
>
<MediaInfoConnector
type={mediaInfoTypes.SUBTITLES}
episodeFileId={episodeFileId}
/>
</TableRowCell>
);
}
if (name === 'videoCodec') {
return (
<TableRowCell
@@ -209,6 +238,20 @@ class EpisodeRow extends Component {
);
}
if (name === 'videoDynamicRangeType') {
return (
<TableRowCell
key={name}
className={styles.videoDynamicRangeType}
>
<MediaInfoConnector
type={mediaInfoTypes.VIDEO_DYNAMIC_RANGE_TYPE}
episodeFileId={episodeFileId}
/>
</TableRowCell>
);
}
if (name === 'size') {
return (
<TableRowCell
@@ -220,6 +263,17 @@ class EpisodeRow extends Component {
);
}
if (name === 'releaseGroup') {
return (
<TableRowCell
key={name}
className={styles.releaseGroup}
>
{releaseGroup}
</TableRowCell>
);
}
if (name === 'status') {
return (
<TableRowCell
@@ -274,6 +328,7 @@ EpisodeRow.propTypes = {
episodeFilePath: PropTypes.string,
episodeFileRelativePath: PropTypes.string,
episodeFileSize: PropTypes.number,
releaseGroup: PropTypes.string,
mediaInfo: PropTypes.object,
alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,

View File

@@ -17,6 +17,7 @@ function createMapStateToProps() {
episodeFilePath: episodeFile ? episodeFile.path : null,
episodeFileRelativePath: episodeFile ? episodeFile.relativePath : null,
episodeFileSize: episodeFile ? episodeFile.size : null,
releaseGroup: episodeFile ? episodeFile.releaseGroup : null,
alternateTitles: series.alternateTitles
};
}

View File

@@ -5,7 +5,7 @@ import TextTruncate from 'react-text-truncate';
import formatBytes from 'Utilities/Number/formatBytes';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import { align, icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
import { align, icons, kinds, sizes, sortDirections, tooltipPositions } from 'Helpers/Props';
import fonts from 'Styles/Variables/fonts';
import HeartRating from 'Components/HeartRating';
import Icon from 'Components/Icon';
@@ -22,7 +22,6 @@ import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import Popover from 'Components/Tooltip/Popover';
import Tooltip from 'Components/Tooltip/Tooltip';
import EpisodeFileEditorModal from 'EpisodeFile/Editor/EpisodeFileEditorModal';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
@@ -32,6 +31,7 @@ import DeleteSeriesModal from 'Series/Delete/DeleteSeriesModal';
import SeriesHistoryModal from 'Series/History/SeriesHistoryModal';
import SeriesAlternateTitles from './SeriesAlternateTitles';
import SeriesDetailsSeasonConnector from './SeriesDetailsSeasonConnector';
import SeriesGenres from './SeriesGenres';
import SeriesTagsConnector from './SeriesTagsConnector';
import SeriesDetailsLinks from './SeriesDetailsLinks';
import MonitoringOptionsModal from 'Series/MonitoringOptions/MonitoringOptionsModal';
@@ -57,6 +57,11 @@ function getExpandedState(newState) {
};
}
function getDateYear(date) {
const dateDate = new Date(date);
return dateDate.getFullYear();
}
class SeriesDetails extends Component {
//
@@ -71,7 +76,6 @@ class SeriesDetails extends Component {
isEditSeriesModalOpen: false,
isDeleteSeriesModalOpen: false,
isSeriesHistoryModalOpen: false,
isInteractiveImportModalOpen: false,
isMonitorOptionsModalOpen: false,
allExpanded: false,
allCollapsed: false,
@@ -99,14 +103,6 @@ class SeriesDetails extends Component {
this.setState({ isManageEpisodesOpen: false });
}
onInteractiveImportPress = () => {
this.setState({ isInteractiveImportModalOpen: true });
}
onInteractiveImportModalClose = () => {
this.setState({ isInteractiveImportModalOpen: false });
}
onEditSeriesPress = () => {
this.setState({ isEditSeriesModalOpen: true });
}
@@ -191,7 +187,10 @@ class SeriesDetails extends Component {
images,
seasons,
alternateTitles,
genres,
tags,
year,
previousAiring,
isSaving,
isRefreshing,
isSearching,
@@ -220,7 +219,6 @@ class SeriesDetails extends Component {
isEditSeriesModalOpen,
isDeleteSeriesModalOpen,
isSeriesHistoryModalOpen,
isInteractiveImportModalOpen,
isMonitorOptionsModalOpen,
allExpanded,
allCollapsed,
@@ -229,6 +227,7 @@ class SeriesDetails extends Component {
} = this.state;
const statusDetails = getSeriesStatusDetails(status);
const runningYears = statusDetails.title === 'Ended' ? `${year}-${getDateYear(previousAiring)}` : `${year}-`;
let episodeFilesCountMessage = 'No episode files';
@@ -280,7 +279,6 @@ class SeriesDetails extends Component {
<PageToolbarButton
label="Manage Episodes"
iconName={icons.EPISODE_FILE}
isDisabled={!hasEpisodeFiles}
onPress={this.onManageEpisodesPress}
/>
@@ -291,12 +289,6 @@ class SeriesDetails extends Component {
onPress={this.onSeriesHistoryPress}
/>
<PageToolbarButton
label="Manual File Import"
iconName={icons.INTERACTIVE}
onPress={this.onInteractiveImportPress}
/>
<PageToolbarSeparator />
<PageToolbarButton
@@ -414,6 +406,12 @@ class SeriesDetails extends Component {
rating={ratings.value}
iconSize={20}
/>
<SeriesGenres genres={genres} />
<span>
{runningYears}
</span>
</div>
</div>
@@ -640,9 +638,19 @@ class SeriesDetails extends Component {
onModalClose={this.onOrganizeModalClose}
/>
<EpisodeFileEditorModal
<InteractiveImportModal
isOpen={isManageEpisodesOpen}
seriesId={id}
title={title}
folder={path}
initialSortKey="relativePath"
initialSortDirection={sortDirections.DESCENDING}
showSeries={false}
allowSeriesChange={false}
autoSelectRow={false}
showDelete={true}
showImportMode={false}
modalTitle={'Manage Episodes'}
onModalClose={this.onManageEpisodesModalClose}
/>
@@ -665,16 +673,6 @@ class SeriesDetails extends Component {
onModalClose={this.onDeleteSeriesModalClose}
/>
<InteractiveImportModal
isOpen={isInteractiveImportModalOpen}
seriesId={id}
folder={path}
allowSeriesChange={false}
showFilterExistingFiles={true}
showImportMode={false}
onModalClose={this.onInteractiveImportModalClose}
/>
<MonitoringOptionsModal
isOpen={isMonitorOptionsModalOpen}
seriesId={id}
@@ -705,7 +703,10 @@ SeriesDetails.propTypes = {
images: PropTypes.arrayOf(PropTypes.object).isRequired,
seasons: PropTypes.arrayOf(PropTypes.object).isRequired,
alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired,
genres: PropTypes.arrayOf(PropTypes.string).isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
year: PropTypes.number.isRequired,
previousAiring: PropTypes.string,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
isRefreshing: PropTypes.bool.isRequired,

View File

@@ -5,7 +5,7 @@ import isAfter from 'Utilities/Date/isAfter';
import isBefore from 'Utilities/Date/isBefore';
import formatBytes from 'Utilities/Number/formatBytes';
import getToggledRange from 'Utilities/Table/getToggledRange';
import { align, icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
import { align, icons, kinds, sizes, sortDirections, tooltipPositions } from 'Helpers/Props';
import Icon from 'Components/Icon';
import IconButton from 'Components/Link/IconButton';
import Label from 'Components/Label';
@@ -20,7 +20,7 @@ import MenuItem from 'Components/Menu/MenuItem';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import Popover from 'Components/Tooltip/Popover';
import EpisodeFileEditorModal from 'EpisodeFile/Editor/EpisodeFileEditorModal';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
import SeriesHistoryModal from 'Series/History/SeriesHistoryModal';
import SeasonInteractiveSearchModalConnector from 'Series/Search/SeasonInteractiveSearchModalConnector';
@@ -204,6 +204,7 @@ class SeriesDetailsSeason extends Component {
render() {
const {
seriesId,
path,
monitored,
seasonNumber,
items,
@@ -234,6 +235,8 @@ class SeriesDetailsSeason extends Component {
isInteractiveSearchModalOpen
} = this.state;
const title = seasonNumber === 0 ? 'Specials' : `Season ${seasonNumber}`;
return (
<div
className={styles.season}
@@ -248,15 +251,9 @@ class SeriesDetailsSeason extends Component {
onPress={onMonitorSeasonPress}
/>
{
seasonNumber === 0 ?
<span className={styles.seasonNumber}>
Specials
</span> :
<span className={styles.seasonNumber}>
Season {seasonNumber}
</span>
}
<span className={styles.seasonNumber}>
{title}
</span>
<Popover
className={styles.episodeCountTooltip}
@@ -486,10 +483,19 @@ class SeriesDetailsSeason extends Component {
onModalClose={this.onOrganizeModalClose}
/>
<EpisodeFileEditorModal
<InteractiveImportModal
isOpen={isManageEpisodesOpen}
seriesId={seriesId}
seasonNumber={seasonNumber}
title={title}
folder={path}
initialSortKey="relativePath"
initialSortDirection={sortDirections.DESCENDING}
showSeries={false}
allowSeriesChange={false}
autoSelectRow={false}
showDelete={true}
showImportMode={false}
onModalClose={this.onManageEpisodesModalClose}
/>
@@ -513,6 +519,7 @@ class SeriesDetailsSeason extends Component {
SeriesDetailsSeason.propTypes = {
seriesId: PropTypes.number.isRequired,
path: PropTypes.string.isRequired,
monitored: PropTypes.bool.isRequired,
seasonNumber: PropTypes.number.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,

View File

@@ -34,6 +34,7 @@ function createMapStateToProps() {
columns: episodes.columns,
isSearching,
seriesMonitored: series.monitored,
path: series.path,
isSmallScreen: dimensions.isSmallScreen
};
}

View File

@@ -0,0 +1,3 @@
.genres {
margin-right: 15px;
}

View File

@@ -0,0 +1,53 @@
import PropTypes from 'prop-types';
import React from 'react';
import { kinds, sizes, tooltipPositions } from 'Helpers/Props';
import Tooltip from 'Components/Tooltip/Tooltip';
import Label from 'Components/Label';
import styles from './SeriesGenres.css';
function SeriesGenres({ genres }) {
const [firstGenre, ...otherGenres] = genres;
if (otherGenres.length) {
return (
<Tooltip
anchor={
<span className={styles.genres}>
{firstGenre}
</span>
}
tooltip={
<div>
{
otherGenres.map((tag) => {
return (
<Label
key={tag}
kind={kinds.INFO}
size={sizes.LARGE}
>
{tag}
</Label>
);
})
}
</div>
}
kind={kinds.INVERSE}
position={tooltipPositions.TOP}
/>
);
}
return (
<span className={styles.genres}>
{firstGenre}
</span>
);
}
SeriesGenres.propTypes = {
genres: PropTypes.arrayOf(PropTypes.string).isRequired
};
export default SeriesGenres;

View File

@@ -270,7 +270,6 @@ SeriesIndexOverview.propTypes = {
};
SeriesIndexOverview.defaultProps = {
overview: '',
statistics: {
seasonCount: 0,
episodeCount: 0,

View File

@@ -60,7 +60,8 @@ class SeriesIndexOverviews extends Component {
columnCount: 1,
posterWidth: 162,
posterHeight: 238,
rowHeight: calculateRowHeight(238, null, props.isSmallScreen, {})
rowHeight: calculateRowHeight(238, null, props.isSmallScreen, {}),
scrollRestored: false
};
this._grid = null;
@@ -71,12 +72,15 @@ class SeriesIndexOverviews extends Component {
items,
sortKey,
overviewOptions,
jumpToCharacter,
scrollTop,
isSmallScreen
} = this.props;
const {
width,
rowHeight
rowHeight,
scrollRestored
} = this.state;
if (prevProps.sortKey !== sortKey ||
@@ -95,6 +99,23 @@ class SeriesIndexOverviews extends Component {
// recomputeGridSize also forces Grid to discard its cache of rendered cells
this._grid.recomputeGridSize();
}
if (this._grid && scrollTop !== 0 && !scrollRestored) {
this.setState({ scrollRestored: true });
this._grid.scrollToPosition({ scrollTop });
}
if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) {
const index = getIndexOfFirstCharacter(items, jumpToCharacter);
if (this._grid && index != null) {
this._grid.scrollToCell({
rowIndex: index,
columnIndex: 0
});
}
}
}
//
@@ -188,7 +209,6 @@ class SeriesIndexOverviews extends Component {
const {
scroller,
items,
jumpToCharacter,
isSmallScreen
} = this.props;
@@ -210,24 +230,6 @@ class SeriesIndexOverviews extends Component {
return <div />;
}
let finalScrollTop = scrollTop;
if (jumpToCharacter != null) {
const index = getIndexOfFirstCharacter(items, jumpToCharacter);
if (index != null) {
if (index > 0) {
// Adjust 5px upwards so there is a gap between the bottom
// of the toolbar and top of the poster.
finalScrollTop = rowHeight * index - 5;
} else {
finalScrollTop = 0;
}
}
}
return (
<div ref={registerChild}>
<Grid
@@ -241,7 +243,7 @@ class SeriesIndexOverviews extends Component {
rowHeight={rowHeight}
width={width}
onScroll={onChildScroll}
scrollTop={finalScrollTop}
scrollTop={scrollTop}
overscanRowCount={2}
cellRenderer={this.cellRenderer}
scrollToAlignment={'start'}
@@ -261,6 +263,7 @@ SeriesIndexOverviews.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired,
sortKey: PropTypes.string,
overviewOptions: PropTypes.object.isRequired,
scrollTop: PropTypes.number.isRequired,
jumpToCharacter: PropTypes.string,
scroller: PropTypes.instanceOf(Element).isRequired,
showRelativeDates: PropTypes.bool.isRequired,

View File

@@ -101,7 +101,8 @@ class SeriesIndexPosters extends Component {
columnCount: 1,
posterWidth: 162,
posterHeight: 238,
rowHeight: calculateRowHeight(238, null, props.isSmallScreen, {})
rowHeight: calculateRowHeight(238, null, props.isSmallScreen, {}),
scrollRestored: false
};
this._isInitialized = false;
@@ -114,6 +115,8 @@ class SeriesIndexPosters extends Component {
items,
sortKey,
posterOptions,
jumpToCharacter,
scrollTop,
isSmallScreen
} = this.props;
@@ -121,7 +124,8 @@ class SeriesIndexPosters extends Component {
width,
columnWidth,
columnCount,
rowHeight
rowHeight,
scrollRestored
} = this.state;
if (prevProps.sortKey !== sortKey ||
@@ -138,6 +142,24 @@ class SeriesIndexPosters extends Component {
// recomputeGridSize also forces Grid to discard its cache of rendered cells
this._grid.recomputeGridSize();
}
if (this._grid && scrollTop !== 0 && !scrollRestored) {
this.setState({ scrollRestored: true });
this._grid.scrollToPosition({ scrollTop });
}
if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) {
const index = getIndexOfFirstCharacter(items, jumpToCharacter);
if (this._grid && index != null) {
const row = Math.floor(index / columnCount);
this._grid.scrollToCell({
rowIndex: row,
columnIndex: 0
});
}
}
}
//
@@ -242,7 +264,6 @@ class SeriesIndexPosters extends Component {
const {
scroller,
items,
jumpToCharacter,
isSmallScreen
} = this.props;
@@ -268,18 +289,6 @@ class SeriesIndexPosters extends Component {
return <div />;
}
let finalScrollTop = scrollTop;
if (jumpToCharacter != null) {
const index = getIndexOfFirstCharacter(items, jumpToCharacter);
if (index != null) {
const row = Math.floor(index / columnCount);
finalScrollTop = rowHeight * row;
}
}
return (
<div ref={registerChild}>
<Grid
@@ -293,7 +302,7 @@ class SeriesIndexPosters extends Component {
rowHeight={rowHeight}
width={width}
onScroll={onChildScroll}
scrollTop={finalScrollTop}
scrollTop={scrollTop}
overscanRowCount={2}
cellRenderer={this.cellRenderer}
scrollToAlignment={'start'}
@@ -314,6 +323,7 @@ SeriesIndexPosters.propTypes = {
sortKey: PropTypes.string,
posterOptions: PropTypes.object.isRequired,
jumpToCharacter: PropTypes.string,
scrollTop: PropTypes.number.isRequired,
scroller: PropTypes.instanceOf(Element).isRequired,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,

View File

@@ -37,6 +37,7 @@
flex: 1 0 125px;
}
.releaseGroups,
.nextAiring,
.previousAiring,
.added,

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