function LoginFlow() 'Collect Jellyfin server and user information start_login: serverUrl = get_setting("server") if isValid(serverUrl) print "Previous server connection saved to registry" startOver = not session.server.UpdateURL(serverUrl) if startOver print "Could not connect to previously saved server." end if else startOver = true print "No previous server connection saved to registry" end if invalidServer = true if not startOver m.scene.isLoading = true invalidServer = ServerInfo().Error m.scene.isLoading = false end if m.serverSelection = "Saved" if startOver or invalidServer print "Get server details" SendPerformanceBeacon("AppDialogInitiate") ' Roku Performance monitoring - Dialog Starting m.serverSelection = CreateServerGroup() SendPerformanceBeacon("AppDialogComplete") ' Roku Performance monitoring - Dialog Closed if m.serverSelection = "backPressed" print "backPressed" m.global.sceneManager.callFunc("clearScenes") return false end if SaveServerList() end if activeUser = get_setting("active_user") if activeUser = invalid print "No active user found in registry" user_select: SendPerformanceBeacon("AppDialogInitiate") ' Roku Performance monitoring - Dialog Starting publicUsers = GetPublicUsers() savedUsers = getSavedUsers() numPubUsers = publicUsers.count() numSavedUsers = savedUsers.count() if numPubUsers > 0 or numSavedUsers > 0 publicUsersNodes = [] publicUserIds = [] ' load public users if numPubUsers > 0 for each item in publicUsers user = CreateObject("roSGNode", "PublicUserData") user.id = item.Id user.name = item.Name if isValid(item.PrimaryImageTag) user.ImageURL = UserImageURL(user.id, { "tag": item.PrimaryImageTag }) end if publicUsersNodes.push(user) publicUserIds.push(user.id) end for end if ' load saved users for this server id if numSavedUsers > 0 for each savedUser in savedUsers if isValid(savedUser.serverId) and savedUser.serverId = m.global.session.server.id ' only show unique userids on screen. if not arrayHasValue(publicUserIds, savedUser.Id) user = CreateObject("roSGNode", "PublicUserData") user.id = savedUser.Id if isValid(savedUser.username) user.name = savedUser.username end if publicUsersNodes.push(user) end if end if end for end if ' push all users to the user select view userSelected = CreateUserSelectGroup(publicUsersNodes) SendPerformanceBeacon("AppDialogComplete") ' Roku Performance monitoring - Dialog Closed if userSelected = "backPressed" session.server.Delete() unset_setting("server") goto start_login else if userSelected <> "" startLoadingSpinner() print "A public user was selected with username=" + userSelected session.user.Update("name", userSelected) ' save userid to session for each user in publicUsersNodes if user.name = userSelected session.user.Update("id", user.id) exit for end if end for ' try to login with token from registry myToken = get_user_setting("token") if myToken <> invalid ' check if token is valid print "Auth token found in registry for selected user" session.user.Update("authToken", myToken) print "Attempting to use API with auth token" currentUser = AboutMe() if currentUser = invalid print "Auth token is no longer valid - deleting token" unset_user_setting("token") unset_user_setting("username") else print "Success! Auth token is still valid" session.user.Login(currentUser, true) LoadUserAbilities() return true end if else print "No auth token found in registry for selected user" end if 'Try to login without password. If the token is valid, we're done print "Attempting to login with no password" userData = get_token(userSelected, "") if isValid(userData) print "login success!" session.user.Login(userData, true) LoadUserAbilities() return true else print "Auth failed. Password required" end if end if else userSelected = "" end if stopLoadingSpinner() passwordEntry = CreateSigninGroup(userSelected) SendPerformanceBeacon("AppDialogComplete") ' Roku Performance monitoring - Dialog Closed if passwordEntry = "backPressed" if numPubUsers > 0 goto user_select else session.server.Delete() unset_setting("server") goto start_login end if end if else print "Active user found in registry" session.user.Update("id", activeUser) myUsername = get_user_setting("username") myAuthToken = get_user_setting("token") if isValid(myAuthToken) and isValid(myUsername) print "Auth token found in registry" session.user.Update("authToken", myAuthToken) session.user.Update("name", myUsername) print "Attempting to use API with auth token" currentUser = AboutMe() if currentUser = invalid print "Auth token is no longer valid" 'Try to login without password. If the token is valid, we're done print "Attempting to login with no password" userData = get_token(myUsername, "") if isValid(userData) print "login success!" session.user.Login(userData, true) LoadUserAbilities() return true else print "Auth failed. Password required" print "delete token and restart login flow" unset_user_setting("token") unset_user_setting("username") goto start_login end if else print "Success! Auth token is still valid" session.user.Login(currentUser, true) end if else print "No auth token found in registry" end if end if if m.global.session.user.id = invalid or m.global.session.user.authToken = invalid print "Login failed, restart flow" unset_setting("active_user") session.user.Logout() goto start_login end if LoadUserAbilities() m.global.sceneManager.callFunc("clearScenes") return true end function sub SaveServerList() 'Save off this server to our list of saved servers for easier navigation between servers server = m.global.session.server.url saved = get_setting("saved_servers") if isValid(server) server = LCase(server)'Saved server data is always lowercase end if entryCount = 0 addNewEntry = true savedServers = { serverList: [] } if isValid(saved) savedServers = ParseJson(saved) entryCount = savedServers.serverList.Count() if isValid(savedServers.serverList) and entryCount > 0 for each item in savedServers.serverList if item.baseUrl = server addNewEntry = false exit for end if end for end if end if if addNewEntry if entryCount = 0 set_setting("saved_servers", FormatJson({ serverList: [{ name: m.serverSelection, baseUrl: server, iconUrl: "pkg:/images/logo-icon120.jpg", iconWidth: 120, iconHeight: 120 }] })) else savedServers.serverList.Push({ name: m.serverSelection, baseUrl: server, iconUrl: "pkg:/images/logo-icon120.jpg", iconWidth: 120, iconHeight: 120 }) set_setting("saved_servers", FormatJson(savedServers)) end if end if end sub sub DeleteFromServerList(urlToDelete) saved = get_setting("saved_servers") if isValid(urlToDelete) urlToDelete = LCase(urlToDelete) end if if isValid(saved) savedServers = ParseJson(saved) newServers = { serverList: [] } for each item in savedServers.serverList if item.baseUrl <> urlToDelete newServers.serverList.Push(item) end if end for set_setting("saved_servers", FormatJson(newServers)) end if end sub ' Roku Performance monitoring sub SendPerformanceBeacon(signalName as string) if m.global.app_loaded = false m.scene.signalBeacon(signalName) end if end sub function CreateServerGroup() screen = CreateObject("roSGNode", "SetServerScreen") screen.optionsAvailable = true m.global.sceneManager.callFunc("pushScene", screen) port = CreateObject("roMessagePort") m.colors = {} if isValid(m.global.session.server.url) screen.serverUrl = m.global.session.server.url end if m.viewModel = {} button = screen.findNode("submit") button.observeField("buttonSelected", port) 'create delete saved server option new_options = [] sidepanel = screen.findNode("options") opt = CreateObject("roSGNode", "OptionsButton") opt.title = tr("Delete Saved") opt.id = "delete_saved" opt.observeField("optionSelected", port) new_options.push(opt) sidepanel.options = new_options sidepanel.observeField("closeSidePanel", port) screen.observeField("backPressed", port) while true msg = wait(0, port) print type(msg), msg if type(msg) = "roSGScreenEvent" and msg.isScreenClosed() return "false" else if isNodeEvent(msg, "backPressed") return "backPressed" else if isNodeEvent(msg, "closeSidePanel") screen.setFocus(true) serverPicker = screen.findNode("serverPicker") serverPicker.setFocus(true) else if type(msg) = "roSGNodeEvent" node = msg.getNode() if node = "submit" m.scene.isLoading = true serverUrl = inferServerUrl(screen.serverUrl) isConnected = session.server.UpdateURL(serverUrl) serverInfoResult = invalid if isConnected set_setting("server", serverUrl) serverInfoResult = ServerInfo() 'If this is a different server from what we know, reset username/password setting if m.global.session.server.url <> serverUrl set_setting("username", "") set_setting("password", "") end if set_setting("server", serverUrl) end if m.scene.isLoading = false if isConnected = false or serverInfoResult = invalid ' Maybe don't unset setting, but offer as a prompt ' Server not found, is it online? New values / Retry print "Server not found, is it online? New values / Retry" screen.errorMessage = tr("Server not found, is it online?") SignOut(false) else if isValid(serverInfoResult.Error) and serverInfoResult.Error ' If server redirected received, update the URL if isValid(serverInfoResult.UpdatedUrl) serverUrl = serverInfoResult.UpdatedUrl isConnected = session.server.UpdateURL(serverUrl) if isConnected set_setting("server", serverUrl) screen.visible = false return "" end if end if ' Display Error Message to user message = tr("Error: ") if isValid(serverInfoResult.ErrorCode) message = message + "[" + serverInfoResult.ErrorCode.toStr() + "] " end if screen.errorMessage = message + tr(serverInfoResult.ErrorMessage) SignOut(false) else screen.visible = false if isValid(serverInfoResult.serverName) return serverInfoResult.ServerName + " (Saved)" else return "Saved" end if end if end if else if node = "delete_saved" serverPicker = screen.findNode("serverPicker") itemToDelete = serverPicker.content.getChild(serverPicker.itemFocused) urlToDelete = itemToDelete.baseUrl if isValid(urlToDelete) DeleteFromServerList(urlToDelete) serverPicker.content.removeChild(itemToDelete) sidepanel.visible = false serverPicker.setFocus(true) end if end if end if end while ' Just hide it when done, in case we need to come back screen.visible = false return "" end function function CreateUserSelectGroup(users = []) if users.count() = 0 return "" end if group = CreateObject("roSGNode", "UserSelect") m.global.sceneManager.callFunc("pushScene", group) port = CreateObject("roMessagePort") group.itemContent = users group.findNode("userRow").observeField("userSelected", port) group.findNode("alternateOptions").observeField("itemSelected", port) group.observeField("backPressed", port) while true msg = wait(0, port) if type(msg) = "roSGScreenEvent" and msg.isScreenClosed() group.visible = false return -1 else if isNodeEvent(msg, "backPressed") return "backPressed" else if type(msg) = "roSGNodeEvent" and msg.getField() = "userSelected" return msg.GetData() else if type(msg) = "roSGNodeEvent" and msg.getField() = "itemSelected" if msg.getData() = 0 return "" end if end if end while ' Just hide it when done, in case we need to come back group.visible = false return "" end function function CreateSigninGroup(user = "") ' Get and Save Jellyfin user login credentials group = CreateObject("roSGNode", "LoginScene") m.global.sceneManager.callFunc("pushScene", group) port = CreateObject("roMessagePort") group.findNode("prompt").text = tr("Sign In") config = group.findNode("configOptions") username_field = CreateObject("roSGNode", "ConfigData") username_field.label = tr("Username") username_field.field = "username" username_field.type = "string" if user = "" and get_setting("username") <> invalid username_field.value = get_setting("username") else username_field.value = user end if password_field = CreateObject("roSGNode", "ConfigData") password_field.label = tr("Password") password_field.field = "password" password_field.type = "password" registryPassword = get_setting("password") if isValid(registryPassword) password_field.value = registryPassword end if ' Add checkbox for saving credentials checkbox = group.findNode("onOff") items = CreateObject("roSGNode", "ContentNode") items.role = "content" saveCheckBox = CreateObject("roSGNode", "ContentNode") saveCheckBox.title = tr("Save Credentials?") items.appendChild(saveCheckBox) checkbox.content = items checkbox.checkedState = [true] quickConnect = group.findNode("quickConnect") ' Quick Connect only supported for server version 10.8+ right now... if versionChecker(m.global.session.server.version, "10.8.0") ' Add option for Quick Connect quickConnect.text = tr("Quick Connect") quickConnect.observeField("buttonSelected", port) else quickConnect.visible = false end if items = [username_field, password_field] config.configItems = items button = group.findNode("submit") button.observeField("buttonSelected", port) config = group.findNode("configOptions") username = config.content.getChild(0) password = config.content.getChild(1) group.observeField("backPressed", port) while true msg = wait(0, port) if type(msg) = "roSGScreenEvent" and msg.isScreenClosed() group.visible = false return "false" else if isNodeEvent(msg, "backPressed") group.unobserveField("backPressed") group.backPressed = false return "backPressed" else if type(msg) = "roSGNodeEvent" node = msg.getNode() if node = "submit" startLoadingSpinner() ' Validate credentials activeUser = get_token(username.value, password.value) if isValid(activeUser) print "activeUser=", activeUser if checkbox.checkedState[0] = true ' save credentials session.user.Login(activeUser, true) set_user_setting("token", activeUser.token) set_user_setting("username", username.value) else session.user.Login(activeUser) end if return "true" end if stopLoadingSpinner() print "Login attempt failed..." group.findNode("alert").text = tr("Login attempt failed.") else if node = "quickConnect" json = initQuickConnect() if json = invalid group.findNode("alert").text = tr("Quick Connect not available.") else ' Server user is talking to is at least 10.8 and has quick connect enabled... m.quickConnectDialog = createObject("roSGNode", "QuickConnectDialog") m.quickConnectDialog.saveCredentials = checkbox.checkedState[0] m.quickConnectDialog.quickConnectJson = json m.quickConnectDialog.title = tr("Quick Connect") m.quickConnectDialog.message = [tr("Here is your Quick Connect code: ") + json.Code, tr("(Dialog will close automatically)")] m.quickConnectDialog.buttons = [tr("Cancel")] m.quickConnectDialog.observeField("authenticated", port) m.scene.dialog = m.quickConnectDialog end if else if msg.getField() = "authenticated" authenticated = msg.getData() if authenticated = true ' Quick connect authentication was successful... return "true" else dialog = createObject("roSGNode", "Dialog") dialog.id = "QuickConnectError" dialog.title = tr("Quick Connect") dialog.buttons = [tr("OK")] dialog.message = tr("There was an error authenticating via Quick Connect.") m.scene.dialog = dialog m.scene.dialog.observeField("buttonSelected", port) end if else ' If there are no other button matches, check if this is a simple "OK" Dialog & Close if so dialog = msg.getRoSGNode() if dialog.id = "QuickConnectError" dialog.unobserveField("buttonSelected") dialog.close = true end if end if end if end while ' Just hide it when done, in case we need to come back group.visible = false return "" end function function CreateHomeGroup() ' Main screen after logging in. Shows the user's libraries group = CreateObject("roSGNode", "Home") group.overhangTitle = tr("Home") group.optionsAvailable = true group.observeField("selectedItem", m.port) group.observeField("quickPlayNode", m.port) sidepanel = group.findNode("options") sidepanel.observeField("closeSidePanel", m.port) new_options = [] options_buttons = [ { "title": "Search", "id": "goto_search" }, { "title": "Change user", "id": "change_user" }, { "title": "Change server", "id": "change_server" }, { "title": "Sign out", "id": "sign_out" } ] for each opt in options_buttons o = CreateObject("roSGNode", "OptionsButton") o.title = tr(opt.title) o.id = opt.id o.observeField("optionSelected", m.port) new_options.push(o) end for ' Add settings option to menu o = CreateObject("roSGNode", "OptionsButton") o.title = "Settings" o.id = "settings" o.observeField("optionSelected", m.port) new_options.push(o) ' And a profile button user_node = CreateObject("roSGNode", "OptionsData") user_node.id = "active_user" user_node.title = tr("Profile") user_node.base_title = tr("Profile") user_options = [] for each user in AvailableUsers() user_options.push({ display: user.username + "@" + user.server, value: user.id }) end for user_node.choices = user_options user_node.value = m.global.session.user.id new_options.push(user_node) sidepanel.options = new_options return group end function function CreateMovieDetailsGroup(movie as object) as dynamic ' validate movie node if not isValid(movie) or not isValid(movie.id) then return invalid startLoadingSpinner() ' get movie meta data movieMetaData = ItemMetaData(movie.id) ' validate movie meta data if not isValid(movieMetaData) stopLoadingSpinner() return invalid end if ' start building MovieDetails view group = CreateObject("roSGNode", "MovieDetails") group.observeField("quickPlayNode", m.port) group.overhangTitle = movie.title group.optionsAvailable = false group.trailerAvailable = false ' push scene asap (to prevent extra button presses when retriving series/movie info) m.global.sceneManager.callFunc("pushScene", group) group.itemContent = movieMetaData ' local trailers trailerData = api.users.GetLocalTrailers(m.global.session.user.id, movie.id) if isValid(trailerData) group.trailerAvailable = trailerData.Count() > 0 end if ' watch for button presses buttons = group.findNode("buttons") for each b in buttons.getChildren(-1, 0) b.observeField("buttonSelected", m.port) end for ' setup and load movie extras extras = group.findNode("extrasGrid") extras.observeField("selectedItem", m.port) extras.callFunc("loadParts", movieMetaData.json) ' done building MovieDetails view stopLoadingSpinner() return group end function function CreateSeriesDetailsGroup(seriesID as string) as dynamic ' validate series node if not isValid(seriesID) or seriesID = "" then return invalid startLoadingSpinner() ' get series meta data seriesMetaData = ItemMetaData(seriesID) ' validate series meta data if not isValid(seriesMetaData) stopLoadingSpinner() return invalid end if ' Get season data early in the function so we can check number of seasons. seasonData = TVSeasons(seriesID) ' Divert to season details if user setting goStraightToEpisodeListing is enabled and only one season exists. if m.global.session.user.settings["ui.tvshows.goStraightToEpisodeListing"] = true and seasonData.Items.Count() = 1 stopLoadingSpinner() return CreateSeasonDetailsGroupByID(seriesID, seasonData.Items[0].id) end if ' start building SeriesDetails view group = CreateObject("roSGNode", "TVShowDetails") group.optionsAvailable = false ' push scene asap (to prevent extra button presses when retriving series/movie info) m.global.sceneManager.callFunc("pushScene", group) group.itemContent = seriesMetaData group.seasonData = seasonData ' watch for button presses group.observeField("seasonSelected", m.port) group.observeField("quickPlayNode", m.port) ' setup and load series extras extras = group.findNode("extrasGrid") extras.observeField("selectedItem", m.port) extras.callFunc("loadParts", seriesMetaData.json) ' done building SeriesDetails view stopLoadingSpinner() return group end function ' Shows details on selected artist. Bio, image, and list of available albums function CreateArtistView(artist as object) as dynamic ' validate artist node if not isValid(artist) or not isValid(artist.id) then return invalid musicData = MusicAlbumList(artist.id) appearsOnData = AppearsOnList(artist.id) if (musicData = invalid or musicData.Items.Count() = 0) and (appearsOnData = invalid or appearsOnData.Items.Count() = 0) ' Just songs under artists... group = CreateObject("roSGNode", "AlbumView") group.pageContent = ItemMetaData(artist.id) ' Lookup songs based on artist id songList = GetSongsByArtist(artist.id) if not isValid(songList) ' Lookup songs based on folder parent / child relationship songList = MusicSongList(artist.id) end if if not isValid(songList) return invalid end if group.albumData = songList group.observeField("playSong", m.port) group.observeField("playAllSelected", m.port) group.observeField("instantMixSelected", m.port) else ' User has albums under artists group = CreateObject("roSGNode", "ArtistView") group.pageContent = ItemMetaData(artist.id) group.musicArtistAlbumData = musicData group.musicArtistAppearsOnData = appearsOnData group.artistOverview = ArtistOverview(artist.name) group.observeField("musicAlbumSelected", m.port) group.observeField("playArtistSelected", m.port) group.observeField("instantMixSelected", m.port) group.observeField("appearsOnSelected", m.port) end if group.observeField("quickPlayNode", m.port) m.global.sceneManager.callFunc("pushScene", group) return group end function ' Shows details on selected album. Description text, image, and list of available songs function CreateAlbumView(album as object) as dynamic ' validate album node if not isValid(album) or not isValid(album.id) then return invalid group = CreateObject("roSGNode", "AlbumView") m.global.sceneManager.callFunc("pushScene", group) group.pageContent = ItemMetaData(album.id) group.albumData = MusicSongList(album.id) ' Watch for user clicking on a song group.observeField("playSong", m.port) ' Watch for user click on Play button on album group.observeField("playAllSelected", m.port) ' Watch for user click on Instant Mix button on album group.observeField("instantMixSelected", m.port) return group end function ' Shows details on selected playlist. Description text, image, and list of available items function CreatePlaylistView(playlist as object) as dynamic ' validate playlist node if not isValid(playlist) or not isValid(playlist.id) then return invalid group = CreateObject("roSGNode", "PlaylistView") m.global.sceneManager.callFunc("pushScene", group) group.pageContent = ItemMetaData(playlist.id) group.albumData = PlaylistItemList(playlist.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 as object, season as object) as dynamic ' validate series node if not isValid(series) or not isValid(series.id) then return invalid ' validate season node if not isValid(season) or not isValid(season.id) then return invalid startLoadingSpinner() ' get season meta data seasonMetaData = ItemMetaData(season.id) ' validate season meta data if not isValid(seasonMetaData) stopLoadingSpinner() return invalid end if ' start building SeasonDetails view group = CreateObject("roSGNode", "TVEpisodes") group.optionsAvailable = false ' push scene asap (to prevent extra button presses when retriving series/movie info) m.global.sceneManager.callFunc("pushScene", group) group.seasonData = seasonMetaData.json group.objects = TVEpisodes(series.id, season.id) group.episodeObjects = group.objects group.extrasObjects = TVSeasonExtras(season.id) ' watch for button presses group.observeField("selectedItem", m.port) group.observeField("quickPlayNode", m.port) ' finished building SeasonDetails view stopLoadingSpinner() return group end function function CreateSeasonDetailsGroupByID(seriesID as string, seasonID as string) as dynamic ' validate parameters if seriesID = "" or seasonID = "" then return invalid startLoadingSpinner() ' get season meta data seasonMetaData = ItemMetaData(seasonID) ' validate season meta data if not isValid(seasonMetaData) stopLoadingSpinner() return invalid end if ' start building SeasonDetails view group = CreateObject("roSGNode", "TVEpisodes") group.optionsAvailable = false ' push scene asap (to prevent extra button presses when retriving series/movie info) group.seasonData = seasonMetaData.json group.objects = TVEpisodes(seriesID, seasonID) group.episodeObjects = group.objects ' watch for button presses group.observeField("selectedItem", m.port) group.observeField("quickPlayNode", m.port) ' don't wait for the extras button stopLoadingSpinner() m.global.sceneManager.callFunc("pushScene", group) ' check for specials/extras for this season group.extrasObjects = TVSeasonExtras(seasonID) ' finished building SeasonDetails view return group end function function CreateItemGrid(libraryItem as object) as dynamic ' validate libraryItem if not isValid(libraryItem) then return invalid group = CreateObject("roSGNode", "ItemGrid") group.parentItem = libraryItem group.optionsAvailable = true group.observeField("selectedItem", m.port) group.observeField("quickPlayNode", m.port) return group end function function CreateMovieLibraryView(libraryItem as object) as dynamic ' validate libraryItem if not isValid(libraryItem) then return invalid group = CreateObject("roSGNode", "MovieLibraryView") group.parentItem = libraryItem group.optionsAvailable = true group.observeField("selectedItem", m.port) group.observeField("quickPlayNode", m.port) return group end function function CreateMusicLibraryView(libraryItem as object) as dynamic ' validate libraryItem if not isValid(libraryItem) then return invalid group = CreateObject("roSGNode", "MusicLibraryView") group.parentItem = libraryItem group.optionsAvailable = true group.observeField("selectedItem", m.port) group.observeField("quickPlayNode", m.port) return group end function function CreateSearchPage() ' Search + Results Page group = CreateObject("roSGNode", "searchResults") group.observeField("quickPlayNode", m.port) options = group.findNode("searchSelect") options.observeField("itemSelected", m.port) return group end function function CreateVideoPlayerGroup(video_id as string, mediaSourceId = invalid as dynamic, audio_stream_idx = 1 as integer, forceTranscoding = false as boolean, showIntro = true as boolean, allowResumeDialog = true as boolean) ' validate video_id if not isValid(video_id) or video_id = "" then return invalid startLoadingSpinner() ' Video is Playing video = VideoPlayer(video_id, mediaSourceId, audio_stream_idx, defaultSubtitleTrackFromVid(video_id), forceTranscoding, showIntro, allowResumeDialog) if video = invalid then return invalid video.allowCaptions = true if video.errorMsg = "introaborted" then return video video.observeField("selectSubtitlePressed", m.port) video.observeField("selectPlaybackInfoPressed", m.port) video.observeField("state", m.port) stopLoadingSpinner() return video end function function CreatePersonView(personData as object) as dynamic ' validate personData node if not isValid(personData) or not isValid(personData.id) then return invalid startLoadingSpinner() ' get person meta data personMetaData = ItemMetaData(personData.id) ' validate season meta data if not isValid(personMetaData) stopLoadingSpinner() return invalid end if ' start building Person View person = CreateObject("roSGNode", "PersonDetails") ' push scene asap (to prevent extra button presses when retriving series/movie info) m.global.SceneManager.callFunc("pushScene", person) person.itemContent = personMetaData person.setFocus(true) ' watch for button presses person.observeField("selectedItem", m.port) person.findNode("favorite-button").observeField("buttonSelected", m.port) ' finished building Person View stopLoadingSpinner() return person end function 'Opens dialog asking user if they want to resume video or start playback over only on the home screen sub playbackOptionDialog(time as longinteger, meta as object) resumeData = [ tr("Resume playing at ") + ticksToHuman(time) + ".", tr("Start over from the beginning.") ] group = m.global.sceneManager.callFunc("getActiveScene") if LCase(group.subtype()) = "home" if LCase(meta.type) = "episode" resumeData.push(tr("Go to series")) resumeData.push(tr("Go to season")) resumeData.push(tr("Go to episode")) end if end if stopLoadingSpinner() m.global.sceneManager.callFunc("optionDialog", tr("Playback Options"), [], resumeData) end sub