diff --git a/Makefile b/Makefile index fdef679b8..ef0eade64 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ # If you want to get_images, you'll also need convert from ImageMagick ########################################################################## -VERSION := 2.0.0 +VERSION := 2.0.1 ## usage diff --git a/components/Clock.bs b/components/Clock.bs index 2137a11cb..4233643d7 100644 --- a/components/Clock.bs +++ b/components/Clock.bs @@ -1,8 +1,16 @@ import "pkg:/source/utils/misc.bs" +' @fileoverview Clock component to display current time formatted based on user's chosen 12 or 24 hour setting + +' Possible clock formats +enum ClockFormat + h12 = "12h" + h24 = "24h" +end enum + sub init() - ' If hideclick setting is checked, exit without setting any variables + ' If hideclick setting is enabled, exit without setting any variables if m.global.session.user.settings["ui.design.hideclock"] return end if @@ -16,11 +24,11 @@ sub init() m.currentTimeTimer.control = "start" ' Default to 12 hour clock - m.format = "short-h12" + m.format = ClockFormat.h12 ' If user has selected a 24 hour clock, update date display format - if LCase(m.global.device.clockFormat) = "24h" - m.format = "short-h24" + if LCase(m.global.device.clockFormat) = ClockFormat.h24 + m.format = ClockFormat.h24 end if end sub @@ -34,6 +42,64 @@ sub onCurrentTimeTimerFire() ' Convert to local time zone m.dateTimeObject.ToLocalTime() - ' Format time as requested - m.clockTime.text = m.dateTimeObject.asTimeStringLoc(m.format) + ' Format time for display - based on 12h/24h setting + formattedTime = formatTimeAsString() + + ' Display time + m.clockTime.text = formattedTime end sub + +' formatTimeAsString: Returns a string with the current time formatted for either a 12 or 24 hour clock +' +' @return {string} current time formatted for either a 12 hour or 24 hour clock +function formatTimeAsString() as string + return m.format = ClockFormat.h12 ? format12HourTime() : format24HourTime() +end function + +' format12HourTime: Returns a string with the current time formatted for a 12 hour clock +' +' @return {string} current time formatted for a 12 hour clock +function format12HourTime() as string + currentHour = m.dateTimeObject.GetHours() + currentMinute = m.dateTimeObject.GetMinutes() + + displayedHour = StrI(currentHour).trim() + displayedMinute = StrI(currentMinute).trim() + meridian = currentHour < 12 ? "am" : "pm" + + if currentHour = 0 + displayedHour = "12" + end if + + if currentHour > 12 + correctedHour = currentHour - 12 + displayedHour = StrI(correctedHour).trim() + end if + + if currentMinute < 10 + displayedMinute = `0${displayedMinute}` + end if + + return `${displayedHour}:${displayedMinute} ${meridian}` +end function + +' format24HourTime: Returns a string with the current time formatted for a 24 hour clock +' +' @return {string} current time formatted for a 24 hour clock +function format24HourTime() as string + currentHour = m.dateTimeObject.GetHours() + currentMinute = m.dateTimeObject.GetMinutes() + + displayedHour = StrI(currentHour).trim() + displayedMinute = StrI(currentMinute).trim() + + if currentHour < 10 + displayedHour = `0${displayedHour}` + end if + + if currentMinute < 10 + displayedMinute = `0${displayedMinute}` + end if + + return `${displayedHour}:${displayedMinute}` +end function diff --git a/components/ItemGrid/LoadVideoContentTask.bs b/components/ItemGrid/LoadVideoContentTask.bs index 4e65bdbd2..fa2a7d0e2 100644 --- a/components/ItemGrid/LoadVideoContentTask.bs +++ b/components/ItemGrid/LoadVideoContentTask.bs @@ -7,6 +7,11 @@ import "pkg:/source/api/Image.bs" import "pkg:/source/api/userauth.bs" import "pkg:/source/utils/deviceCapabilities.bs" +enum SubtitleSelection + notset = -2 + none = -1 +end enum + sub init() m.user = AboutMe() m.top.functionName = "loadItems" @@ -44,19 +49,18 @@ sub loadItems() id = m.top.itemId mediaSourceId = invalid audio_stream_idx = m.top.selectedAudioStreamIndex - subtitle_idx = m.top.selectedSubtitleIndex forceTranscoding = false - m.top.content = [LoadItems_VideoPlayer(id, mediaSourceId, audio_stream_idx, subtitle_idx, forceTranscoding)] + m.top.content = [LoadItems_VideoPlayer(id, mediaSourceId, audio_stream_idx, forceTranscoding)] end sub -function LoadItems_VideoPlayer(id as string, mediaSourceId = invalid as dynamic, audio_stream_idx = 1 as integer, subtitle_idx = -1 as integer, forceTranscoding = false as boolean) as dynamic +function LoadItems_VideoPlayer(id as string, mediaSourceId = invalid as dynamic, audio_stream_idx = 1 as integer, forceTranscoding = false as boolean) as dynamic video = {} video.id = id video.content = createObject("RoSGNode", "ContentNode") - LoadItems_AddVideoContent(video, mediaSourceId, audio_stream_idx, subtitle_idx, forceTranscoding) + LoadItems_AddVideoContent(video, mediaSourceId, audio_stream_idx, forceTranscoding) if video.content = invalid return invalid @@ -65,9 +69,10 @@ function LoadItems_VideoPlayer(id as string, mediaSourceId = invalid as dynamic, return video end function -sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_stream_idx = 1 as integer, subtitle_idx = -1 as integer, forceTranscoding = false as boolean) +sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_stream_idx = 1 as integer, forceTranscoding = false as boolean) meta = ItemMetaData(video.id) + subtitle_idx = m.top.selectedSubtitleIndex if not isValid(meta) video.errorMsg = "Error loading metadata" @@ -77,6 +82,21 @@ sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_s videotype = LCase(meta.type) + ' Check for any Live TV streams coming from other places other than the TV Guide + if isValid(meta.json) and isValid(meta.json.ChannelId) + if isValid(meta.json.EpisodeTitle) + meta.title = meta.json.EpisodeTitle + else if isValid(meta.json.Name) + meta.title = meta.json.Name + end if + meta.live = true + if LCase(meta.json.type) = "program" + video.id = meta.json.ChannelId + else + video.id = meta.json.id + end if + end if + if videotype = "episode" or videotype = "series" video.content.contenttype = "episode" end if @@ -107,16 +127,41 @@ sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_s if meta.live then mediaSourceId = "" m.playbackInfo = ItemPostPlaybackInfo(video.id, mediaSourceId, audio_stream_idx, subtitle_idx, playbackPosition) - video.videoId = video.id - video.mediaSourceId = mediaSourceId - video.audioIndex = audio_stream_idx - if not isValid(m.playbackInfo) video.errorMsg = "Error loading playback info" video.content = invalid return end if + addSubtitlesToVideo(video, meta) + + ' Enable default subtitle track + if subtitle_idx = SubtitleSelection.notset + defaultSubtitleIndex = defaultSubtitleTrackFromVid(video.id) + + if defaultSubtitleIndex <> SubtitleSelection.none + video.SelectedSubtitle = defaultSubtitleIndex + subtitle_idx = defaultSubtitleIndex + + m.playbackInfo = ItemPostPlaybackInfo(video.id, mediaSourceId, audio_stream_idx, subtitle_idx, playbackPosition) + if not isValid(m.playbackInfo) + video.errorMsg = "Error loading playback info" + video.content = invalid + return + end if + + addSubtitlesToVideo(video, meta) + else + video.SelectedSubtitle = subtitle_idx + end if + else + video.SelectedSubtitle = subtitle_idx + end if + + video.videoId = video.id + video.mediaSourceId = mediaSourceId + video.audioIndex = audio_stream_idx + video.PlaySessionId = m.playbackInfo.PlaySessionId if meta.live @@ -130,8 +175,6 @@ sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_s m.playbackInfo = meta.json end if - addSubtitlesToVideo(video, meta) - if meta.live video.transcodeParams = { "MediaSourceId": m.playbackInfo.MediaSources[0].Id, @@ -183,13 +226,106 @@ sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_s setCertificateAuthority(video.content) video.audioTrack = (audio_stream_idx + 1).ToStr() ' Roku's track indexes count from 1. Our index is zero based - video.SelectedSubtitle = subtitle_idx - if not fully_external video.content = authRequest(video.content) end if end sub +' defaultSubtitleTrackFromVid: Identifies the default subtitle track given video id +' +' @param {dynamic} videoID - id of video user is playing +' @return {integer} indicating the default track's server-side index. Defaults to {SubtitleSelection.none} is one is not found +function defaultSubtitleTrackFromVid(videoID) as integer + if m.global.session.user.configuration.SubtitleMode = "None" + return SubtitleSelection.none ' No subtitles desired: return none + end if + + meta = ItemMetaData(videoID) + + if not isValid(meta) then return SubtitleSelection.none + if not isValid(meta.json) then return SubtitleSelection.none + if not isValidAndNotEmpty(meta.json.mediaSources) then return SubtitleSelection.none + if not isValidAndNotEmpty(meta.json.MediaSources[0].MediaStreams) then return SubtitleSelection.none + + subtitles = sortSubtitles(meta.id, meta.json.MediaSources[0].MediaStreams) + selectedAudioLanguage = meta.json.MediaSources[0].MediaStreams[m.top.selectedAudioStreamIndex].Language ?? "" + + defaultTextSubs = defaultSubtitleTrack(subtitles["all"], selectedAudioLanguage, true) ' Find correct subtitle track (forced text) + if defaultTextSubs <> SubtitleSelection.none + return defaultTextSubs + end if + + if not m.global.session.user.settings["playback.subs.onlytext"] + return defaultSubtitleTrack(subtitles["all"], selectedAudioLanguage) ' if no appropriate text subs exist, allow non-text + end if + + return SubtitleSelection.none +end function + +' defaultSubtitleTrack: +' +' @param {dynamic} sortedSubtitles - array of subtitles sorted by type and language +' @param {string} selectedAudioLanguage - language for selected audio track +' @param {boolean} [requireText=false] - indicates if only text subtitles should be considered +' @return {integer} indicating the default track's server-side index. Defaults to {SubtitleSelection.none} is one is not found +function defaultSubtitleTrack(sortedSubtitles, selectedAudioLanguage as string, requireText = false as boolean) as integer + userConfig = m.global.session.user.configuration + + subtitleMode = isValid(userConfig.SubtitleMode) ? LCase(userConfig.SubtitleMode) : "" + + allowSmartMode = false + + ' Only evaluate selected audio language if we have a value + if selectedAudioLanguage <> "" + allowSmartMode = selectedAudioLanguage <> userConfig.SubtitleLanguagePreference + end if + + for each item in sortedSubtitles + ' Only auto-select subtitle if language matches SubtitleLanguagePreference + languageMatch = true + if userConfig.SubtitleLanguagePreference <> "" + languageMatch = (userConfig.SubtitleLanguagePreference = item.Track.Language) + end if + + ' Ensure textuality of subtitle matches preferenced passed as arg + matchTextReq = ((requireText and item.IsTextSubtitleStream) or not requireText) + + if languageMatch and matchTextReq + if subtitleMode = "default" and (item.isForced or item.IsDefault) + ' Return first forced or default subtitle track + return item.Index + else if subtitleMode = "always" + ' Return the first found subtitle track + return item.Index + else if subtitleMode = "onlyforced" and item.IsForced + ' Return first forced subtitle track + return item.Index + else if subtitleMode = "smart" and allowSmartMode + ' Return the first found subtitle track + return item.Index + end if + end if + end for + + ' User has chosed smart subtitle mode + ' We already attempted to load subtitles in preferred language, but none were found. + ' Fall back to default behaviour while ignoring preferredlanguage + if subtitleMode = "smart" and allowSmartMode + for each item in sortedSubtitles + ' Ensure textuality of subtitle matches preferenced passed as arg + matchTextReq = ((requireText and item.IsTextSubtitleStream) or not requireText) + if matchTextReq + if item.isForced or item.IsDefault + ' Return first forced or default subtitle track + return item.Index + end if + end if + end for + end if + + return SubtitleSelection.none ' Keep current default behavior of "None", if no correct subtitle is identified +end function + sub addVideoContentURL(video, mediaSourceId, audio_stream_idx, fully_external) protocol = LCase(m.playbackInfo.MediaSources[0].Protocol) if protocol <> "file" diff --git a/components/ItemGrid/LoadVideoContentTask.xml b/components/ItemGrid/LoadVideoContentTask.xml index bf4a88295..f935c3e33 100644 --- a/components/ItemGrid/LoadVideoContentTask.xml +++ b/components/ItemGrid/LoadVideoContentTask.xml @@ -4,7 +4,7 @@ - + diff --git a/components/ItemGrid/MusicLibraryView.bs b/components/ItemGrid/MusicLibraryView.bs index 77d79d62c..ceadaaa5b 100644 --- a/components/ItemGrid/MusicLibraryView.bs +++ b/components/ItemGrid/MusicLibraryView.bs @@ -140,14 +140,12 @@ sub loadInitialItems() m.loadItemsTask.itemId = m.top.parentItem.parentFolder else if LCase(m.view) = "artistspresentation" or LCase(m.options.view) = "artistspresentation" m.loadItemsTask.genreIds = "" - m.top.showItemTitles = "hidealways" else if LCase(m.view) = "artistsgrid" or LCase(m.options.view) = "artistsgrid" m.loadItemsTask.genreIds = "" else if LCase(m.view) = "albumartistsgrid" or LCase(m.options.view) = "albumartistsgrid" m.loadItemsTask.genreIds = "" else if LCase(m.view) = "albumartistspresentation" or LCase(m.options.view) = "albumartistspresentation" m.loadItemsTask.genreIds = "" - m.top.showItemTitles = "hidealways" else m.loadItemsTask.itemId = m.top.parentItem.Id end if diff --git a/components/manager/ViewCreator.bs b/components/manager/ViewCreator.bs index 763834432..ebee6b3a9 100644 --- a/components/manager/ViewCreator.bs +++ b/components/manager/ViewCreator.bs @@ -111,19 +111,20 @@ sub processSubtitleSelection() end if if m.selectedSubtitle.IsEncoded + ' Roku can not natively display these subtitles, so turn off the caption mode on the device m.view.globalCaptionMode = "Off" else + ' Roku can natively display these subtitles, ensure the caption mode on the device is on m.view.globalCaptionMode = "On" - end if - if m.selectedSubtitle.IsExternal + ' Roku may rearrange subtitle tracks. Look up track based on name to ensure we get the correct index availableSubtitleTrackIndex = availSubtitleTrackIdx(m.selectedSubtitle.Track.TrackName) if availableSubtitleTrackIndex = -1 then return m.view.subtitleTrack = m.view.availableSubtitleTracks[availableSubtitleTrackIndex].TrackName - else - m.view.selectedSubtitle = m.selectedSubtitle.Index end if + + m.view.selectedSubtitle = m.selectedSubtitle.Index end sub ' User requested playback info diff --git a/components/tvshows/TVEpisodes.bs b/components/tvshows/TVEpisodes.bs index ffc225cdc..57164ceb3 100644 --- a/components/tvshows/TVEpisodes.bs +++ b/components/tvshows/TVEpisodes.bs @@ -93,6 +93,8 @@ function onKeyEvent(key as string, press as boolean) as boolean focusedItem = getFocusedItem() if isValid(focusedItem) m.top.selectedItem = focusedItem + 'Prevent the selected item event from double firing + m.top.selectedItem = invalid end if return true end if diff --git a/components/video/VideoPlayerView.bs b/components/video/VideoPlayerView.bs index 1aa96ba96..c2a893061 100644 --- a/components/video/VideoPlayerView.bs +++ b/components/video/VideoPlayerView.bs @@ -256,6 +256,9 @@ end sub ' Event handler for when selectedSubtitle changes sub onSubtitleChange() + ' If the global caption mode is on, that means Roku can display the subtitles natively and doesn't need a video stop/start + if LCase(m.top.globalCaptionMode) = "on" then return + ' Save the current video position m.global.queueManager.callFunc("setTopStartingPoint", int(m.top.position) * 10000000&) @@ -332,6 +335,29 @@ sub onVideoContentLoaded() m.top.allowCaptions = true end if + ' Allow default subtitles + m.top.unobserveField("selectedSubtitle") + + ' Set subtitleTrack property is subs are natively supported by Roku + selectedSubtitle = invalid + for each subtitle in m.top.fullSubtitleData + if subtitle.Index = videoContent[0].selectedSubtitle + selectedSubtitle = subtitle + exit for + end if + end for + + if isValid(selectedSubtitle) + availableSubtitleTrackIndex = availSubtitleTrackIdx(selectedSubtitle.Track.TrackName) + if availableSubtitleTrackIndex <> -1 + m.top.subtitleTrack = m.top.availableSubtitleTracks[availableSubtitleTrackIndex].TrackName + end if + end if + + m.top.selectedSubtitle = videoContent[0].selectedSubtitle + + m.top.observeField("selectedSubtitle", "onSubtitleChange") + if isValid(m.top.audioIndex) m.top.audioTrack = (m.top.audioIndex + 1).toStr() else @@ -579,6 +605,25 @@ function stateAllowsOSD() as boolean return inArray(validStates, m.top.state) end function + +' availSubtitleTrackIdx: Returns Roku's index for requested subtitle track +' +' @param {string} tracknameToFind - TrackName for subtitle we're looking to match +' @return {integer} indicating Roku's index for requested subtitle track. Returns -1 if not found +function availSubtitleTrackIdx(tracknameToFind as string) as integer + idx = 0 + for each availTrack in m.top.availableSubtitleTracks + ' The TrackName must contain the URL we supplied originally, though + ' Roku mangles the name a bit, so we check if the URL is a substring, rather + ' than strict equality + if Instr(1, availTrack.TrackName, tracknameToFind) + return idx + end if + idx = idx + 1 + end for + return -1 +end function + function onKeyEvent(key as string, press as boolean) as boolean ' Keypress handler while user is inside the chapter menu diff --git a/components/video/VideoPlayerView.xml b/components/video/VideoPlayerView.xml index 3469b17d9..e33724b00 100644 --- a/components/video/VideoPlayerView.xml +++ b/components/video/VideoPlayerView.xml @@ -6,7 +6,7 @@ - + diff --git a/manifest b/manifest index e4fe84332..af9c146dd 100644 --- a/manifest +++ b/manifest @@ -3,7 +3,7 @@ title=Jellyfin major_version=2 minor_version=0 -build_version=0 +build_version=1 ### Main Menu Icons / Channel Poster Artwork diff --git a/package.json b/package.json index fc011f065..03f9eec4e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "jellyfin-roku", "type": "module", - "version": "2.0.0", + "version": "2.0.1", "description": "Roku app for Jellyfin media server", "dependencies": { "@rokucommunity/bslib": "0.1.1", diff --git a/source/ShowScenes.bs b/source/ShowScenes.bs index 353ee136e..3610b2498 100644 --- a/source/ShowScenes.bs +++ b/source/ShowScenes.bs @@ -811,7 +811,7 @@ function CreateSeasonDetailsGroupByID(seriesID as string, seasonID as string) as group.objects = TVEpisodes(seriesID, seasonID) group.episodeObjects = group.objects ' watch for button presses - group.observeField("episodeSelected", m.port) + group.observeField("selectedItem", m.port) group.observeField("quickPlayNode", m.port) ' don't wait for the extras button stopLoadingSpinner() diff --git a/source/migrations.bs b/source/migrations.bs index 29cb36a37..aecb9d352 100644 --- a/source/migrations.bs +++ b/source/migrations.bs @@ -71,11 +71,13 @@ sub runRegistryUserMigrations() ' app versions < 2.0.0 didn't save LastRunVersion at the user level ' fall back to using the apps lastRunVersion lastRunVersion = m.global.app.lastRunVersion - registry_write("LastRunVersion", lastRunVersion, section) + if isValid(lastRunVersion) + registry_write("LastRunVersion", lastRunVersion, section) + end if end if ' BASE_MIGRATION - if not versionChecker(lastRunVersion, CLIENT_VERSION_REQUIRING_BASE_MIGRATION) + if isValid(lastRunVersion) and not versionChecker(lastRunVersion, CLIENT_VERSION_REQUIRING_BASE_MIGRATION) m.wasMigrated = true print `Running Registry Migration for ${CLIENT_VERSION_REQUIRING_BASE_MIGRATION} for userid: ${section}` diff --git a/source/static/whatsNew/2.0.1.json b/source/static/whatsNew/2.0.1.json new file mode 100644 index 000000000..d93efc8a0 --- /dev/null +++ b/source/static/whatsNew/2.0.1.json @@ -0,0 +1,34 @@ +[ + { + "description": "Fix startup crash", + "author": "cewert" + }, + { + "description": "Fix selection and display of subtitles that are not encoded", + "author": "1hitsong" + }, + { + "description": "Make music artist presentation views honor the Item Titles setting", + "author": "1hitsong" + }, + { + "description": "Fix launching Live TV channels from outside the guide", + "author": "jimdogx" + }, + { + "description": "Fix default subtitle track selection", + "author": "1hitsong" + }, + { + "description": "Fix video playback for Roku devices running a version of Roku OS less than 12.0", + "author": "1hitsong" + }, + { + "description": "Fix selecting episode using OK button on episode list view", + "author": "1hitsong" + }, + { + "description": "Make GH jobs work with new branch workflow", + "author": "cewert" + } +] \ No newline at end of file