Playlist support & TV Shuffle (#986)

* Add TV Episode Shuffle
* Reuse playback info
* Get Subtitle Popup working
* Get Subtitle Popup working
* Get Resume/Restart popup working
* Playlist poster, bug fixes
* Remove commented out code
* Start from beginning if playing queue
* Fix Playback Info issue
* Remove optional chaining to fix formatter
* Fix playlist content list. Code cleanup.
* Remove commented out code
This commit is contained in:
1hitsong 2023-02-25 11:43:36 -05:00 committed by GitHub
parent 948a9b550e
commit a33ce8bd57
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 2583 additions and 21 deletions

View File

@ -0,0 +1,161 @@
sub init()
m.top.functionName = "getPlaybackInfoTask"
end sub
function ItemPostPlaybackInfo(id as string, mediaSourceId = "" as string, audioTrackIndex = -1 as integer, subtitleTrackIndex = -1 as integer, startTimeTicks = 0 as longinteger)
body = {
"DeviceProfile": getDeviceProfile()
}
params = {
"UserId": get_setting("active_user"),
"StartTimeTicks": startTimeTicks,
"IsPlayback": true,
"AutoOpenLiveStream": true,
"MaxStreamingBitrate": "140000000",
"MaxStaticBitrate": "140000000",
"SubtitleStreamIndex": subtitleTrackIndex
}
mediaSourceId = id
if mediaSourceId <> "" then params.MediaSourceId = mediaSourceId
if audioTrackIndex > -1 then params.AudioStreamIndex = audioTrackIndex
req = APIRequest(Substitute("Items/{0}/PlaybackInfo", id), params)
req.SetRequest("POST")
return postJson(req, FormatJson(body))
end function
' Returns an array of playback info to be displayed during playback.
' In the future, with a custom playback info view, we can return an associated array.
sub getPlaybackInfoTask()
sessions = api_API().sessions.get()
m.playbackInfo = ItemPostPlaybackInfo(m.top.videoID)
if isValid(sessions) and sessions.Count() > 0
m.top.data = { playbackInfo: GetTranscodingStats(sessions[0]) }
else
m.top.data = { playbackInfo: [tr("Unable to get playback information")] }
end if
end sub
function GetTranscodingStats(session)
sessionStats = { data: [] }
if isValid(session.TranscodingInfo) and session.TranscodingInfo.Count() > 0
transcodingReasons = session.TranscodingInfo.TranscodeReasons
videoCodec = session.TranscodingInfo.VideoCodec
audioCodec = session.TranscodingInfo.AudioCodec
totalBitrate = session.TranscodingInfo.Bitrate
audioChannels = session.TranscodingInfo.AudioChannels
if isValid(transcodingReasons) and transcodingReasons.Count() > 0
sessionStats.data.push("<header>" + tr("Transcoding Information") + "</header>")
for each item in transcodingReasons
sessionStats.data.push("<b>• " + tr("Reason") + ":</b> " + item)
end for
end if
if isValid(videoCodec)
data = "<b>• " + tr("Video Codec") + ":</b> " + videoCodec
if session.TranscodingInfo.IsVideoDirect
data = data + " (" + tr("direct") + ")"
end if
sessionStats.data.push(data)
end if
if isValid(audioCodec)
data = "<b>• " + tr("Audio Codec") + ":</b> " + audioCodec
if session.TranscodingInfo.IsAudioDirect
data = data + " (" + tr("direct") + ")"
end if
sessionStats.data.push(data)
end if
if isValid(totalBitrate)
data = "<b>• " + tr("Total Bitrate") + ":</b> " + getDisplayBitrate(totalBitrate)
sessionStats.data.push(data)
end if
if isValid(audioChannels)
data = "<b>• " + tr("Audio Channels") + ":</b> " + Str(audioChannels)
sessionStats.data.push(data)
end if
end if
if havePlaybackInfo()
stream = m.playbackInfo.mediaSources[0].MediaStreams[0]
sessionStats.data.push("<header>" + tr("Stream Information") + "</header>")
if isValid(stream.Container)
data = "<b>• " + tr("Container") + ":</b> " + stream.Container
sessionStats.data.push(data)
end if
if isValid(stream.Size)
data = "<b>• " + tr("Size") + ":</b> " + stream.Size
sessionStats.data.push(data)
end if
if isValid(stream.BitRate)
data = "<b>• " + tr("Bit Rate") + ":</b> " + getDisplayBitrate(stream.BitRate)
sessionStats.data.push(data)
end if
if isValid(stream.Codec)
data = "<b>• " + tr("Codec") + ":</b> " + stream.Codec
sessionStats.data.push(data)
end if
if isValid(stream.CodecTag)
data = "<b>• " + tr("Codec Tag") + ":</b> " + stream.CodecTag
sessionStats.data.push(data)
end if
if isValid(stream.VideoRangeType)
data = "<b>• " + tr("Video range type") + ":</b> " + stream.VideoRangeType
sessionStats.data.push(data)
end if
if isValid(stream.PixelFormat)
data = "<b>• " + tr("Pixel format") + ":</b> " + stream.PixelFormat
sessionStats.data.push(data)
end if
if isValid(stream.Width) and isValid(stream.Height)
data = "<b>• " + tr("WxH") + ":</b> " + Str(stream.Width) + " x " + Str(stream.Height)
sessionStats.data.push(data)
end if
if isValid(stream.Level)
data = "<b>• " + tr("Level") + ":</b> " + Str(stream.Level)
sessionStats.data.push(data)
end if
end if
return sessionStats
end function
function havePlaybackInfo()
if not isValid(m.playbackInfo)
return false
end if
if not isValid(m.playbackInfo.mediaSources)
return false
end if
if m.playbackInfo.mediaSources.Count() <= 0
return false
end if
if not isValid(m.playbackInfo.mediaSources[0].MediaStreams)
return false
end if
if m.playbackInfo.mediaSources[0].MediaStreams.Count() <= 0
return false
end if
return true
end function
function getDisplayBitrate(bitrate)
if bitrate > 1000000
return Str(Fix(bitrate / 1000000)) + " Mbps"
else
return Str(Fix(bitrate / 1000)) + " Kbps"
end if
end function

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8" ?>
<component name="GetPlaybackInfoTask" extends="Task">
<interface>
<field id="videoID" type="string" />
<field id="data" type="assocarray" />
</interface>
<script type="text/brightscript" uri="GetPlaybackInfoTask.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/deviceCapabilities.brs" />
<script type="text/brightscript" uri="pkg:/source/api/baserequest.brs" />
<script type="text/brightscript" uri="pkg:/source/roku_modules/api/api.brs" />
</component>

View File

@ -0,0 +1,13 @@
sub init()
m.top.functionName = "getShuffleEpisodesTask"
end sub
sub getShuffleEpisodesTask()
data = api_API().shows.getepisodes(m.top.showID, {
UserId: get_setting("active_user"),
SortBy: "Random",
Limit: 200
})
m.top.data = data
end sub

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8" ?>
<component name="GetShuffleEpisodesTask" extends="Task">
<interface>
<field id="showID" type="string" />
<field id="data" type="assocarray" />
</interface>
<script type="text/brightscript" uri="GetShuffleEpisodesTask.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
<script type="text/brightscript" uri="pkg:/source/roku_modules/api/api.brs" />
</component>

View File

@ -72,6 +72,10 @@ sub itemContentChanged()
m.itemPoster.uri = itemData.PosterUrl
m.itemIcon.uri = itemData.iconUrl
m.itemText.text = itemData.Title
else if itemData.type = "Playlist"
m.itemPoster.uri = itemData.PosterUrl
m.itemIcon.uri = itemData.iconUrl
m.itemText.text = itemData.Title
else if itemData.type = "Photo"
m.itemPoster.uri = itemData.PosterUrl
m.itemIcon.uri = itemData.iconUrl

View File

@ -135,6 +135,10 @@ sub loadItems()
tmp = CreateObject("roSGNode", "PhotoData")
else if item.type = "PhotoAlbum"
tmp = CreateObject("roSGNode", "FolderData")
else if item.type = "Playlist"
tmp = CreateObject("roSGNode", "PlaylistData")
tmp.type = "Playlist"
tmp.image = PosterImage(item.id, { "maxHeight": 425, "maxWidth": 290, "quality": "90" })
else if item.type = "Episode"
tmp = CreateObject("roSGNode", "TVEpisode")
else if item.Type = "Genre"
@ -207,6 +211,8 @@ sub loadItems()
tmp = CreateObject("roSGNode", "MusicArtistData")
else if item.Type = "Audio"
tmp = CreateObject("roSGNode", "MusicSongData")
tmp.type = "Audio"
tmp.image = api_API().items.getimageurl(item.id, "primary", 0, { "maxHeight": 280, "maxWidth": 280, "quality": "90" })
else if item.Type = "MusicGenre"
tmp = CreateObject("roSGNode", "FolderData")
tmp.title = item.name

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8" ?>
<component name="LoadVideoContentTask" extends="Task">
<interface>
<field id="itemId" type="string" />
<field id="startIndex" type="integer" value="0" />
<field id="itemType" type="string" value="" />
<field id="limit" type="integer" value="60" />
<field id="metadata" type="assocarray" />
<field id="sortField" type="string" value="SortName" />
<field id="sortAscending" type="boolean" value="true" />
<field id="nameStartsWith" type="string" value="" />
<field id="recursive" type="boolean" value="true" />
<field id="filter" type="string" value="All" />
<field id="searchTerm" type="string" value="" />
<field id="studioIds" type="string" value="" />
<field id="genreIds" type="string" value="" />
<field id="view" type="string" value="" />
<!-- Total records available from server-->
<field id="totalRecordCount" type="int" value="-1" />
<field id="content" type="array" />
</interface>
<script type="text/brightscript" uri="LoadVideoContentTask.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
<script type="text/brightscript" uri="pkg:/source/api/Items.brs" />
<script type="text/brightscript" uri="pkg:/source/api/UserLibrary.brs" />
<script type="text/brightscript" uri="pkg:/source/roku_modules/api/api.brs" />
<script type="text/brightscript" uri="pkg:/source/api/baserequest.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
<script type="text/brightscript" uri="pkg:/source/api/Image.brs" />
<script type="text/brightscript" uri="pkg:/source/api/userauth.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/deviceCapabilities.brs" />
</component>

View File

@ -0,0 +1,33 @@
sub init()
m.content = m.top.findNode("content")
m.top.observeField("contentData", "onContentDataChanged")
m.top.observeFieldScoped("buttonSelected", "onButtonSelected")
m.top.id = "OKDialog"
m.top.height = 900
m.top.title = "What's New?"
m.top.buttons = [tr("OK")]
end sub
sub onButtonSelected()
if m.top.buttonSelected = 0
m.global.sceneManager.returnData = m.top.contentData.data[m.content.selectedIndex]
end if
end sub
sub onContentDataChanged()
i = 0
for each item in m.top.contentData.data
cardItem = m.content.CreateChild("StdDlgActionCardItem")
cardItem.iconType = "radiobutton"
if isValid(item.selected)
m.content.selectedIndex = i
end if
textLine = cardItem.CreateChild("SimpleLabel")
textLine.text = item.description
i++
end for
end sub

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8" ?>
<component name="RadioDialog" extends="StandardMessageDialog">
<children>
<StdDlgContentArea>
<StdDlgItemGroup id="content" />
</StdDlgContentArea>
</children>
<interface>
<field id="contentData" type="assocarray" />
</interface>
<script type="text/brightscript" uri="RadioDialog.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
</component>

View File

@ -0,0 +1,36 @@
sub init()
m.content = m.top.findNode("content")
m.top.observeField("contentData", "onContentDataChanged")
m.top.id = "OKDialog"
m.top.height = 900
m.top.title = "What's New?"
m.top.buttons = [tr("OK")]
m.dialogStyles = {
"default": {
"fontSize": 27,
"fontUri": "font:BoldSystemFontFile",
"color": "#EFEFEFFF"
},
"b": {
"fontSize": 27,
"fontUri": "font:SystemFontFile",
"color": "#999999"
},
"header": {
"fontSize": 35,
"fontUri": "font:SystemFontFile",
"color": "#00a4dcFF"
}
}
end sub
sub onContentDataChanged()
for each item in m.top.contentData.data
textLine = m.content.CreateChild("StdDlgMultiStyleTextItem")
textLine.drawingStyles = m.dialogStyles
textLine.text = item
end for
end sub

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8" ?>
<component name="StandardDialog" extends="StandardMessageDialog">
<children>
<StdDlgContentArea id="content" />
</children>
<interface>
<field id="contentData" type="assocarray" />
</interface>
<script type="text/brightscript" uri="StandardDialog.brs" />
</component>

View File

@ -0,0 +1,15 @@
sub setFields()
datum = m.top.json
m.top.id = datum.id
m.top.title = datum.name
m.top.overview = datum.overview
end sub
sub setPoster()
if m.top.image <> invalid
m.top.posterURL = m.top.image.url
else
m.top.posterURL = ""
end if
end sub

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8" ?>
<component name="PlaylistData" extends="JFContentItem">
<interface>
<field id="id" type="string" />
<field id="title" type="string" />
<field id="image" type="node" onChange="setPoster" />
<field id="overview" type="string" />
</interface>
<script type="text/brightscript" uri="PlaylistData.brs" />
</component>

View File

@ -237,7 +237,7 @@ end sub
'
' Display dialog to user with an OK button
sub userMessage(title as string, message as string)
dialog = createObject("roSGNode", "Dialog")
dialog = createObject("roSGNode", "StandardMessageDialog")
dialog.title = title
dialog.message = message
dialog.buttons = [tr("OK")]
@ -245,9 +245,101 @@ sub userMessage(title as string, message as string)
m.scene.dialog = dialog
end sub
'
' Display dialog to user with an OK button
sub standardDialog(title, message)
dialog = createObject("roSGNode", "StandardDialog")
dlgPalette = createObject("roSGNode", "RSGPalette")
dlgPalette.colors = {
DialogBackgroundColor: "0x262828FF",
DialogFocusColor: "0xcececeFF",
DialogFocusItemColor: "0x202020FF",
DialogSecondaryTextColor: "0xf8f8f8ff",
DialogSecondaryItemColor: "#00a4dcFF",
DialogTextColor: "0xeeeeeeFF"
}
dialog.palette = dlgPalette
dialog.observeField("buttonSelected", "dismiss_dialog")
dialog.title = title
dialog.contentData = message
dialog.buttons = [tr("OK")]
m.scene.dialog = dialog
end sub
'
' Display dialog to user with an OK button
sub radioDialog(title, message)
dialog = createObject("roSGNode", "RadioDialog")
dlgPalette = createObject("roSGNode", "RSGPalette")
dlgPalette.colors = {
DialogBackgroundColor: "0x262828FF",
DialogFocusColor: "0xcececeFF",
DialogFocusItemColor: "0x202020FF",
DialogSecondaryTextColor: "0xf8f8f8ff",
DialogSecondaryItemColor: "#00a4dcFF",
DialogTextColor: "0xeeeeeeFF"
}
dialog.palette = dlgPalette
dialog.observeField("buttonSelected", "dismiss_dialog")
dialog.title = title
dialog.contentData = message
dialog.buttons = [tr("OK")]
m.scene.dialog = dialog
end sub
'
' Display dialog to user with an OK button
sub optionDialog(title, message, buttons)
m.top.returnData = invalid
m.userselection = false
dialog = createObject("roSGNode", "StandardMessageDialog")
dlgPalette = createObject("roSGNode", "RSGPalette")
dlgPalette.colors = {
DialogBackgroundColor: "0x262828FF",
DialogFocusColor: "0xcececeFF",
DialogFocusItemColor: "0x202020FF",
DialogSecondaryTextColor: "0xf8f8f8ff",
DialogSecondaryItemColor: "#00a4dcFF",
DialogTextColor: "0xeeeeeeFF"
}
dialog.palette = dlgPalette
dialog.observeField("buttonSelected", "optionSelected")
dialog.observeField("wasClosed", "optionClosed")
dialog.title = title
dialog.message = message
dialog.buttons = buttons
m.scene.dialog = dialog
end sub
'
' Return button the user selected
sub optionClosed()
if m.userselection then return
m.top.returnData = {
indexSelected: -1,
buttonSelected: ""
}
end sub
'
' Return button the user selected
sub optionSelected()
m.userselection = true
m.top.returnData = {
indexSelected: m.scene.dialog.buttonSelected,
buttonSelected: m.scene.dialog.buttons[m.scene.dialog.buttonSelected]
}
dismiss_dialog()
end sub
'
' Close currently displayed dialog
sub dismiss_dialog()
print "Button Pressed"
m.scene.dialog.close = true
end sub

View File

@ -9,7 +9,11 @@
<function name="clearPreviousScene" />
<function name="resetTime" />
<function name="userMessage" />
<function name="standardDialog" />
<function name="radioDialog" />
<function name="optionDialog" />
<field id="currentUser" type="string" onChange="updateUser" />
<field id="returnData" type="assocarray" />
</interface>
<script type="text/brightscript" uri="SceneManager.brs" />
</component>

View File

@ -1,5 +1,6 @@
sub init()
m.queue = []
m.queueTypes = []
m.position = 0
end sub
@ -7,6 +8,7 @@ end sub
' Clear all content from play queue
sub clear()
m.queue = []
m.queueTypes = []
setPosition(0)
end sub
@ -15,6 +17,7 @@ end sub
' Delete item from play queue at passed index
sub deleteAtIndex(index)
m.queue.Delete(index)
m.queueTypes.Delete(index)
end sub
@ -67,6 +70,28 @@ function getQueue()
end function
'
' Return the types of items in current play queue
function getQueueTypes()
return m.queueTypes
end function
'
' Return the unique types of items in current play queue
function getQueueUniqueTypes()
itemTypes = []
for each item in getQueueTypes()
if not inArray(itemTypes, item)
itemTypes.push(item)
end if
end for
return itemTypes
end function
'
' Return item at end of play queue without removing
function peek()
@ -77,19 +102,17 @@ end function
'
' Play items in queue
sub playQueue()
nextItem = top()
nextItemMediaType = invalid
if isValid(nextItem?.json?.mediatype) and nextItem.json.mediatype <> ""
nextItemMediaType = LCase(nextItem.json.mediatype)
else if isValid(nextItem?.type) and nextItem.type <> ""
nextItemMediaType = LCase(nextItem.type)
end if
nextItem = getCurrentItem()
nextItemMediaType = getItemType(nextItem)
if not isValid(nextItemMediaType) then return
if nextItemMediaType = "audio"
CreateAudioPlayerView()
else if nextItemMediaType = "video"
CreateVideoPlayerView()
else if nextItemMediaType = "episode"
CreateVideoPlayerView()
end if
end sub
@ -98,6 +121,7 @@ end sub
' Remove item at end of play queue
sub pop()
m.queue.pop()
m.queueTypes.pop()
end sub
@ -105,6 +129,7 @@ end sub
' Push new items to the play queue
sub push(newItem)
m.queue.push(newItem)
m.queueTypes.push(getItemType(newItem))
end sub
'
@ -126,4 +151,18 @@ end function
sub set(items)
setPosition(0)
m.queue = items
for each item in items
m.queueTypes.push(getItemType(item))
end for
end sub
function getItemType(item) as string
if isValid(item?.json?.mediatype) and item.json.mediatype <> ""
return LCase(item.json.mediatype)
else if isValid(item?.type) and item.type <> ""
return LCase(item.type)
end if
return invalid
end function

View File

@ -8,6 +8,8 @@
<function name="getItemByIndex" />
<function name="getPosition" />
<function name="getQueue" />
<function name="getQueueTypes" />
<function name="getQueueUniqueTypes" />
<function name="moveBack" />
<function name="moveForward" />
<function name="peek" />

View File

@ -1,6 +1,118 @@
'
' View Creators
' ----------------
' Play Audio
sub CreateAudioPlayerView()
view = CreateObject("roSGNode", "AudioPlayerView")
view.observeField("state", m.port)
m.global.sceneManager.callFunc("pushScene", view)
m.view = CreateObject("roSGNode", "AudioPlayerView")
m.view.observeField("state", "onStateChange")
m.global.sceneManager.callFunc("pushScene", m.view)
end sub
' Play Video
sub CreateVideoPlayerView()
m.playbackData = {}
m.selectedSubtitle = {}
m.view = CreateObject("roSGNode", "VideoPlayerView")
m.view.observeField("state", "onStateChange")
m.view.observeField("selectPlaybackInfoPressed", "onSelectPlaybackInfoPressed")
m.view.observeField("selectSubtitlePressed", "onSelectSubtitlePressed")
m.getPlaybackInfoTask = createObject("roSGNode", "GetPlaybackInfoTask")
m.getPlaybackInfoTask.videoID = m.global.queueManager.callFunc("getCurrentItem").id
m.getPlaybackInfoTask.observeField("data", "onPlaybackInfoLoaded")
m.global.sceneManager.callFunc("pushScene", m.view)
end sub
'
' Event Handlers
' -----------------
' User requested subtitle selection popup
sub onSelectSubtitlePressed()
' None is always first in the subtitle list
subtitleData = {
data: [{ "description": "None", "type": "subtitleselection" }]
}
for each item in m.view.content.subtitletracks
item.type = "subtitleselection"
if item.description = m.selectedSubtitle.description
item.selected = true
end if
subtitleData.data.push(item)
end for
m.global.sceneManager.callFunc("radioDialog", tr("Select Subtitles"), subtitleData)
m.global.sceneManager.observeField("returnData", "onSelectionMade")
end sub
' User has selected something from the radioDialog popup
sub onSelectionMade()
m.global.sceneManager.unobserveField("returnData")
if not isValid(m.global.sceneManager.returnData) then return
if not isValid(m.global.sceneManager.returnData.type) then return
if LCase(m.global.sceneManager.returnData.type) = "subtitleselection"
processSubtitleSelection()
end if
end sub
sub processSubtitleSelection()
m.selectedSubtitle = m.global.sceneManager.returnData
if LCase(m.selectedSubtitle.description) = "none"
m.view.globalCaptionMode = "Off"
m.view.subtitleTrack = ""
return
end if
m.view.globalCaptionMode = "On"
m.view.subtitleTrack = m.selectedSubtitle.TrackName
end sub
' User requested playback info
sub onSelectPlaybackInfoPressed()
' Check if we already have playback info and show it in a popup
if isValid(m.playbackData?.playbackinfo)
m.global.sceneManager.callFunc("standardDialog", tr("Playback Info"), m.playbackData.playbackinfo)
return
end if
m.getPlaybackInfoTask.control = "RUN"
end sub
' The playback info task has returned data
sub onPlaybackInfoLoaded()
m.playbackData = m.getPlaybackInfoTask.data
' Check if we have playback info and show it in a popup
if isValid(m.playbackData?.playbackinfo)
m.global.sceneManager.callFunc("standardDialog", tr("Playback Info"), m.playbackData.playbackinfo)
end if
end sub
' Playback state change event handlers
sub onStateChange()
if LCase(m.view.state) = "finished"
' If there is something next in the queue, play it
if m.global.queueManager.callFunc("getPosition") < m.global.queueManager.callFunc("getCount") - 1
m.global.sceneManager.callFunc("clearPreviousScene")
m.global.queueManager.callFunc("moveForward")
m.global.queueManager.callFunc("playQueue")
return
end if
' Playback completed, return user to previous screen
m.global.sceneManager.callFunc("popScene")
end if
end sub

View File

@ -8,6 +8,8 @@ sub init()
setupDataTasks()
setupScreenSaver()
m.playlistTypeCount = m.global.queueManager.callFunc("getQueueUniqueTypes").count()
m.shuffleEnabled = false
m.loopMode = ""
m.buttonCount = m.buttons.getChildCount()
@ -226,8 +228,7 @@ sub audioStateChanged()
end if
if m.global.queueManager.callFunc("getPosition") < m.global.queueManager.callFunc("getCount") - 1
' We are not at the end of the song queue, advance to next song
LoadNextSong()
m.top.state = "finished"
else
' We are at the end of the song queue
@ -266,6 +267,8 @@ function playAction() as boolean
end function
function previousClicked() as boolean
if m.playlistTypeCount > 1 then return false
if m.top.audio.state = "playing"
m.top.audio.control = "stop"
end if
@ -296,6 +299,8 @@ function loopClicked() as boolean
end function
function nextClicked() as boolean
if m.playlistTypeCount > 1 then return false
if m.global.queueManager.callFunc("getPosition") < m.global.queueManager.callFunc("getCount") - 1
LoadNextSong()
end if
@ -415,6 +420,9 @@ end sub
' If we have more and 1 song to play, fade in the next and previous controls
sub loadButtons()
' Don't show audio buttons if we have a mixed playlist
if m.playlistTypeCount > 1 then return
if m.global.queueManager.callFunc("getCount") > 1
m.shuffleIndicator.opacity = ".4"
m.loopIndicator.opacity = ".4"
@ -504,7 +512,11 @@ sub setOnScreenTextValues(json)
if m.shuffleEnabled
currentSongIndex = findCurrentSongIndex(m.originalSongList)
end if
setFieldTextValue("numberofsongs", "Track " + stri(currentSongIndex + 1) + "/" + stri(m.global.queueManager.callFunc("getCount")))
if m.playlistTypeCount = 1
setFieldTextValue("numberofsongs", "Track " + stri(currentSongIndex + 1) + "/" + stri(m.global.queueManager.callFunc("getCount")))
end if
setFieldTextValue("artist", json.Artists[0])
setFieldTextValue("song", json.name)
end if

View File

@ -0,0 +1,166 @@
sub init()
m.top.optionsAvailable = false
setupMainNode()
m.playAll = m.top.findNode("playAll")
m.albumCover = m.top.findNode("albumCover")
m.songList = m.top.findNode("songList")
m.infoGroup = m.top.FindNode("infoGroup")
m.songListRect = m.top.FindNode("songListRect")
m.songList.observeField("doneLoading", "onDoneLoading")
m.spinner = m.top.findNode("spinner")
m.spinner.visible = true
m.dscr = m.top.findNode("overview")
createDialogPallete()
end sub
sub setupMainNode()
main = m.top.findNode("toplevel")
main.translation = [96, 175]
end sub
' Set values for displayed values on screen
sub pageContentChanged()
item = m.top.pageContent
setPosterImage(item.posterURL)
setScreenTitle(item.json)
setOnScreenTextValues(item.json)
' Only 1 song shown, so hide Play Album button
if item.json.ChildCount = 1
m.playAll.visible = false
end if
end sub
' Set poster image on screen
sub setPosterImage(posterURL)
if isValid(posterURL)
m.albumCover.uri = posterURL
end if
end sub
' Set screen's title text
sub setScreenTitle(json)
newTitle = ""
if isValid(json)
if isValid(json.AlbumArtist)
newTitle = json.AlbumArtist
end if
if isValid(json.AlbumArtist) and isValid(json.name)
newTitle = newTitle + " / "
end if
if isValid(json.name)
newTitle = newTitle + json.name
end if
end if
m.top.overhangTitle = newTitle
end sub
' Adjust scene by removing overview node and showing more songs
sub adjustScreenForNoOverview()
m.infoGroup.removeChild(m.dscr)
m.songListRect.height = 800
m.songList.numRows = 12
end sub
' Populate on screen text variables
sub setOnScreenTextValues(json)
if isValid(json)
if isValid(json.overview) and json.overview <> ""
' We have overview text
setFieldTextValue("overview", json.overview)
else
' We don't have overview text
adjustScreenForNoOverview()
end if
setFieldTextValue("numberofsongs", stri(json.ChildCount) + " Tracks")
if type(json.ProductionYear) = "roInt"
setFieldTextValue("released", "Released " + stri(json.ProductionYear))
end if
if json.genres.count() > 0
setFieldTextValue("genres", json.genres.join(", "))
end if
if type(json.RunTimeTicks) = "LongInteger"
setFieldTextValue("runtime", stri(getMinutes(json.RunTimeTicks)) + " mins")
end if
end if
end sub
function onKeyEvent(key as string, press as boolean) as boolean
if not press then return false
if m.spinner.visible then return false
if key = "options"
if m.dscr.isTextEllipsized
createFullDscrDlg()
return true
end if
return false
end if
if key = "right"
if m.playAll.hasFocus()
m.songList.setFocus(true)
return true
end if
else if key = "left" and m.songList.hasFocus()
if m.playAll.visible
m.playAll.setFocus(true)
else
return false
end if
return true
end if
return false
end function
sub createFullDscrDlg()
dlg = CreateObject("roSGNode", "OverviewDialog")
dlg.Title = tr("Press 'Back' to Close")
dlg.width = 1290
dlg.palette = m.dlgPalette
dlg.overview = [m.dscr.text]
m.fullDscrDlg = dlg
m.top.getScene().dialog = dlg
border = createObject("roSGNode", "Poster")
border.uri = "pkg:/images/hd_focul_9.png"
border.blendColor = "#c9c9c9ff"
border.width = dlg.width + 6
border.height = dlg.height + 6
border.translation = [dlg.translation[0] - 3, dlg.translation[1] - 3]
border.visible = true
end sub
sub createDialogPallete()
m.dlgPalette = createObject("roSGNode", "RSGPalette")
m.dlgPalette.colors = {
DialogBackgroundColor: "0x262828FF",
DialogItemColor: "0x00EF00FF",
DialogTextColor: "0xb0b0b0FF",
DialogFocusColor: "0xcececeFF",
DialogFocusItemColor: "0x202020FF",
DialogSecondaryTextColor: "0xf8f8f8ff",
DialogSecondaryItemColor: "0xcc7ecc4D",
DialogInputFieldColor: "0x80FF8080",
DialogKeyboardColor: "0x80FF804D",
DialogFootprintColor: "0x80FF804D"
}
end sub
sub onDoneLoading()
m.songList.unobservefield("doneLoading")
m.spinner.visible = false
end sub
sub OnScreenHidden()
m.spinner.visible = false
end sub

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8" ?>
<component name="PlaylistView" extends="JFScreen">
<children>
<LayoutGroup id="toplevel" layoutDirection="vert" itemSpacings="[-10]">
<LayoutGroup id="main_group" layoutDirection="horiz" itemSpacings="[15]">
<LayoutGroup layoutDirection="vert" itemSpacings="[15]">
<Poster id="albumCover" width="450" height="450" />
<Label id="numberofsongs" width="450" height="25" />
<Label id="genres" width="450" height="25" />
<Label id="runtime" width="450" height="25" />
<Label id="released" width="450" height="25" />
<JFButton id="playAll" minChars="8" text="Play All"></JFButton>
</LayoutGroup>
<LayoutGroup id="infoGroup" layoutDirection="vert" itemSpacings="[15]">
<Label id="overview" wrap="true" height="310" width="1250" ellipsisText=" ... (Press * to read more)" />
<Rectangle id='songListRect' translation="[-30, 0]" width="1260" height="510" color="0x202020ff">
<AlbumTrackList itemComponentName="SongItem" id="songList" translation="[45, 25]" itemSize="[1170,60]" numRows="7" />
</Rectangle>
</LayoutGroup>
</LayoutGroup>
</LayoutGroup>
<Spinner id="spinner" translation="[920, 540]" visible="false" />
</children>
<interface>
<field id="pageContent" type="node" onChange="pageContentChanged" />
<field id="albumData" type="assocarray" alias="songList.MusicArtistAlbumData" />
<field id="playItem" alias="songList.itemSelected" />
<field id="playAllSelected" alias="playAll.buttonSelected" />
</interface>
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
<script type="text/brightscript" uri="PlaylistView.brs" />
</component>

View File

@ -3,6 +3,7 @@ sub init()
m.rows = m.top.findNode("picker")
m.poster = m.top.findNode("seasonPoster")
m.Shuffle = m.top.findNode("Shuffle")
m.Random = m.top.findNode("Random")
m.tvEpisodeRow = m.top.findNode("tvEpisodeRow")
@ -29,17 +30,28 @@ sub updateSeason()
imgParams = { "maxHeight": 450, "maxWidth": 300 }
m.poster.uri = ImageURL(m.top.seasonData.Id, "Primary", imgParams)
m.Random.visible = true
m.Shuffle.visible = true
m.top.overhangTitle = m.top.seasonData.SeriesName + " - " + m.top.seasonData.name
end sub
function onKeyEvent(key as string, press as boolean) as boolean
handled = false
if key = "left" and not m.Random.hasFocus()
if key = "left" and not m.Shuffle.hasFocus()
m.Shuffle.setFocus(true)
return true
end if
if key = "down" and m.Shuffle.hasFocus()
m.Random.setFocus(true)
return true
end if
if key = "up" and m.Random.hasFocus()
m.Shuffle.setFocus(true)
return true
end if
if key = "right" and not m.tvEpisodeRow.hasFocus()
m.tvEpisodeRow.setFocus(true)
return true
@ -52,6 +64,21 @@ function onKeyEvent(key as string, press as boolean) as boolean
m.top.quickPlayNode = m.rows.getChild(0).objects.items[randomEpisode]
return true
end if
if m.Shuffle.hasFocus()
episodeList = m.rows.getChild(0).objects.items
for i = 0 to episodeList.count() - 1
j = Rnd(episodeList.count() - 1)
temp = episodeList[i]
episodeList[i] = episodeList[j]
episodeList[j] = temp
end for
m.global.queueManager.callFunc("set", episodeList)
m.global.queueManager.callFunc("playQueue")
return true
end if
end if

View File

@ -6,7 +6,8 @@
<Label id="unplayedEpisodeCount" width="90" height="60" font="font:SmallestBoldSystemFont" horizAlign="center" vertAlign="center" />
</Rectangle>
</Poster>
<JFButton id="Random" text="Play Random" translation="[90, 640]" visible="false"></JFButton>
<JFButton id="Shuffle" minChars="10" text="Shuffle" translation="[90, 640]" visible="false"></JFButton>
<JFButton id="Random" minChars="12" text="Play Random" translation="[90, 740]" visible="false"></JFButton>
<TVEpisodeRowWithOptions id="picker" visible="true" />
</children>
<interface>

View File

@ -5,7 +5,8 @@ sub init()
m.extrasSlider = m.top.findNode("tvSeasonExtras")
m.unplayedCount = m.top.findNode("unplayedCount")
m.unplayedEpisodeCount = m.top.findNode("unplayedEpisodeCount")
'm.extrasSlider.translation = [30,1014]
m.getShuffleEpisodesTask = createObject("roSGNode", "getShuffleEpisodesTask")
m.Shuffle = m.top.findNode("Shuffle")
m.extrasSlider.visible = true
end sub
@ -55,6 +56,7 @@ sub itemContentChanged()
setFieldText("overview", itemData.overview)
m.Shuffle.visible = true
if type(itemData.RunTimeTicks) = "LongInteger"
setFieldText("runtime", stri(getRuntime()) + " mins")
@ -170,7 +172,22 @@ function round(f as float) as integer
end if
end function
sub onShuffleEpisodeDataLoaded()
m.getShuffleEpisodesTask.unobserveField("data")
m.global.queueManager.callFunc("set", m.getShuffleEpisodesTask.data.items)
m.global.queueManager.callFunc("playQueue")
end sub
function onKeyEvent(key as string, press as boolean) as boolean
if key = "OK" or key = "play"
if m.Shuffle.hasFocus()
m.getShuffleEpisodesTask.showID = m.top.itemContent.id
m.getShuffleEpisodesTask.observeField("data", "onShuffleEpisodeDataLoaded")
m.getShuffleEpisodesTask.control = "RUN"
return true
end if
end if
if not press then return false
overview = m.top.findNode("overview")
@ -178,6 +195,9 @@ function onKeyEvent(key as string, press as boolean) as boolean
bottomGrp = m.top.findNode("extrasGrid")
if key = "down" and overview.hasFocus()
m.Shuffle.setFocus(true)
return true
else if key = "down" and m.Shuffle.hasFocus()
topGrp.setFocus(true)
return true
else if key = "down" and topGrp.hasFocus()
@ -195,6 +215,9 @@ function onKeyEvent(key as string, press as boolean) as boolean
return true
end if
else if key = "up" and topGrp.hasFocus()
m.Shuffle.setFocus(true)
return true
else if key = "up" and m.Shuffle.hasFocus()
overview.setFocus(true)
return true
end if

View File

@ -21,6 +21,7 @@
<Label id="tagline" />
<Label id="overview" wrap="true" width="1400" maxLines="4" />
<Label id="history" />
<JFButton id="Shuffle" minChars="15" text="Shuffle" translation="[90, 640]" visible="false"></JFButton>
</LayoutGroup>
</LayoutGroup>
<TVSeasonRow id="seasons" />

View File

@ -0,0 +1,308 @@
sub init()
currentItem = m.global.queueManager.callFunc("getCurrentItem")
m.top.id = currentItem.id
' Load meta data
m.LoadMetaDataTask = CreateObject("roSGNode", "LoadVideoContentTask")
m.LoadMetaDataTask.itemId = currentItem.id
m.LoadMetaDataTask.observeField("content", "onVideoContentLoaded")
m.LoadMetaDataTask.control = "RUN"
m.playbackTimer = m.top.findNode("playbackTimer")
m.bufferCheckTimer = m.top.findNode("bufferCheckTimer")
m.top.observeField("state", "onState")
m.top.observeField("content", "onContentChange")
m.playbackTimer.observeField("fire", "ReportPlayback")
m.bufferPercentage = 0 ' Track whether content is being loaded
m.playReported = false
m.top.transcodeReasons = []
m.bufferCheckTimer.duration = 30
if get_user_setting("ui.design.hideclock") = "true"
clockNode = findNodeBySubtype(m.top, "clock")
if clockNode[0] <> invalid then clockNode[0].parent.removeChild(clockNode[0].node)
end if
'Play Next Episode button
m.nextEpisodeButton = m.top.findNode("nextEpisode")
m.nextEpisodeButton.text = tr("Next Episode")
m.nextEpisodeButton.setFocus(false)
m.showNextEpisodeButtonAnimation = m.top.findNode("showNextEpisodeButton")
m.hideNextEpisodeButtonAnimation = m.top.findNode("hideNextEpisodeButton")
m.checkedForNextEpisode = false
m.getNextEpisodeTask = createObject("roSGNode", "GetNextEpisodeTask")
m.getNextEpisodeTask.observeField("nextEpisodeData", "onNextEpisodeDataLoaded")
m.top.retrievingBar.filledBarBlendColor = m.global.constants.colors.blue
m.top.bufferingBar.filledBarBlendColor = m.global.constants.colors.blue
m.top.trickPlayBar.filledBarBlendColor = m.global.constants.colors.blue
end sub
sub onVideoContentLoaded()
m.LoadMetaDataTask.unobserveField("content")
' If we have nothing to play, return to previous screen
if not isValid(m.LoadMetaDataTask.content)
m.global.sceneManager.callFunc("popScene")
return
end if
if not isValid(m.LoadMetaDataTask.content[0])
m.global.sceneManager.callFunc("popScene")
return
end if
if m.LoadMetaDataTask.content.count() = 0
m.global.sceneManager.callFunc("popScene")
return
end if
m.top.content = m.LoadMetaDataTask.content[0].content
m.top.PlaySessionId = m.LoadMetaDataTask.content[0].PlaySessionId
m.top.videoId = m.LoadMetaDataTask.content[0].id
m.top.container = m.LoadMetaDataTask.content[0].container
m.top.mediaSourceId = m.LoadMetaDataTask.content[0].mediaSourceId
m.top.audioIndex = m.LoadMetaDataTask.content[0].audio_stream_idx
m.top.setFocus(true)
m.top.control = "play"
m.top.getScene().findNode("overhang").visible = false
end sub
' Event handler for when video content field changes
sub onContentChange()
if not isValid(m.top.content) then return
m.top.observeField("position", "onPositionChanged")
' If video content type is not episode, remove position observer
if m.top.content.contenttype <> 4
m.top.unobserveField("position")
end if
end sub
sub onNextEpisodeDataLoaded()
m.checkedForNextEpisode = true
m.top.observeField("position", "onPositionChanged")
if m.getNextEpisodeTask.nextEpisodeData.Items.count() <> 2
m.top.unobserveField("position")
end if
end sub
'
' Runs Next Episode button animation and sets focus to button
sub showNextEpisodeButton()
if not m.nextEpisodeButton.visible
m.showNextEpisodeButtonAnimation.control = "start"
m.nextEpisodeButton.setFocus(true)
m.nextEpisodeButton.visible = true
end if
end sub
'
'Update count down text
sub updateCount()
m.nextEpisodeButton.text = tr("Next Episode") + " " + Int(m.top.duration - m.top.position).toStr()
end sub
'
' Runs hide Next Episode button animation and sets focus back to video
sub hideNextEpisodeButton()
m.hideNextEpisodeButtonAnimation.control = "start"
m.nextEpisodeButton.setFocus(false)
m.top.setFocus(true)
end sub
' Checks if we need to display the Next Episode button
sub checkTimeToDisplayNextEpisode()
if int(m.top.position) >= (m.top.duration - 30)
showNextEpisodeButton()
updateCount()
return
end if
if m.nextEpisodeButton.visible or m.nextEpisodeButton.hasFocus()
m.nextEpisodeButton.visible = false
m.nextEpisodeButton.setFocus(false)
end if
end sub
' When Video Player state changes
sub onPositionChanged()
' Check if dialog is open
m.dialog = m.top.getScene().findNode("dialogBackground")
if not isValid(m.dialog)
checkTimeToDisplayNextEpisode()
end if
end sub
'
' When Video Player state changes
sub onState(msg)
' When buffering, start timer to monitor buffering process
if m.top.state = "buffering" and m.bufferCheckTimer <> invalid
' start timer
m.bufferCheckTimer.control = "start"
m.bufferCheckTimer.ObserveField("fire", "bufferCheck")
else if m.top.state = "error"
if not m.playReported and m.top.transcodeAvailable
m.top.retryWithTranscoding = true ' If playback was not reported, retry with transcoding
else
' If an error was encountered, Display dialog
dialog = createObject("roSGNode", "Dialog")
dialog.title = tr("Error During Playback")
dialog.buttons = [tr("OK")]
dialog.message = tr("An error was encountered while playing this item.")
dialog.observeField("buttonSelected", "dialogClosed")
m.top.getScene().dialog = dialog
end if
' Stop playback and exit player
m.top.control = "stop"
m.top.backPressed = true
else if m.top.state = "playing"
' Check if next episde is available
if isValid(m.top.showID)
if m.top.showID <> "" and not m.checkedForNextEpisode and m.top.content.contenttype = 4
m.getNextEpisodeTask.showID = m.top.showID
m.getNextEpisodeTask.videoID = m.top.id
m.getNextEpisodeTask.control = "RUN"
end if
end if
if m.playReported = false
ReportPlayback("start")
m.playReported = true
else
ReportPlayback()
end if
m.playbackTimer.control = "start"
else if m.top.state = "paused"
m.playbackTimer.control = "stop"
ReportPlayback()
else if m.top.state = "stopped"
m.playbackTimer.control = "stop"
ReportPlayback("stop")
m.playReported = false
end if
end sub
'
' Report playback to server
sub ReportPlayback(state = "update" as string)
if m.top.position = invalid then return
params = {
"ItemId": m.top.id,
"PlaySessionId": m.top.PlaySessionId,
"PositionTicks": int(m.top.position) * 10000000&, 'Ensure a LongInteger is used
"IsPaused": (m.top.state = "paused")
}
if m.top.content.live
params.append({
"MediaSourceId": m.top.transcodeParams.MediaSourceId,
"LiveStreamId": m.top.transcodeParams.LiveStreamId
})
m.bufferCheckTimer.duration = 30
end if
' Report playstate via worker task
playstateTask = m.global.playstateTask
playstateTask.setFields({ status: state, params: params })
playstateTask.control = "RUN"
end sub
'
' Check the the buffering has not hung
sub bufferCheck(msg)
if m.top.state <> "buffering"
' If video is not buffering, stop timer
m.bufferCheckTimer.control = "stop"
m.bufferCheckTimer.unobserveField("fire")
return
end if
if m.top.bufferingStatus <> invalid
' Check that the buffering percentage is increasing
if m.top.bufferingStatus["percentage"] > m.bufferPercentage
m.bufferPercentage = m.top.bufferingStatus["percentage"]
else if m.top.content.live = true
m.top.callFunc("refresh")
else
' If buffering has stopped Display dialog
dialog = createObject("roSGNode", "Dialog")
dialog.title = tr("Error Retrieving Content")
dialog.buttons = [tr("OK")]
dialog.message = tr("There was an error retrieving the data for this item from the server.")
dialog.observeField("buttonSelected", "dialogClosed")
m.top.getScene().dialog = dialog
' Stop playback and exit player
m.top.control = "stop"
m.top.backPressed = true
end if
end if
end sub
'
' Clean up on Dialog Closed
sub dialogClosed(msg)
sourceNode = msg.getRoSGNode()
sourceNode.unobserveField("buttonSelected")
sourceNode.close = true
end sub
function onKeyEvent(key as string, press as boolean) as boolean
if key = "OK" and m.nextEpisodeButton.hasfocus() and not m.top.trickPlayBar.visible
m.top.control = "stop"
m.top.state = "finished"
hideNextEpisodeButton()
return true
else
'Hide Next Episode Button
if m.nextEpisodeButton.visible or m.nextEpisodeButton.hasFocus()
m.nextEpisodeButton.visible = false
m.nextEpisodeButton.setFocus(false)
m.top.setFocus(true)
end if
end if
if not press then return false
if key = "down"
m.top.selectSubtitlePressed = true
return true
else if key = "up"
m.top.selectPlaybackInfoPressed = true
return true
else if key = "OK"
' OK will play/pause depending on current state
' return false to allow selection during seeking
if m.top.state = "paused"
m.top.control = "resume"
return false
else if m.top.state = "playing"
m.top.control = "pause"
return false
end if
end if
if key = "back"
m.top.control = "stop"
end if
return false
end function

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8" ?>
<component name="VideoPlayerView" extends="Video">
<interface>
<field id="backPressed" type="boolean" alwaysNotify="true" />
<field id="selectSubtitlePressed" type="boolean" alwaysNotify="true" />
<field id="selectPlaybackInfoPressed" type="boolean" alwaysNotify="true" />
<field id="PlaySessionId" type="string" />
<field id="Subtitles" type="array" />
<field id="SelectedSubtitle" type="integer" />
<field id="captionMode" type="string" />
<field id="container" type="string" />
<field id="directPlaySupported" type="boolean" />
<field id="systemOverlay" type="boolean" value="false" />
<field id="showID" type="string" />
<field id="lastFocus" type="node" />
<field id="transcodeParams" type="assocarray" />
<field id="transcodeAvailable" type="boolean" value="false" />
<field id="retryWithTranscoding" type="boolean" value="false" />
<field id="isTranscoded" type="boolean" />
<field id="transcodeReasons" type="array" />
<field id="videoId" type="string" />
<field id="mediaSourceId" type="string" />
<field id="audioIndex" type="integer" />
<field id="runTime" type="integer" />
</interface>
<script type="text/brightscript" uri="VideoPlayerView.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
<script type="text/brightscript" uri="pkg:/source/roku_modules/api/api.brs" />
<children>
<timer id="playbackTimer" repeat="true" duration="30" />
<timer id="bufferCheckTimer" repeat="true" />
<JFButton id="nextEpisode" opacity="0" textColor="#f0f0f0" focusedTextColor="#202020" focusFootprintBitmapUri="pkg:/images/option-menu-bg.9.png" focusBitmapUri="pkg:/images/white.9.png" translation="[1500, 900]" />
<!--animation for the play next episode button-->
<Animation id="showNextEpisodeButton" duration="1.0" repeat="false" easeFunction="inQuad">
<FloatFieldInterpolator key="[0.0, 1.0]" keyValue="[0.0, .9]" fieldToInterp="nextEpisode.opacity" />
</Animation>
<Animation id="hideNextEpisodeButton" duration=".2" repeat="false" easeFunction="inQuad">
<FloatFieldInterpolator key="[0.0, 1.0]" keyValue="[.9, 0]" fieldToInterp="nextEpisode.opacity" />
</Animation>
</children>
</component>

View File

@ -233,6 +233,8 @@ sub Main (args as dynamic) as void
end if
else if selectedItem.type = "MusicAlbum"
group = CreateAlbumView(selectedItem.json)
else if selectedItem.type = "Playlist"
group = CreatePlaylistView(selectedItem.json)
else if selectedItem.type = "Audio"
m.global.queueManager.callFunc("clear")
m.global.queueManager.callFunc("push", selectedItem.json)
@ -273,6 +275,14 @@ sub Main (args as dynamic) as void
selectedIndex = msg.getData()
screenContent = msg.getRoSGNode()
m.global.queueManager.callFunc("clear")
m.global.queueManager.callFunc("push", screenContent.albumData.items[selectedIndex])
m.global.queueManager.callFunc("playQueue")
else if isNodeEvent(msg, "playItem")
' User has selected audio they want us to play
selectedIndex = msg.getData()
screenContent = msg.getRoSGNode()
m.global.queueManager.callFunc("clear")
m.global.queueManager.callFunc("push", screenContent.albumData.items[selectedIndex])
m.global.queueManager.callFunc("playQueue")

View File

@ -448,6 +448,23 @@ function CreateAlbumView(album)
return group
end function
' Shows details on selected playlist. Description text, image, and list of available items
function CreatePlaylistView(album)
group = CreateObject("roSGNode", "PlaylistView")
m.global.sceneManager.callFunc("pushScene", group)
group.pageContent = ItemMetaData(album.id)
group.albumData = PlaylistItemList(album.id)
' Watch for user clicking on an item
group.observeField("playItem", m.port)
' Watch for user click on Play button
group.observeField("playAllSelected", m.port)
return group
end function
function CreateSeasonDetailsGroup(series, season)
startLoadingSpinner()
group = CreateObject("roSGNode", "TVEpisodes")

View File

@ -76,6 +76,7 @@ function ItemMetaData(id as string)
resp = APIRequest(url)
data = getJson(resp)
if data = invalid then return invalid
imgParams = {}
if data.type <> "Audio"
if data?.UserData?.PlayedPercentage <> invalid
@ -250,6 +251,30 @@ function GetSongsByArtist(id as string)
return data
end function
' Get Items that are under the provided item
function PlaylistItemList(id as string)
url = Substitute("Playlists/{0}/Items", id)
resp = APIRequest(url, {
"UserId": get_setting("active_user")
})
results = []
data = getJson(resp)
if data = invalid then return invalid
if data.Items = invalid then return invalid
if data.Items.Count() = 0 then return invalid
for each item in data.Items
tmp = CreateObject("roSGNode", "PlaylistData")
tmp.image = PosterImage(item.id)
tmp.json = item
results.push(tmp)
end for
data.Items = results
return data
end function
' Get Songs that are on an Album
function MusicSongList(id as string)
url = Substitute("Users/{0}/Items", get_setting("active_user"), id)
@ -402,3 +427,23 @@ function TVEpisodes(show_id as string, season_id as string)
data.Items = results
return data
end function
function TVEpisodeShuffleList(show_id as string)
url = Substitute("Shows/{0}/Episodes", show_id)
resp = APIRequest(url, {
"UserId": get_setting("active_user"),
"Limit": 200,
"sortBy": "Random"
})
data = getJson(resp)
results = []
for each item in data.Items
tmp = CreateObject("roSGNode", "TVEpisodeData")
tmp.json = item
results.push(tmp)
end for
data.Items = results
return data
end function

View File

@ -9,7 +9,8 @@ sub setConstants()
poster_bg_pallet: ["#00455c", "#44bae1", "#00a4db", "#1c4c5c", "#007ea8"],
colors: {
button: "#006fab"
button: "#006fab",
blue: "#00a4dcFF"
},
icons: {

View File

@ -282,6 +282,14 @@ function findNodeBySubtype(node, subtype)
return foundNodes
end function
' Search string array for search value. Return if it's found
function inArray(array, searchValue) as boolean
for each item in array
if lcase(item) = lcase(searchValue) then return true
end for
return false
end function
sub startLoadingSpinner()
m.spinner = createObject("roSGNode", "Spinner")
m.spinner.translation = "[900, 450]"
@ -289,7 +297,6 @@ sub startLoadingSpinner()
m.scene.appendChild(m.spinner)
end sub
sub startMediaLoadingSpinner()
dialog = createObject("roSGNode", "ProgressDialog")
dialog.id = "invisibiledialog"