jellyfin-roku/source/Main.brs

576 lines
20 KiB
Plaintext
Raw Normal View History

2019-01-30 04:19:27 +00:00
sub Main()
2019-05-02 17:45:07 +00:00
' If the Rooibos files are included in deployment, run tests
2021-01-31 19:35:18 +00:00
'bs:disable-next-line
2019-05-02 17:45:07 +00:00
if (type(Rooibos__Init) = "Function") then Rooibos__Init()
2019-04-29 16:44:37 +00:00
' The main function that runs when the application is launched.
m.screen = CreateObject("roSGScreen")
' Set global constants
setConstants()
m.port = CreateObject("roMessagePort")
m.screen.setMessagePort(m.port)
2019-10-13 19:33:14 +00:00
m.scene = m.screen.CreateScene("JFScene")
m.screen.show()
' Set any initial Global Variables
m.global = m.screen.getGlobalNode()
m.global.addFields( {app_loaded: false} )
m.overhang = CreateObject("roSGNode", "JFOverhang")
m.scene.insertChild(m.overhang, 0)
2021-02-01 03:10:04 +00:00
m.page_size = 48
app_start:
m.overhang.title = ""
2019-03-05 04:59:31 +00:00
' First thing to do is validate the ability to use the API
if not LoginFlow() then return
wipe_groups()
' load home page
m.overhang.title = tr("Home")
2020-02-26 22:55:47 +00:00
m.overhang.currentUser = m.user.Name
m.overhang.showOptions = true
group = CreateHomeGroup()
2020-12-06 04:59:32 +00:00
group.userConfig = m.user.configuration
group.callFunc("loadLibraries")
m.scene.appendChild(group)
2019-10-13 19:33:14 +00:00
m.scene.observeField("backPressed", m.port)
m.scene.observeField("optionsPressed", m.port)
m.scene.observeField("mutePressed", m.port)
' Handle input messages
input = CreateObject("roInput")
input.SetMessagePort(m.port)
2019-10-13 19:33:14 +00:00
2020-05-07 01:51:17 +00:00
m.device = CreateObject("roDeviceInfo")
m.device.setMessagePort(m.port)
m.device.EnableScreensaverExitedEvent(true)
2019-10-13 19:33:14 +00:00
' This is the core logic loop. Mostly for transitioning between scenes
' This now only references m. fields so could be placed anywhere, in theory
' "group" is always "whats on the screen"
' m.scene's children is the "previous view" stack
while(true)
msg = wait(0, m.port)
if type(msg) = "roSGScreenEvent" and msg.isScreenClosed() then
print "CLOSING SCREEN"
return
else if isNodeEvent(msg, "buttonSelected")
' Dialog Button Selected - If not handled more locally, just close the dialog
dialog = msg.getRoSGNode()
dialog.unobserveField("buttonSelected")
dialog.close = true
else if isNodeEvent(msg, "backPressed")
n = m.scene.getChildCount() - 1
2020-02-17 22:13:20 +00:00
if msg.getRoSGNode().focusedChild <> invalid and msg.getRoSGNode().focusedChild.isSubtype("JFVideo")
2020-02-17 19:30:04 +00:00
stopPlayback()
2020-11-30 07:46:20 +00:00
RemoveCurrentGroup()
else
if n = 1 then return
RemoveCurrentGroup()
end if
group = m.scene.getChild(n-1)
2019-10-13 19:33:14 +00:00
else if isNodeEvent(msg, "optionsPressed")
group.lastFocus = group.focusedChild
panel = group.findNode("options")
panel.visible = true
panel.findNode("panelList").setFocus(true)
else if isNodeEvent(msg, "closeSidePanel")
if group.lastFocus <> invalid
group.lastFocus.setFocus(true)
else
group.setFocus(true)
end if
else if isNodeEvent(msg, "quickPlayNode")
reportingNode = msg.getRoSGNode()
itemNode = reportingNode.quickPlayNode
if itemNode = invalid or itemNode.id = "" then return
if itemNode.type = "Episode" or itemNode.type = "Movie" or itemNode.type = "Video" then
video = CreateVideoPlayerGroup(itemNode.id)
if video <> invalid then
group.lastFocus = group.focusedChild
group.setFocus(false)
group.visible = false
group = video
m.scene.appendChild(group)
group.setFocus(true)
group.control = "play"
ReportPlayback(group, "start")
m.overhang.visible = false
end if
end if
else if isNodeEvent(msg, "selectedItem")
' If you select a library from ANYWHERE, follow this flow
selectedItem = msg.getData()
if selectedItem.type = "CollectionFolder" OR selectedItem.type = "UserView" OR selectedItem.type = "Folder"
group.lastFocus = group.focusedChild
group.setFocus(false)
group.visible = false
2020-07-11 13:37:33 +00:00
m.overhang.title = selectedItem.title
group = CreateItemGrid(selectedItem)
group.overhangTitle = selectedItem.title
m.scene.appendChild(group)
2021-02-01 03:10:04 +00:00
else if selectedItem.type = "Folder"
2020-10-30 17:23:28 +00:00
group.lastFocus = group.focusedChild
group.setFocus(false)
group.visible = false
m.overhang.title = selectedItem.title
group = CreateItemGrid(selectedItem)
2020-10-30 17:23:28 +00:00
group.overhangTitle = selectedItem.title
m.scene.appendChild(group)
else if selectedItem.type = "Episode" then
' play episode
' todo: create an episode page to link here
video_id = selectedItem.id
video = CreateVideoPlayerGroup(video_id)
if video <> invalid then
group.lastFocus = group.focusedChild
group.setFocus(false)
group.visible = false
group = video
m.scene.appendChild(group)
group.setFocus(true)
group.control = "play"
ReportPlayback(group, "start")
m.overhang.visible = false
end if
else if selectedItem.type = "Series" then
group.lastFocus = group.focusedChild
group.setFocus(false)
group.visible = false
2020-07-11 13:37:33 +00:00
m.overhang.title = selectedItem.title
m.overhang.showOptions = false
m.scene.unobserveField("optionsPressed")
group = CreateSeriesDetailsGroup(selectedItem.json)
2020-07-11 13:37:33 +00:00
group.overhangTitle = selectedItem.title
m.scene.appendChild(group)
else if selectedItem.type = "Movie" then
' open movie detail page
group.lastFocus = group.focusedChild
group.setFocus(false)
group.visible = false
2020-07-11 13:37:33 +00:00
m.overhang.title = selectedItem.title
m.overhang.showOptions = false
2020-03-08 01:44:02 +00:00
m.scene.unobserveField("optionsPressed")
group = CreateMovieDetailsGroup(selectedItem)
2020-07-11 13:37:33 +00:00
group.overhangTitle = selectedItem.title
m.scene.appendChild(group)
2020-10-27 17:12:18 +00:00
else if selectedItem.type = "TvChannel" or selectedItem.type = "Video" then
' play channel feed
video_id = selectedItem.id
' Show Channel Loading spinner
dialog = createObject("roSGNode", "ProgressDialog")
dialog.title = tr("Loading Channel Data")
m.scene.dialog = dialog
video = CreateVideoPlayerGroup(video_id)
dialog.close = true
if video <> invalid then
2020-11-23 17:13:57 +00:00
if group.lastFocus = invalid then group.lastFocus = group.focusedChild
group.setFocus(false)
group.visible = false
group = video
m.scene.appendChild(group)
group.setFocus(true)
group.control = "play"
ReportPlayback(group, "start")
m.overhang.visible = false
2021-02-01 03:10:04 +00:00
else
dialog = createObject("roSGNode", "Dialog")
dialog.title = tr("Error loading Channel Data")
dialog.message = tr("Unable to load Channel Data from the server")
dialog.buttons = [tr("OK")]
m.scene.dialog = dialog
m.scene.dialog.observeField("buttonSelected", m.port)
end if
else
2019-10-13 19:33:14 +00:00
' TODO - switch on more node types
2021-02-12 15:19:30 +00:00
message_dialog("This type is not yet supported: " + selectedItem.type + ".")
selectedItem = invalid
end if
else if isNodeEvent(msg, "movieSelected")
' If you select a movie from ANYWHERE, follow this flow
node = getMsgPicker(msg, "picker")
group.lastFocus = group.focusedChild
group.setFocus(false)
group.visible = false
m.overhang.title = node.title
m.overhang.showOptions = false
2020-03-08 01:44:02 +00:00
m.scene.unobserveField("optionsPressed")
2019-10-13 19:33:14 +00:00
group = CreateMovieDetailsGroup(node)
group.overhangTitle = node.title
m.scene.appendChild(group)
else if isNodeEvent(msg, "seriesSelected")
' If you select a TV Series from ANYWHERE, follow this flow
node = getMsgPicker(msg, "picker")
group.lastFocus = group.focusedChild
group.setFocus(false)
group.visible = false
m.overhang.title = node.title
m.overhang.showOptions = false
2020-03-08 01:44:02 +00:00
m.scene.unobserveField("optionsPressed")
group = CreateSeriesDetailsGroup(node)
group.overhangTitle = node.title
m.scene.appendChild(group)
else if isNodeEvent(msg, "seasonSelected")
' If you select a TV Season from ANYWHERE, follow this flow
ptr = msg.getData()
' ptr is for [row, col] of selected item... but we only have 1 row
series = msg.getRoSGNode()
node = series.seasonData.items[ptr[1]]
group.lastFocus = group.focusedChild.focusedChild
group.setFocus(false)
group.visible = false
m.overhang.title = series.overhangTitle + " - " + node.title
m.overhang.showOptions = false
2020-03-08 01:44:02 +00:00
m.scene.unobserveField("optionsPressed")
group = CreateSeasonDetailsGroup(series.itemContent, node)
m.scene.appendChild(group)
else if isNodeEvent(msg, "episodeSelected")
' If you select a TV Episode from ANYWHERE, follow this flow
node = getMsgPicker(msg, "picker")
video_id = node.id
2020-03-04 02:46:26 +00:00
video = CreateVideoPlayerGroup(video_id)
if video <> invalid then
group.lastFocus = group.focusedChild
2020-03-04 02:46:26 +00:00
group.setFocus(false)
group.visible = false
group = video
m.scene.appendChild(group)
group.setFocus(true)
group.control = "play"
ReportPlayback(group, "start")
m.overhang.visible = false
end if
2019-10-13 20:52:34 +00:00
else if isNodeEvent(msg, "search_value")
query = msg.getRoSGNode().search_value
group.findNode("SearchBox").visible = false
options = group.findNode("SearchSelect")
options.visible = true
options.setFocus(true)
results = SearchMedia(query)
options.itemData = results
options.query = query
2019-10-13 22:10:23 +00:00
else if isNodeEvent(msg, "pageSelected")
2019-10-14 16:25:05 +00:00
group.pageNumber = msg.getRoSGNode().pageSelected
collectionType = group.subType()
if collectionType = "Collections"
CollectionLister(group, m.page_size)
else if collectionType = "TVShows"
2020-02-15 17:56:56 +00:00
SeriesLister(group, m.page_size)
else if collectionType = "Channels"
ChannelLister(group, m.page_size)
end if
' TODO - abstract away the "picker" node
group.findNode("picker").setFocus(true)
2019-10-13 20:52:34 +00:00
else if isNodeEvent(msg, "itemSelected")
' Search item selected
node = getMsgPicker(msg)
group.lastFocus = group.focusedChild
2019-10-13 20:52:34 +00:00
group.setFocus(false)
group.visible = false
' TODO - swap this based on target.mediatype
2020-11-16 21:53:39 +00:00
' types: [ Series (Show), Episode, Movie, Audio, Person, Studio, MusicArtist ]
if node.type = "Series" then
group = CreateSeriesDetailsGroup(node)
else
group = CreateMovieDetailsGroup(node)
end if
2019-10-13 20:52:34 +00:00
m.scene.appendChild(group)
m.overhang.title = group.overhangTitle
else if isNodeEvent(msg, "buttonSelected")
' If a button is selected, we have some determining to do
btn = getButton(msg)
if btn.id = "play-button"
' Check is a specific Audio Stream was selected
audio_stream_idx = 1
if group.selectedAudioStreamIndex <> invalid
audio_stream_idx = group.selectedAudioStreamIndex
end if
2019-10-13 19:33:14 +00:00
' TODO - Do a better job of picking the last focus
2019-10-14 17:03:48 +00:00
' This is currently page layout Group, button Group, then button
video_id = group.id
video = CreateVideoPlayerGroup(video_id, audio_stream_idx)
2020-03-04 02:46:26 +00:00
if video <> invalid then
group.lastFocus = group.focusedChild.focusedChild.focusedChild
2020-03-04 02:46:26 +00:00
group.setFocus(false)
group.visible = false
group = video
m.scene.appendChild(group)
group.setFocus(true)
group.control = "play"
ReportPlayback(group, "start")
m.overhang.visible = false
end if
2019-10-15 01:30:10 +00:00
else if btn.id = "watched-button"
movie = group.itemContent
if movie.watched
UnmarkItemWatched(movie.id)
else
MarkItemWatched(movie.id)
end if
movie.watched = not movie.watched
else if btn.id = "favorite-button"
movie = group.itemContent
if movie.favorite
UnmarkItemFavorite(movie.id)
else
MarkItemFavorite(movie.id)
end if
movie.favorite = not movie.favorite
end if
2019-10-13 19:33:14 +00:00
else if isNodeEvent(msg, "optionSelected")
button = msg.getRoSGNode()
if button.id = "goto_search"
2019-10-13 20:52:34 +00:00
' Exit out of the side panel
panel.visible = false
if group.lastFocus <> invalid
group.lastFocus.setFocus(true)
else
group.setFocus(true)
end if
group.lastFocus = group.focusedChild
2019-10-13 20:52:34 +00:00
group.setFocus(false)
group.visible = false
2020-04-29 23:55:51 +00:00
m.overhang.showOptions = false
m.scene.unobserveField("optionsPressed")
2019-10-13 20:52:34 +00:00
group = CreateSearchPage()
m.scene.appendChild(group)
m.overhang.title = group.overhangTitle
group.findNode("SearchBox").findNode("search-input").setFocus(true)
group.findNode("SearchBox").findNode("search-input").active = true
2019-10-13 19:33:14 +00:00
else if button.id = "change_server"
unset_setting("server")
unset_setting("port")
SignOut()
wipe_groups()
goto app_start
else if button.id = "sign_out"
SignOut()
wipe_groups()
goto app_start
end if
else if isNodeEvent(msg, "selectSubtitlePressed")
node = m.scene.focusedChild
if node.isSubType("JFVideo") then
trackSelected = selectSubtitleTrack(node.Subtitles, node.SelectedSubtitle)
if trackSelected <> invalid and trackSelected <> node.SelectedSubtitle then
changeSubtitleDuringPlayback(trackSelected)
end if
end if
2020-02-17 19:30:04 +00:00
else if isNodeEvent(msg, "fire")
ReportPlayback(group, "update")
else if isNodeEvent(msg, "state")
node = msg.getRoSGNode()
if node.state = "finished" then
stopPlayback()
2020-11-30 07:46:20 +00:00
if node.showID = invalid then
RemoveCurrentGroup()
else
nextEpisode =autoPlayNextEpisode(node.id, node.showID)
if nextEpisode <> invalid then group = nextEpisode
2020-11-30 07:46:20 +00:00
end if
2020-02-17 19:30:04 +00:00
else if node.state = "playing" or node.state = "paused" then
ReportPlayback(group, "update")
end if
else if type(msg) = "roDeviceInfoEvent" then
event = msg.GetInfo()
if event.appFocused <> invalid then
child = m.scene.focusedChild
if child <> invalid and child.isSubType("JFVideo") then
child.systemOverlay = not event.appFocused
if event.AppFocused = true then
systemOverlayClosed()
end if
end if
else if event.Mute <> invalid then
m.mute = event.Mute
child = m.scene.focusedChild
if child <> invalid and child.isSubType("JFVideo") and areSubtitlesDisplayed() and child.systemOverlay = false then
'Event will be called on caption change which includes the current mute status, but we do not want to call until the overlay is closed
reviewSubtitleDisplay()
end if
else if event.exitedScreensaver = true then
m.overhang.callFunc("resetTime")
if group.subtype() = "Home" then
currentTime = CreateObject("roDateTime").AsSeconds()
group.timeLastRefresh = currentTime
group.callFunc("refresh")
end if
' todo: add other screens to be refreshed - movie detail, tv series, episode list etc.
else
print "Unhandled roDeviceInfoEvent:"
print msg.GetInfo()
end if
else
2020-11-23 17:13:57 +00:00
print "Unhandled " type(msg)
print msg
end if
end while
end sub
2020-03-22 22:40:47 +00:00
function LoginFlow(startOver = false as boolean)
if m.scene <> invalid then
m.scene.unobserveField("backPressed")
end if
2019-04-29 16:44:37 +00:00
'Collect Jellyfin server and user information
start_login:
2020-03-22 22:40:47 +00:00
if get_setting("server") = invalid or ServerInfo() = invalid or startOver = true then
print "Get server details"
SendPerformanceBeacon("AppDialogInitiate") ' Roku Performance monitoring - Dialog Starting
2020-03-22 22:40:47 +00:00
serverSelection = CreateServerGroup()
SendPerformanceBeacon("AppDialogComplete") ' Roku Performance monitoring - Dialog Closed
2020-03-22 22:40:47 +00:00
if serverSelection = "backPressed" then
print "backPressed"
wipe_groups()
return false
end if
end if
2019-01-30 04:19:27 +00:00
if get_setting("active_user") = invalid then
SendPerformanceBeacon("AppDialogInitiate") ' Roku Performance monitoring - Dialog Starting
2020-03-22 22:40:47 +00:00
publicUsers = GetPublicUsers()
if publicUsers.count() then
publicUsersNodes = []
for each item in publicUsers
user = CreateObject("roSGNode", "PublicUserData")
user.id = item.Id
2020-03-22 22:40:47 +00:00
user.name = item.Name
if item.PrimaryImageTag <> invalid then
user.ImageURL = UserImageURL(user.id, { "tag": item.PrimaryImageTag })
end if
2020-03-22 22:40:47 +00:00
publicUsersNodes.push(user)
end for
userSelected = CreateUserSelectGroup(publicUsersNodes)
m.scene.focusedChild.visible = false
2020-03-23 19:59:01 +00:00
if userSelected = "backPressed" then
SendPerformanceBeacon("AppDialogComplete") ' Roku Performance monitoring - Dialog Closed
return LoginFlow(true)
else
'Try to login without password. If the token is valid, we're done
2020-03-23 19:59:01 +00:00
get_token(userSelected, "")
if get_setting("active_user") <> invalid then
m.user = AboutMe()
LoadUserPreferences()
SendPerformanceBeacon("AppDialogComplete") ' Roku Performance monitoring - Dialog Closed
return true
end if
end if
else
userSelected = ""
end if
passwordEntry = CreateSigninGroup(userSelected)
SendPerformanceBeacon("AppDialogComplete") ' Roku Performance monitoring - Dialog Closed
2020-03-22 22:40:47 +00:00
if passwordEntry = "backPressed" then
m.scene.focusedChild.visible = false
return LoginFlow(true)
end if
end if
2019-03-02 22:03:11 +00:00
2019-03-05 04:59:31 +00:00
m.user = AboutMe()
2019-04-20 17:40:06 +00:00
if m.user = invalid or m.user.id <> get_setting("active_user")
print "Login failed, restart flow"
2019-04-20 17:40:06 +00:00
unset_setting("active_user")
goto start_login
2019-03-05 04:59:31 +00:00
end if
2019-10-13 19:33:14 +00:00
LoadUserPreferences()
2019-10-13 19:33:14 +00:00
wipe_groups()
'Send Device Profile information to server
body = getDeviceCapabilities()
req = APIRequest("/Sessions/Capabilities/Full")
req.SetRequest("POST")
postJson(req, FormatJson(body))
return true
end function
2019-10-05 07:50:05 +00:00
sub RunScreenSaver()
print "Starting screensaver..."
screen = createObject("roSGScreen")
m.port = createObject("roMessagePort")
screen.setMessagePort(m.port)
scene = screen.createScene("Screensaver")
screen.Show()
while(true)
msg = wait(8000, m.port)
if (msg <> invalid)
msgType = type(msg)
if msgType = "roSGScreenEvent"
if msg.isScreenClosed() then return
end if
end if
end while
2019-10-13 19:33:14 +00:00
end sub
sub wipe_groups()
' The 1 remaining child should be the overhang
while(m.scene.getChildCount() > 1)
m.scene.removeChildIndex(1)
end while
2020-02-15 17:56:56 +00:00
end sub
sub RemoveCurrentGroup()
' Pop a group off the stack and expose what's below
n = m.scene.getChildCount() - 1
group = m.scene.focusedChild
m.scene.removeChildIndex(n)
prevOptionsAvailable = group.optionsAvailable
group = m.scene.getChild(n - 1)
m.overhang.title = group.overhangTitle
m.overhang.showOptions = group.optionsAvailable
if group.optionsAvailable <> prevOptionsAvailable then
if group.optionsAvailable = false then
m.scene.unobserveField("optionsPressed")
else
m.scene.observeField("optionsPressed", m.port)
end if
end if
m.overhang.visible = true
if group.lastFocus <> invalid
group.lastFocus.setFocus(true)
else
group.setFocus(true)
end if
2020-03-25 02:47:26 +00:00
if group.subtype() = "Home" then
2020-03-25 04:20:37 +00:00
currentTime = CreateObject("roDateTime").AsSeconds()
if group.timeLastRefresh = invalid or (currentTime - group.timeLastRefresh) > 20 then
group.timeLastRefresh = currentTime
group.callFunc("refresh")
end if
end if
group.visible = true
end sub
' Roku Performance monitoring
sub SendPerformanceBeacon(signalName as string)
if m.global.app_loaded = false then
m.scene.signalBeacon(signalName)
end if
2021-02-01 03:10:04 +00:00
end sub