mirror of
https://github.com/jellyfin/jellyfin-roku.git
synced 2024-11-23 06:09:41 +00:00
Merge pull request #1965 from jellyfin/2.1.z
This commit is contained in:
commit
6756b0188f
2
Makefile
2
Makefile
@ -3,7 +3,7 @@
|
|||||||
# If you want to get_images, you'll also need convert from ImageMagick
|
# If you want to get_images, you'll also need convert from ImageMagick
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
VERSION := 2.1.5
|
VERSION := 2.1.6
|
||||||
|
|
||||||
## usage
|
## usage
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import "pkg:/source/utils/config.bs"
|
|||||||
sub init()
|
sub init()
|
||||||
m.itemPoster = m.top.findNode("itemPoster")
|
m.itemPoster = m.top.findNode("itemPoster")
|
||||||
m.posterText = m.top.findNode("posterText")
|
m.posterText = m.top.findNode("posterText")
|
||||||
m.title = m.top.findNode("title")
|
initTitle()
|
||||||
m.posterText.font.size = 30
|
m.posterText.font.size = 30
|
||||||
m.title.font.size = 25
|
m.title.font.size = 25
|
||||||
m.backdrop = m.top.findNode("backdrop")
|
m.backdrop = m.top.findNode("backdrop")
|
||||||
@ -23,6 +23,10 @@ sub init()
|
|||||||
end if
|
end if
|
||||||
end sub
|
end sub
|
||||||
|
|
||||||
|
sub initTitle()
|
||||||
|
m.title = m.top.findNode("title")
|
||||||
|
end sub
|
||||||
|
|
||||||
sub itemContentChanged()
|
sub itemContentChanged()
|
||||||
m.backdrop.blendColor = "#101010"
|
m.backdrop.blendColor = "#101010"
|
||||||
|
|
||||||
@ -54,6 +58,8 @@ sub itemContentChanged()
|
|||||||
end sub
|
end sub
|
||||||
|
|
||||||
sub focusChanged()
|
sub focusChanged()
|
||||||
|
if not isValid(m.title) then initTitle()
|
||||||
|
|
||||||
if m.top.itemHasFocus = true
|
if m.top.itemHasFocus = true
|
||||||
m.title.repeatCount = -1
|
m.title.repeatCount = -1
|
||||||
else
|
else
|
||||||
|
@ -6,6 +6,7 @@ import "pkg:/source/utils/config.bs"
|
|||||||
import "pkg:/source/api/Image.bs"
|
import "pkg:/source/api/Image.bs"
|
||||||
import "pkg:/source/api/userauth.bs"
|
import "pkg:/source/api/userauth.bs"
|
||||||
import "pkg:/source/utils/deviceCapabilities.bs"
|
import "pkg:/source/utils/deviceCapabilities.bs"
|
||||||
|
import "pkg:/source/utils/session.bs"
|
||||||
|
|
||||||
enum SubtitleSelection
|
enum SubtitleSelection
|
||||||
notset = -2
|
notset = -2
|
||||||
@ -71,14 +72,24 @@ end function
|
|||||||
sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_stream_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)
|
meta = ItemMetaData(video.id)
|
||||||
subtitle_idx = m.top.selectedSubtitleIndex
|
|
||||||
|
|
||||||
if not isValid(meta)
|
if not isValid(meta)
|
||||||
video.errorMsg = "Error loading metadata"
|
video.errorMsg = "Error loading metadata"
|
||||||
video.content = invalid
|
video.content = invalid
|
||||||
return
|
return
|
||||||
end if
|
end if
|
||||||
|
|
||||||
|
session.video.Update(meta)
|
||||||
|
|
||||||
|
if isValid(meta.json.MediaSources[0].RunTimeTicks)
|
||||||
|
if meta.json.MediaSources[0].RunTimeTicks = 0
|
||||||
|
video.length = 0
|
||||||
|
else
|
||||||
|
video.length = meta.json.MediaSources[0].RunTimeTicks / 10000000
|
||||||
|
end if
|
||||||
|
end if
|
||||||
|
video.MaxVideoDecodeResolution = [meta.json.MediaSources[0].MediaStreams[0].Width, meta.json.MediaSources[0].MediaStreams[0].Height]
|
||||||
|
|
||||||
|
subtitle_idx = m.top.selectedSubtitleIndex
|
||||||
videotype = LCase(meta.type)
|
videotype = LCase(meta.type)
|
||||||
|
|
||||||
' Check for any Live TV streams or Recordings coming from other places other than the TV Guide
|
' Check for any Live TV streams or Recordings coming from other places other than the TV Guide
|
||||||
@ -183,6 +194,11 @@ sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_s
|
|||||||
end if
|
end if
|
||||||
|
|
||||||
video.container = getContainerType(meta)
|
video.container = getContainerType(meta)
|
||||||
|
if video.container = "mp4"
|
||||||
|
video.content.StreamFormat = "mp4"
|
||||||
|
else if video.container = "mkv"
|
||||||
|
video.content.StreamFormat = "mkv"
|
||||||
|
end if
|
||||||
|
|
||||||
if not isValid(m.playbackInfo.MediaSources[0])
|
if not isValid(m.playbackInfo.MediaSources[0])
|
||||||
m.playbackInfo = meta.json
|
m.playbackInfo = meta.json
|
||||||
@ -197,12 +213,10 @@ sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_s
|
|||||||
}
|
}
|
||||||
end if
|
end if
|
||||||
|
|
||||||
|
|
||||||
' 'TODO: allow user selection of subtitle track before playback initiated, for now set to no subtitles
|
' 'TODO: allow user selection of subtitle track before playback initiated, for now set to no subtitles
|
||||||
video.directPlaySupported = m.playbackInfo.MediaSources[0].SupportsDirectPlay
|
video.directPlaySupported = m.playbackInfo.MediaSources[0].SupportsDirectPlay
|
||||||
fully_external = false
|
fully_external = false
|
||||||
|
|
||||||
|
|
||||||
' For h264/hevc video, Roku spec states that it supports specfic encoding levels
|
' For h264/hevc video, Roku spec states that it supports specfic encoding levels
|
||||||
' The device can decode content with a Higher Encoding level but may play it back with certain
|
' The device can decode content with a Higher Encoding level but may play it back with certain
|
||||||
' artifacts. If the user preference is set, and the only reason the server says we need to
|
' artifacts. If the user preference is set, and the only reason the server says we need to
|
||||||
@ -351,11 +365,15 @@ sub addVideoContentURL(video, mediaSourceId, audio_stream_idx, fully_external)
|
|||||||
protocol = LCase(m.playbackInfo.MediaSources[0].Protocol)
|
protocol = LCase(m.playbackInfo.MediaSources[0].Protocol)
|
||||||
if protocol <> "file"
|
if protocol <> "file"
|
||||||
uri = parseUrl(m.playbackInfo.MediaSources[0].Path)
|
uri = parseUrl(m.playbackInfo.MediaSources[0].Path)
|
||||||
if isLocalhost(uri[2])
|
if not isValidAndNotEmpty(uri) then return
|
||||||
|
|
||||||
|
if isValid(uri[2]) and isLocalhost(uri[2])
|
||||||
' if the domain of the URI is local to the server,
|
' if the domain of the URI is local to the server,
|
||||||
' create a new URI by appending the received path to the server URL
|
' create a new URI by appending the received path to the server URL
|
||||||
' later we will substitute the users provided URL for this case
|
' later we will substitute the users provided URL for this case
|
||||||
video.content.url = buildURL(uri[4])
|
if isValid(uri[4])
|
||||||
|
video.content.url = buildURL(uri[4])
|
||||||
|
end if
|
||||||
else
|
else
|
||||||
fully_external = true
|
fully_external = true
|
||||||
video.content.url = m.playbackInfo.MediaSources[0].Path
|
video.content.url = m.playbackInfo.MediaSources[0].Path
|
||||||
|
@ -18,10 +18,8 @@ sub init()
|
|||||||
m.itemPoster.loadDisplayMode = m.topParent.imageDisplayMode
|
m.itemPoster.loadDisplayMode = m.topParent.imageDisplayMode
|
||||||
end if
|
end if
|
||||||
|
|
||||||
m.gridTitles = m.global.session.user.settings["itemgrid.gridTitles"]
|
|
||||||
m.posterText.visible = false
|
m.posterText.visible = false
|
||||||
m.postTextBackground.visible = false
|
m.postTextBackground.visible = false
|
||||||
|
|
||||||
end sub
|
end sub
|
||||||
|
|
||||||
sub itemContentChanged()
|
sub itemContentChanged()
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import "pkg:/source/utils/config.bs"
|
import "pkg:/source/utils/config.bs"
|
||||||
import "pkg:/source/api/baserequest.bs"
|
import "pkg:/source/api/baserequest.bs"
|
||||||
|
import "pkg:/source/roku_modules/log/LogMixin.brs"
|
||||||
|
|
||||||
sub init()
|
sub init()
|
||||||
|
m.log = log.Logger("captionTask")
|
||||||
m.top.observeField("url", "fetchCaption")
|
m.top.observeField("url", "fetchCaption")
|
||||||
m.top.currentCaption = []
|
m.top.currentCaption = []
|
||||||
m.top.currentPos = 0
|
m.top.currentPos = 0
|
||||||
@ -41,17 +43,26 @@ sub setFont()
|
|||||||
end sub
|
end sub
|
||||||
|
|
||||||
sub fetchCaption()
|
sub fetchCaption()
|
||||||
|
m.log.debug("start fetchCaption()")
|
||||||
m.captionTimer.control = "stop"
|
m.captionTimer.control = "stop"
|
||||||
re = CreateObject("roRegex", "(http.*?\.vtt)", "s")
|
re = CreateObject("roRegex", "(http.*?\.vtt)", "s")
|
||||||
url = re.match(m.top.url)[0]
|
url = re.match(m.top.url)[0]
|
||||||
|
|
||||||
if url <> invalid
|
if url <> invalid
|
||||||
|
port = createObject("roMessagePort")
|
||||||
m.reader.setUrl(url)
|
m.reader.setUrl(url)
|
||||||
text = m.reader.GetToString()
|
m.reader.setMessagePort(port)
|
||||||
m.captionList = parseVTT(text)
|
if m.reader.AsyncGetToString()
|
||||||
m.captionTimer.control = "start"
|
msg = port.waitMessage(0)
|
||||||
|
if type(msg) = "roUrlEvent"
|
||||||
|
m.captionList = parseVTT(msg.GetString())
|
||||||
|
m.captionTimer.control = "start"
|
||||||
|
end if
|
||||||
|
end if
|
||||||
else
|
else
|
||||||
m.captionTimer.control = "stop"
|
m.captionTimer.control = "stop"
|
||||||
end if
|
end if
|
||||||
|
m.log.debug("end fetchCaption()", url)
|
||||||
end sub
|
end sub
|
||||||
|
|
||||||
function newlabel(txt)
|
function newlabel(txt)
|
||||||
|
@ -4,9 +4,12 @@ import "pkg:/source/utils/config.bs"
|
|||||||
|
|
||||||
sub setFields()
|
sub setFields()
|
||||||
json = m.top.json
|
json = m.top.json
|
||||||
|
m.top.Type = "Person"
|
||||||
|
|
||||||
|
if json = invalid then return
|
||||||
|
|
||||||
m.top.id = json.id
|
m.top.id = json.id
|
||||||
m.top.favorite = json.UserData.isFavorite
|
m.top.favorite = json.UserData.isFavorite
|
||||||
m.top.Type = "Person"
|
|
||||||
setPoster()
|
setPoster()
|
||||||
end sub
|
end sub
|
||||||
|
|
||||||
|
@ -10,12 +10,12 @@ sub init()
|
|||||||
initItemPoster()
|
initItemPoster()
|
||||||
m.itemProgress = m.top.findNode("progress")
|
m.itemProgress = m.top.findNode("progress")
|
||||||
m.itemProgressBackground = m.top.findNode("progressBackground")
|
m.itemProgressBackground = m.top.findNode("progressBackground")
|
||||||
m.itemIcon = m.top.findNode("itemIcon")
|
initItemIcon()
|
||||||
initItemTextExtra()
|
initItemTextExtra()
|
||||||
m.itemPoster.observeField("loadStatus", "onPosterLoadStatusChanged")
|
m.itemPoster.observeField("loadStatus", "onPosterLoadStatusChanged")
|
||||||
m.unplayedCount = m.top.findNode("unplayedCount")
|
m.unplayedCount = m.top.findNode("unplayedCount")
|
||||||
m.unplayedEpisodeCount = m.top.findNode("unplayedEpisodeCount")
|
m.unplayedEpisodeCount = m.top.findNode("unplayedEpisodeCount")
|
||||||
m.playedIndicator = m.top.findNode("playedIndicator")
|
initPlayedIndicator()
|
||||||
|
|
||||||
m.showProgressBarAnimation = m.top.findNode("showProgressBar")
|
m.showProgressBarAnimation = m.top.findNode("showProgressBar")
|
||||||
m.showProgressBarField = m.top.findNode("showProgressBarField")
|
m.showProgressBarField = m.top.findNode("showProgressBarField")
|
||||||
@ -50,6 +50,14 @@ sub initBackdrop()
|
|||||||
m.backdrop = m.top.findNode("backdrop")
|
m.backdrop = m.top.findNode("backdrop")
|
||||||
end sub
|
end sub
|
||||||
|
|
||||||
|
sub initItemIcon()
|
||||||
|
m.itemIcon = m.top.findNode("itemIcon")
|
||||||
|
end sub
|
||||||
|
|
||||||
|
sub initPlayedIndicator()
|
||||||
|
m.playedIndicator = m.top.findNode("playedIndicator")
|
||||||
|
end sub
|
||||||
|
|
||||||
sub itemContentChanged()
|
sub itemContentChanged()
|
||||||
if isValid(m.unplayedCount) then m.unplayedCount.visible = false
|
if isValid(m.unplayedCount) then m.unplayedCount.visible = false
|
||||||
itemData = m.top.itemContent
|
itemData = m.top.itemContent
|
||||||
@ -63,6 +71,8 @@ sub itemContentChanged()
|
|||||||
if not isValid(m.itemText) then initItemText()
|
if not isValid(m.itemText) then initItemText()
|
||||||
if not isValid(m.itemTextExtra) then initItemTextExtra()
|
if not isValid(m.itemTextExtra) then initItemTextExtra()
|
||||||
if not isValid(m.backdrop) then initBackdrop()
|
if not isValid(m.backdrop) then initBackdrop()
|
||||||
|
if not isValid(m.itemIcon) then initItemIcon()
|
||||||
|
if not isValid(m.playedIndicator) then initPlayedIndicator()
|
||||||
|
|
||||||
m.itemPoster.width = itemData.imageWidth
|
m.itemPoster.width = itemData.imageWidth
|
||||||
m.itemText.maxWidth = itemData.imageWidth
|
m.itemText.maxWidth = itemData.imageWidth
|
||||||
@ -83,11 +93,14 @@ sub itemContentChanged()
|
|||||||
|
|
||||||
if LCase(itemData.type) = "series"
|
if LCase(itemData.type) = "series"
|
||||||
if isValid(localGlobal) and isValid(localGlobal.session) and isValid(localGlobal.session.user) and isValid(localGlobal.session.user.settings)
|
if isValid(localGlobal) and isValid(localGlobal.session) and isValid(localGlobal.session.user) and isValid(localGlobal.session.user.settings)
|
||||||
if not localGlobal.session.user.settings["ui.tvshows.disableUnwatchedEpisodeCount"]
|
unwatchedEpisodeCountSetting = localGlobal.session.user.settings["ui.tvshows.disableUnwatchedEpisodeCount"]
|
||||||
|
if isValid(unwatchedEpisodeCountSetting) and not unwatchedEpisodeCountSetting
|
||||||
if isValid(itemData.json.UserData) and isValid(itemData.json.UserData.UnplayedItemCount)
|
if isValid(itemData.json.UserData) and isValid(itemData.json.UserData.UnplayedItemCount)
|
||||||
if itemData.json.UserData.UnplayedItemCount > 0
|
if itemData.json.UserData.UnplayedItemCount > 0
|
||||||
if isValid(m.unplayedCount) then m.unplayedCount.visible = true
|
if isValid(m.unplayedCount) then m.unplayedCount.visible = true
|
||||||
m.unplayedEpisodeCount.text = itemData.json.UserData.UnplayedItemCount
|
if isValid(m.unplayedEpisodeCount)
|
||||||
|
m.unplayedEpisodeCount.text = itemData.json.UserData.UnplayedItemCount
|
||||||
|
end if
|
||||||
end if
|
end if
|
||||||
end if
|
end if
|
||||||
end if
|
end if
|
||||||
|
@ -181,8 +181,10 @@ end function
|
|||||||
|
|
||||||
sub onShuffleEpisodeDataLoaded()
|
sub onShuffleEpisodeDataLoaded()
|
||||||
m.getShuffleEpisodesTask.unobserveField("data")
|
m.getShuffleEpisodesTask.unobserveField("data")
|
||||||
m.global.queueManager.callFunc("set", m.getShuffleEpisodesTask.data.items)
|
if isValid(m.getShuffleEpisodesTask.data)
|
||||||
m.global.queueManager.callFunc("playQueue")
|
m.global.queueManager.callFunc("set", m.getShuffleEpisodesTask.data.items)
|
||||||
|
m.global.queueManager.callFunc("playQueue")
|
||||||
|
end if
|
||||||
end sub
|
end sub
|
||||||
|
|
||||||
function onKeyEvent(key as string, press as boolean) as boolean
|
function onKeyEvent(key as string, press as boolean) as boolean
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import "pkg:/source/utils/misc.bs"
|
import "pkg:/source/utils/misc.bs"
|
||||||
import "pkg:/source/utils/config.bs"
|
import "pkg:/source/utils/config.bs"
|
||||||
|
import "pkg:/source/utils/session.bs"
|
||||||
import "pkg:/source/roku_modules/log/LogMixin.brs"
|
import "pkg:/source/roku_modules/log/LogMixin.brs"
|
||||||
|
|
||||||
sub init()
|
sub init()
|
||||||
@ -102,6 +103,7 @@ sub handleItemSkipAction(action as string)
|
|||||||
' If there is something next in the queue, play it
|
' If there is something next in the queue, play it
|
||||||
if m.global.queueManager.callFunc("getPosition") < m.global.queueManager.callFunc("getCount") - 1
|
if m.global.queueManager.callFunc("getPosition") < m.global.queueManager.callFunc("getCount") - 1
|
||||||
m.top.control = "stop"
|
m.top.control = "stop"
|
||||||
|
session.video.Delete()
|
||||||
m.global.sceneManager.callFunc("clearPreviousScene")
|
m.global.sceneManager.callFunc("clearPreviousScene")
|
||||||
m.global.queueManager.callFunc("moveForward")
|
m.global.queueManager.callFunc("moveForward")
|
||||||
m.global.queueManager.callFunc("playQueue")
|
m.global.queueManager.callFunc("playQueue")
|
||||||
@ -114,6 +116,7 @@ sub handleItemSkipAction(action as string)
|
|||||||
' If there is something previous in the queue, play it
|
' If there is something previous in the queue, play it
|
||||||
if m.global.queueManager.callFunc("getPosition") > 0
|
if m.global.queueManager.callFunc("getPosition") > 0
|
||||||
m.top.control = "stop"
|
m.top.control = "stop"
|
||||||
|
session.video.Delete()
|
||||||
m.global.sceneManager.callFunc("clearPreviousScene")
|
m.global.sceneManager.callFunc("clearPreviousScene")
|
||||||
m.global.queueManager.callFunc("moveBack")
|
m.global.queueManager.callFunc("moveBack")
|
||||||
m.global.queueManager.callFunc("playQueue")
|
m.global.queueManager.callFunc("playQueue")
|
||||||
@ -612,19 +615,12 @@ sub onState(msg)
|
|||||||
' Pass video state into OSD
|
' Pass video state into OSD
|
||||||
m.osd.playbackState = m.top.state
|
m.osd.playbackState = m.top.state
|
||||||
|
|
||||||
' When buffering, start timer to monitor buffering process
|
|
||||||
if m.top.state = "buffering"
|
if m.top.state = "buffering"
|
||||||
' start buffer timer
|
' When buffering, start timer to monitor buffering process
|
||||||
if isValid(m.bufferCheckTimer)
|
if isValid(m.bufferCheckTimer)
|
||||||
m.bufferCheckTimer.control = "start"
|
m.bufferCheckTimer.control = "start"
|
||||||
m.bufferCheckTimer.ObserveField("fire", "bufferCheck")
|
m.bufferCheckTimer.ObserveField("fire", "bufferCheck")
|
||||||
end if
|
end if
|
||||||
|
|
||||||
' update server if needed
|
|
||||||
if not m.playReported
|
|
||||||
m.playReported = true
|
|
||||||
ReportPlayback("start")
|
|
||||||
end if
|
|
||||||
else if m.top.state = "error"
|
else if m.top.state = "error"
|
||||||
m.log.error(m.top.errorCode, m.top.errorMsg, m.top.errorStr, m.top.errorCode)
|
m.log.error(m.top.errorCode, m.top.errorMsg, m.top.errorStr, m.top.errorCode)
|
||||||
|
|
||||||
@ -635,10 +631,10 @@ sub onState(msg)
|
|||||||
else
|
else
|
||||||
' If an error was encountered, Display dialog
|
' If an error was encountered, Display dialog
|
||||||
showPlaybackErrorDialog(tr("Error During Playback"))
|
showPlaybackErrorDialog(tr("Error During Playback"))
|
||||||
|
session.video.Delete()
|
||||||
end if
|
end if
|
||||||
|
|
||||||
' Stop playback and exit player
|
|
||||||
m.top.control = "stop"
|
|
||||||
else if m.top.state = "playing"
|
else if m.top.state = "playing"
|
||||||
|
|
||||||
' Check if next episode is available
|
' Check if next episode is available
|
||||||
@ -664,9 +660,11 @@ sub onState(msg)
|
|||||||
m.playbackTimer.control = "stop"
|
m.playbackTimer.control = "stop"
|
||||||
ReportPlayback("stop")
|
ReportPlayback("stop")
|
||||||
m.playReported = false
|
m.playReported = false
|
||||||
|
session.video.Delete()
|
||||||
else if m.top.state = "finished"
|
else if m.top.state = "finished"
|
||||||
m.playbackTimer.control = "stop"
|
m.playbackTimer.control = "stop"
|
||||||
ReportPlayback("finished")
|
ReportPlayback("finished")
|
||||||
|
session.video.Delete()
|
||||||
else
|
else
|
||||||
m.log.warning("Unhandled state", m.top.state, m.playReported, m.playFinished)
|
m.log.warning("Unhandled state", m.top.state, m.playReported, m.playFinished)
|
||||||
end if
|
end if
|
||||||
@ -725,6 +723,7 @@ sub bufferCheck(msg)
|
|||||||
|
|
||||||
' Stop playback and exit player
|
' Stop playback and exit player
|
||||||
m.top.control = "stop"
|
m.top.control = "stop"
|
||||||
|
session.video.Delete()
|
||||||
end if
|
end if
|
||||||
end if
|
end if
|
||||||
|
|
||||||
@ -794,6 +793,7 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
|||||||
if key = "OK" and m.nextEpisodeButton.hasfocus() and not m.top.trickPlayBar.visible
|
if key = "OK" and m.nextEpisodeButton.hasfocus() and not m.top.trickPlayBar.visible
|
||||||
m.top.control = "stop"
|
m.top.control = "stop"
|
||||||
m.top.state = "finished"
|
m.top.state = "finished"
|
||||||
|
session.video.Delete()
|
||||||
hideNextEpisodeButton()
|
hideNextEpisodeButton()
|
||||||
return true
|
return true
|
||||||
else
|
else
|
||||||
@ -868,6 +868,7 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
|||||||
|
|
||||||
if key = "back"
|
if key = "back"
|
||||||
m.top.control = "stop"
|
m.top.control = "stop"
|
||||||
|
session.video.Delete()
|
||||||
end if
|
end if
|
||||||
|
|
||||||
return false
|
return false
|
||||||
|
2
manifest
2
manifest
@ -3,7 +3,7 @@
|
|||||||
title=Jellyfin
|
title=Jellyfin
|
||||||
major_version=2
|
major_version=2
|
||||||
minor_version=1
|
minor_version=1
|
||||||
build_version=5
|
build_version=6
|
||||||
|
|
||||||
### Main Menu Icons / Channel Poster Artwork
|
### Main Menu Icons / Channel Poster Artwork
|
||||||
|
|
||||||
|
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "jellyfin-roku",
|
"name": "jellyfin-roku",
|
||||||
"version": "2.1.5",
|
"version": "2.1.6",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "jellyfin-roku",
|
"name": "jellyfin-roku",
|
||||||
"version": "2.1.5",
|
"version": "2.1.6",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "GPL-2.0",
|
"license": "GPL-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "jellyfin-roku",
|
"name": "jellyfin-roku",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "2.1.5",
|
"version": "2.1.6",
|
||||||
"description": "Roku app for Jellyfin media server",
|
"description": "Roku app for Jellyfin media server",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rokucommunity/bslib": "0.1.1",
|
"@rokucommunity/bslib": "0.1.1",
|
||||||
|
@ -224,8 +224,12 @@ sub Main (args as dynamic) as void
|
|||||||
' Find the object in the scene's data and update its json data
|
' Find the object in the scene's data and update its json data
|
||||||
for i = 0 to currentScene.objects.Items.count() - 1
|
for i = 0 to currentScene.objects.Items.count() - 1
|
||||||
if LCase(currentScene.objects.Items[i].id) = LCase(currentEpisode.id)
|
if LCase(currentScene.objects.Items[i].id) = LCase(currentEpisode.id)
|
||||||
currentScene.objects.Items[i].json = api.users.GetItem(m.global.session.user.id, currentEpisode.id)
|
|
||||||
m.global.queueManager.callFunc("setTopStartingPoint", currentScene.objects.Items[i].json.UserData.PlaybackPositionTicks)
|
data = api.users.GetItem(m.global.session.user.id, currentEpisode.id)
|
||||||
|
if isValid(data)
|
||||||
|
currentScene.objects.Items[i].json = data
|
||||||
|
m.global.queueManager.callFunc("setTopStartingPoint", data.UserData.PlaybackPositionTicks)
|
||||||
|
end if
|
||||||
exit for
|
exit for
|
||||||
end if
|
end if
|
||||||
end for
|
end for
|
||||||
@ -244,15 +248,19 @@ sub Main (args as dynamic) as void
|
|||||||
currentScene = m.global.sceneManager.callFunc("getActiveScene")
|
currentScene = m.global.sceneManager.callFunc("getActiveScene")
|
||||||
|
|
||||||
if isValid(currentScene) and isValid(currentScene.itemContent) and isValid(currentScene.itemContent.id)
|
if isValid(currentScene) and isValid(currentScene.itemContent) and isValid(currentScene.itemContent.id)
|
||||||
' Refresh movie detail data
|
data = api.users.GetItem(m.global.session.user.id, currentScene.itemContent.id)
|
||||||
currentScene.itemContent.json = api.users.GetItem(m.global.session.user.id, currentScene.itemContent.id)
|
if isValid(data)
|
||||||
movieMetaData = ItemMetaData(currentScene.itemContent.id)
|
currentScene.itemContent.json = data
|
||||||
|
' Set updated starting point for the queue item
|
||||||
|
m.global.queueManager.callFunc("setTopStartingPoint", data.UserData.PlaybackPositionTicks)
|
||||||
|
|
||||||
' Redraw movie poster
|
' Refresh movie detail data
|
||||||
currentScene.newPosterImageURI = movieMetaData.posterURL
|
movieMetaData = ItemMetaData(currentScene.itemContent.id)
|
||||||
|
if isValid(movieMetaData)
|
||||||
' Set updated starting point for the queue item
|
' Redraw movie poster
|
||||||
m.global.queueManager.callFunc("setTopStartingPoint", currentScene.itemContent.json.UserData.PlaybackPositionTicks)
|
currentScene.newPosterImageURI = movieMetaData.posterURL
|
||||||
|
end if
|
||||||
|
end if
|
||||||
end if
|
end if
|
||||||
|
|
||||||
stopLoadingSpinner()
|
stopLoadingSpinner()
|
||||||
@ -577,36 +585,41 @@ sub Main (args as dynamic) as void
|
|||||||
' If a button is selected, we have some determining to do
|
' If a button is selected, we have some determining to do
|
||||||
btn = getButton(msg)
|
btn = getButton(msg)
|
||||||
group = sceneManager.callFunc("getActiveScene")
|
group = sceneManager.callFunc("getActiveScene")
|
||||||
|
|
||||||
if isValid(btn) and btn.id = "play-button"
|
if isValid(btn) and btn.id = "play-button"
|
||||||
|
if not isValid(group) then return
|
||||||
|
|
||||||
' User chose Play button from movie detail view
|
' User chose Play button from movie detail view
|
||||||
startLoadingSpinner()
|
startLoadingSpinner()
|
||||||
' Check if a specific Audio Stream was selected
|
' Check if a specific Audio Stream was selected
|
||||||
audio_stream_idx = 0
|
audio_stream_idx = 0
|
||||||
if isValid(group) and isValid(group.selectedAudioStreamIndex)
|
if isValid(group.selectedAudioStreamIndex)
|
||||||
audio_stream_idx = group.selectedAudioStreamIndex
|
audio_stream_idx = group.selectedAudioStreamIndex
|
||||||
end if
|
end if
|
||||||
|
|
||||||
group.itemContent.selectedAudioStreamIndex = audio_stream_idx
|
if isValid(group.itemContent)
|
||||||
group.itemContent.id = group.selectedVideoStreamId
|
group.itemContent.selectedAudioStreamIndex = audio_stream_idx
|
||||||
|
group.itemContent.id = group.selectedVideoStreamId
|
||||||
|
|
||||||
' Display playback options dialog
|
' Display playback options dialog
|
||||||
if group.itemContent.json.userdata.PlaybackPositionTicks > 0
|
if group.itemContent.json.userdata.PlaybackPositionTicks > 0
|
||||||
m.global.queueManager.callFunc("hold", group.itemContent)
|
m.global.queueManager.callFunc("hold", group.itemContent)
|
||||||
playbackOptionDialog(group.itemContent.json.userdata.PlaybackPositionTicks, group.itemContent.json)
|
playbackOptionDialog(group.itemContent.json.userdata.PlaybackPositionTicks, group.itemContent.json)
|
||||||
else
|
else
|
||||||
m.global.queueManager.callFunc("clear")
|
m.global.queueManager.callFunc("clear")
|
||||||
m.global.queueManager.callFunc("push", group.itemContent)
|
m.global.queueManager.callFunc("push", group.itemContent)
|
||||||
m.global.queueManager.callFunc("playQueue")
|
m.global.queueManager.callFunc("playQueue")
|
||||||
|
end if
|
||||||
end if
|
end if
|
||||||
|
|
||||||
if isValid(group) and isValid(group.lastFocus) and isValid(group.lastFocus.id) and group.lastFocus.id = "main_group"
|
if isValid(group.lastFocus) and isValid(group.lastFocus.id) and group.lastFocus.id = "main_group"
|
||||||
buttons = group.findNode("buttons")
|
buttons = group.findNode("buttons")
|
||||||
if isValid(buttons)
|
if isValid(buttons)
|
||||||
group.lastFocus = group.findNode("buttons")
|
group.lastFocus = group.findNode("buttons")
|
||||||
end if
|
end if
|
||||||
end if
|
end if
|
||||||
|
|
||||||
if isValid(group) and isValid(group.lastFocus)
|
if isValid(group.lastFocus)
|
||||||
group.lastFocus.setFocus(true)
|
group.lastFocus.setFocus(true)
|
||||||
end if
|
end if
|
||||||
|
|
||||||
|
@ -263,16 +263,20 @@ sub AddVideoContent(video as object, mediaSourceId as dynamic, audio_stream_idx
|
|||||||
protocol = LCase(m.playbackInfo.MediaSources[0].Protocol)
|
protocol = LCase(m.playbackInfo.MediaSources[0].Protocol)
|
||||||
if protocol <> "file"
|
if protocol <> "file"
|
||||||
uri = parseUrl(m.playbackInfo.MediaSources[0].Path)
|
uri = parseUrl(m.playbackInfo.MediaSources[0].Path)
|
||||||
if isLocalhost(uri[2])
|
if not isValidAndNotEmpty(uri) then return
|
||||||
|
|
||||||
|
if isValid(uri[2]) and isLocalhost(uri[2])
|
||||||
' the domain of the URI is local to the server.
|
' the domain of the URI is local to the server.
|
||||||
' create a new URI by appending the received path to the server URL
|
' create a new URI by appending the received path to the server URL
|
||||||
' later we will substitute the users provided URL for this case
|
' later we will substitute the users provided URL for this case
|
||||||
video.content.url = buildURL(uri[4])
|
if isValid(uri[4])
|
||||||
|
video.content.url = buildURL(uri[4])
|
||||||
|
end if
|
||||||
else
|
else
|
||||||
fully_external = true
|
fully_external = true
|
||||||
video.content.url = m.playbackInfo.MediaSources[0].Path
|
video.content.url = m.playbackInfo.MediaSources[0].Path
|
||||||
end if
|
end if
|
||||||
else:
|
else
|
||||||
params.append({
|
params.append({
|
||||||
"Static": "true",
|
"Static": "true",
|
||||||
"Container": video.container,
|
"Container": video.container,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import "pkg:/source/api/sdk.bs"
|
import "pkg:/source/api/sdk.bs"
|
||||||
|
import "pkg:/source/utils/misc.bs"
|
||||||
|
|
||||||
function ItemGetPlaybackInfo(id as string, startTimeTicks = 0 as longinteger)
|
function ItemGetPlaybackInfo(id as string, startTimeTicks = 0 as longinteger)
|
||||||
params = {
|
params = {
|
||||||
@ -13,9 +14,6 @@ function ItemGetPlaybackInfo(id as string, startTimeTicks = 0 as longinteger)
|
|||||||
end function
|
end function
|
||||||
|
|
||||||
function ItemPostPlaybackInfo(id as string, mediaSourceId = "" as string, audioTrackIndex = -1 as integer, subtitleTrackIndex = -1 as integer, startTimeTicks = 0 as longinteger)
|
function ItemPostPlaybackInfo(id as string, mediaSourceId = "" as string, audioTrackIndex = -1 as integer, subtitleTrackIndex = -1 as integer, startTimeTicks = 0 as longinteger)
|
||||||
body = {
|
|
||||||
"DeviceProfile": getDeviceProfile()
|
|
||||||
}
|
|
||||||
params = {
|
params = {
|
||||||
"UserId": m.global.session.user.id,
|
"UserId": m.global.session.user.id,
|
||||||
"StartTimeTicks": startTimeTicks,
|
"StartTimeTicks": startTimeTicks,
|
||||||
@ -25,6 +23,7 @@ function ItemPostPlaybackInfo(id as string, mediaSourceId = "" as string, audioT
|
|||||||
"MaxStaticBitrate": "140000000",
|
"MaxStaticBitrate": "140000000",
|
||||||
"SubtitleStreamIndex": subtitleTrackIndex
|
"SubtitleStreamIndex": subtitleTrackIndex
|
||||||
}
|
}
|
||||||
|
deviceProfile = getDeviceProfile()
|
||||||
|
|
||||||
' Note: Jellyfin v10.9+ now remuxs LiveTV and does not allow DirectPlay anymore.
|
' Note: Jellyfin v10.9+ now remuxs LiveTV and does not allow DirectPlay anymore.
|
||||||
' Because of this, we need to tell the server "EnableDirectPlay = false" so that we receive the
|
' Because of this, we need to tell the server "EnableDirectPlay = false" so that we receive the
|
||||||
@ -38,11 +37,38 @@ function ItemPostPlaybackInfo(id as string, mediaSourceId = "" as string, audioT
|
|||||||
params.EnableDirectPlay = false
|
params.EnableDirectPlay = false
|
||||||
end if
|
end if
|
||||||
|
|
||||||
if audioTrackIndex > -1 then params.AudioStreamIndex = audioTrackIndex
|
if audioTrackIndex > -1
|
||||||
|
selectedAudioStream = m.global.session.video.json.MediaStreams[audioTrackIndex]
|
||||||
|
|
||||||
|
if selectedAudioStream <> invalid
|
||||||
|
params.AudioStreamIndex = audioTrackIndex
|
||||||
|
|
||||||
|
' force the server to transcode AAC profiles we don't support to MP3 instead of the usual AAC
|
||||||
|
' TODO: Remove this after server adds support for transcoding AAC from one profile to another
|
||||||
|
if LCase(selectedAudioStream.Codec) = "aac"
|
||||||
|
if LCase(selectedAudioStream.Profile) = "main" or LCase(selectedAudioStream.Profile) = "he-aac"
|
||||||
|
for each rule in deviceProfile.TranscodingProfiles
|
||||||
|
if rule.Container = "ts" or rule.Container = "mp4"
|
||||||
|
if rule.AudioCodec = "aac"
|
||||||
|
rule.AudioCodec = "mp3"
|
||||||
|
else if rule.AudioCodec.Left(4) = "aac,"
|
||||||
|
rule.AudioCodec = mid(rule.AudioCodec, 5)
|
||||||
|
|
||||||
|
if rule.AudioCodec.Left(3) <> "mp3"
|
||||||
|
rule.AudioCodec = "mp3," + rule.AudioCodec
|
||||||
|
end if
|
||||||
|
end if
|
||||||
|
end if
|
||||||
|
end for
|
||||||
|
end if
|
||||||
|
end if
|
||||||
|
|
||||||
|
end if
|
||||||
|
end if
|
||||||
|
|
||||||
req = APIRequest(Substitute("Items/{0}/PlaybackInfo", id), params)
|
req = APIRequest(Substitute("Items/{0}/PlaybackInfo", id), params)
|
||||||
req.SetRequest("POST")
|
req.SetRequest("POST")
|
||||||
return postJson(req, FormatJson(body))
|
return postJson(req, FormatJson({ "DeviceProfile": deviceProfile }))
|
||||||
end function
|
end function
|
||||||
|
|
||||||
' Search across all libraries
|
' Search across all libraries
|
||||||
|
@ -451,6 +451,12 @@ function getCodecProfiles() as object
|
|||||||
"Value": "Main",
|
"Value": "Main",
|
||||||
"IsRequired": true
|
"IsRequired": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"Condition": "NotEquals",
|
||||||
|
"Property": "AudioProfile",
|
||||||
|
"Value": "HE-AAC",
|
||||||
|
"IsRequired": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"Condition": "LessThanEqual",
|
"Condition": "LessThanEqual",
|
||||||
"Property": "AudioChannels",
|
"Property": "AudioChannels",
|
||||||
|
@ -16,6 +16,9 @@ namespace session
|
|||||||
Policy: {},
|
Policy: {},
|
||||||
settings: {},
|
settings: {},
|
||||||
lastRunVersion: invalid
|
lastRunVersion: invalid
|
||||||
|
},
|
||||||
|
video: {
|
||||||
|
json: {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -31,7 +34,7 @@ namespace session
|
|||||||
' Update one value from the global session array (m.global.session)
|
' Update one value from the global session array (m.global.session)
|
||||||
sub Update(key as string, value = {} as object)
|
sub Update(key as string, value = {} as object)
|
||||||
' validate parameters
|
' validate parameters
|
||||||
if key = "" or (key <> "user" and key <> "server") or value = invalid
|
if key = "" or (key <> "user" and key <> "server" and key <> "video") or value = invalid
|
||||||
print "Error in session.Update(): Invalid parameters provided"
|
print "Error in session.Update(): Invalid parameters provided"
|
||||||
return
|
return
|
||||||
end if
|
end if
|
||||||
@ -430,4 +433,19 @@ namespace session
|
|||||||
end sub
|
end sub
|
||||||
end namespace
|
end namespace
|
||||||
end namespace
|
end namespace
|
||||||
|
|
||||||
|
namespace video
|
||||||
|
' Return the global video session array to it's default state
|
||||||
|
sub Delete()
|
||||||
|
session.Update("video", { json: {} })
|
||||||
|
end sub
|
||||||
|
|
||||||
|
' Update the global video session array (m.global.session.video)
|
||||||
|
sub Update(videoMetaData as object)
|
||||||
|
if videoMetaData = invalid then return
|
||||||
|
|
||||||
|
session.video.Delete()
|
||||||
|
session.Update("video", videoMetaData)
|
||||||
|
end sub
|
||||||
|
end namespace
|
||||||
end namespace
|
end namespace
|
||||||
|
Loading…
Reference in New Issue
Block a user