2023-09-14 02:20:50 +00:00
function LoginFlow()
2023-04-22 13:03:44 +00:00
'Collect Jellyfin server and user information
start_login:
2023-06-01 12:43:27 +00:00
serverUrl = get_setting("server")
if isValid(serverUrl)
print "Previous server connection saved to registry"
2023-06-02 03:08:56 +00:00
startOver = not session.server.UpdateURL(serverUrl)
if startOver
print "Could not connect to previously saved server."
end if
2023-06-01 12:43:27 +00:00
else
startOver = true
print "No previous server connection saved to registry"
end if
2023-04-22 13:03:44 +00:00
invalidServer = true
if not startOver
' Show Connecting to Server spinner
dialog = createObject("roSGNode", "ProgressDialog")
dialog.title = tr("Connecting to Server")
m.scene.dialog = dialog
invalidServer = ServerInfo().Error
dialog.close = true
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
2023-06-01 12:43:27 +00:00
activeUser = get_setting("active_user")
if activeUser = invalid
print "No active user found in registry"
2023-09-14 02:20:50 +00:00
user_select:
2023-04-22 13:03:44 +00:00
SendPerformanceBeacon("AppDialogInitiate") ' Roku Performance monitoring - Dialog Starting
2023-09-23 21:06:36 +00:00
2023-04-22 13:03:44 +00:00
publicUsers = GetPublicUsers()
2023-09-23 21:06:36 +00:00
savedUsers = getSavedUsers()
2023-09-14 02:20:50 +00:00
numPubUsers = publicUsers.count()
2023-09-23 21:06:36 +00:00
numSavedUsers = savedUsers.count()
if numPubUsers > 0 or numSavedUsers > 0
2023-04-22 13:03:44 +00:00
publicUsersNodes = []
2023-09-23 21:06:36 +00:00
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
2023-04-22 13:03:44 +00:00
userSelected = CreateUserSelectGroup(publicUsersNodes)
2023-09-14 15:52:32 +00:00
SendPerformanceBeacon("AppDialogComplete") ' Roku Performance monitoring - Dialog Closed
2023-04-22 13:03:44 +00:00
if userSelected = "backPressed"
2023-09-14 02:20:50 +00:00
session.server.Delete()
unset_setting("server")
goto start_login
2023-11-05 01:48:39 +00:00
else if userSelected <> ""
startMediaLoadingSpinner()
2023-09-14 15:52:32 +00:00
print "A public user was selected with username=" + userSelected
2023-09-16 23:40:57 +00:00
session.user.Update("name", userSelected)
regex = CreateObject("roRegex", "[^a-zA-Z0-9\ \-\_]", "")
session.user.Update("friendlyName", regex.ReplaceAll(userSelected, ""))
2023-09-14 15:52:32 +00:00
' 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")
2023-09-16 23:40:57 +00:00
unset_user_setting("username")
2023-09-14 15:52:32 +00:00
else
print "Success! Auth token is still valid"
2023-09-23 21:06:36 +00:00
session.user.Login(currentUser, true)
2023-09-14 15:52:32 +00:00
LoadUserAbilities()
return true
end if
else
print "No auth token found in registry for selected user"
end if
2023-04-22 13:03:44 +00:00
'Try to login without password. If the token is valid, we're done
2023-09-14 15:52:32 +00:00
print "Attempting to login with no password"
2023-06-01 12:43:27 +00:00
userData = get_token(userSelected, "")
if isValid(userData)
2023-09-14 15:52:32 +00:00
print "login success!"
2023-09-23 21:06:36 +00:00
session.user.Login(userData, true)
2023-06-01 12:43:27 +00:00
LoadUserAbilities()
2023-04-22 13:03:44 +00:00
return true
2023-09-14 15:52:32 +00:00
else
print "Auth failed. Password required"
2023-04-22 13:03:44 +00:00
end if
end if
else
userSelected = ""
end if
2023-11-05 01:48:39 +00:00
stopLoadingSpinner()
2023-04-22 13:03:44 +00:00
passwordEntry = CreateSigninGroup(userSelected)
SendPerformanceBeacon("AppDialogComplete") ' Roku Performance monitoring - Dialog Closed
if passwordEntry = "backPressed"
2023-09-14 02:20:50 +00:00
if numPubUsers > 0
goto user_select
else
session.server.Delete()
unset_setting("server")
goto start_login
end if
2023-04-22 13:03:44 +00:00
end if
2023-06-01 12:43:27 +00:00
else
print "Active user found in registry"
session.user.Update("id", activeUser)
2023-09-16 23:40:57 +00:00
myUsername = get_user_setting("username")
2023-06-01 12:43:27 +00:00
myAuthToken = get_user_setting("token")
2023-09-16 23:40:57 +00:00
if isValid(myAuthToken) and isValid(myUsername)
2023-06-01 12:43:27 +00:00
print "Auth token found in registry"
session.user.Update("authToken", myAuthToken)
2023-09-16 23:40:57 +00:00
session.user.Update("name", myUsername)
regex = CreateObject("roRegex", "[^a-zA-Z0-9\ \-\_]", "")
session.user.Update("friendlyName", regex.ReplaceAll(myUsername, ""))
2023-06-01 12:43:27 +00:00
print "Attempting to use API with auth token"
currentUser = AboutMe()
if currentUser = invalid
2023-09-16 23:40:57 +00:00
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"
2023-10-26 15:27:45 +00:00
userData = get_token(myUsername, "")
2023-09-16 23:40:57 +00:00
if isValid(userData)
print "login success!"
2023-09-23 21:06:36 +00:00
session.user.Login(userData, true)
2023-09-16 23:40:57 +00:00
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
2023-06-01 12:43:27 +00:00
else
print "Success! Auth token is still valid"
2023-09-23 21:06:36 +00:00
session.user.Login(currentUser, true)
2023-06-01 12:43:27 +00:00
end if
else
print "No auth token found in registry"
end if
2023-04-22 13:03:44 +00:00
end if
2023-06-01 12:43:27 +00:00
if m.global.session.user.id = invalid or m.global.session.user.authToken = invalid
2023-04-22 13:03:44 +00:00
print "Login failed, restart flow"
unset_setting("active_user")
2023-06-01 12:43:27 +00:00
session.user.Logout()
2023-04-22 13:03:44 +00:00
goto start_login
end if
2023-06-01 12:43:27 +00:00
LoadUserAbilities()
2023-04-22 13:03:44 +00:00
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
2023-06-01 12:43:27 +00:00
server = m.global.session.server.url
2023-04-22 13:03:44 +00:00
saved = get_setting("saved_servers")
2023-06-01 12:43:27 +00:00
if isValid(server)
2023-04-22 13:03:44 +00:00
server = LCase(server)'Saved server data is always lowercase
end if
entryCount = 0
addNewEntry = true
savedServers = { serverList: [] }
2023-06-01 12:43:27 +00:00
if isValid(saved)
2023-04-22 13:03:44 +00:00
savedServers = ParseJson(saved)
entryCount = savedServers.serverList.Count()
2023-06-01 12:43:27 +00:00
if isValid(savedServers.serverList) and entryCount > 0
2023-04-22 13:03:44 +00:00
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")
2023-06-01 12:43:27 +00:00
if isValid(urlToDelete)
2023-04-22 13:03:44 +00:00
urlToDelete = LCase(urlToDelete)
end if
2023-06-01 12:43:27 +00:00
if isValid(saved)
2023-04-22 13:03:44 +00:00
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
2020-03-21 21:22:26 +00:00
function CreateServerGroup()
2021-07-09 20:08:32 +00:00
screen = CreateObject("roSGNode", "SetServerScreen")
2021-12-23 01:00:47 +00:00
screen.optionsAvailable = true
2021-10-10 02:04:37 +00:00
m.global.sceneManager.callFunc("pushScene", screen)
2021-07-09 20:08:32 +00:00
port = CreateObject("roMessagePort")
m.colors = {}
2023-06-01 12:43:27 +00:00
if isValid(m.global.session.server.url)
screen.serverUrl = m.global.session.server.url
2019-03-07 03:14:52 +00:00
end if
2021-07-09 20:08:32 +00:00
m.viewModel = {}
button = screen.findNode("submit")
button.observeField("buttonSelected", port)
2021-12-23 01:00:47 +00:00
'create delete saved server option
new_options = []
sidepanel = screen.findNode("options")
opt = CreateObject("roSGNode", "OptionsButton")
opt.title = tr("Delete Saved")
2021-12-24 04:08:43 +00:00
opt.id = "delete_saved"
2021-12-23 01:00:47 +00:00
opt.observeField("optionSelected", port)
new_options.push(opt)
sidepanel.options = new_options
sidepanel.observeField("closeSidePanel", port)
2021-12-24 04:08:43 +00:00
2021-07-09 20:08:32 +00:00
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"
2021-12-23 01:00:47 +00:00
else if isNodeEvent(msg, "closeSidePanel")
screen.setFocus(true)
serverPicker = screen.findNode("serverPicker")
serverPicker.setFocus(true)
2021-07-09 20:08:32 +00:00
else if type(msg) = "roSGNodeEvent"
node = msg.getNode()
if node = "submit"
2023-06-24 17:01:42 +00:00
' Show Connecting to Server spinner
dialog = createObject("roSGNode", "ProgressDialog")
dialog.title = tr("Connecting to Server")
m.scene.dialog = dialog
2023-09-07 00:47:22 +00:00
serverUrl = inferServerUrl(screen.serverUrl)
2021-07-09 20:08:32 +00:00
2023-06-24 17:01:42 +00:00
isConnected = session.server.UpdateURL(serverUrl)
serverInfoResult = invalid
if isConnected
2023-09-21 17:19:22 +00:00
set_setting("server", serverUrl)
2023-06-24 17:01:42 +00:00
serverInfoResult = ServerInfo()
2023-09-11 02:09:16 +00:00
'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)
2023-06-24 17:01:42 +00:00
end if
2021-07-09 20:08:32 +00:00
dialog.close = true
2023-06-24 17:01:42 +00:00
if isConnected = false or serverInfoResult = invalid
2021-07-09 20:08:32 +00:00
' 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?")
2021-12-30 01:00:13 +00:00
SignOut(false)
2021-07-09 20:08:32 +00:00
else
2023-09-11 02:16:28 +00:00
2023-06-24 17:01:42 +00:00
if isValid(serverInfoResult.Error) and serverInfoResult.Error
' If server redirected received, update the URL
if isValid(serverInfoResult.UpdatedUrl)
serverUrl = serverInfoResult.UpdatedUrl
2023-09-21 17:19:22 +00:00
2023-06-24 17:01:42 +00:00
isConnected = session.server.UpdateURL(serverUrl)
if isConnected
2023-09-21 17:19:22 +00:00
set_setting("server", serverUrl)
2023-06-24 17:01:42 +00:00
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)
2021-12-26 18:52:43 +00:00
else
2023-06-24 17:01:42 +00:00
screen.visible = false
if isValid(serverInfoResult.serverName)
return serverInfoResult.ServerName + " (Saved)"
else
return "Saved"
end if
2021-12-26 18:52:43 +00:00
end if
2021-07-09 20:08:32 +00:00
end if
2021-12-23 01:00:47 +00:00
else if node = "delete_saved"
serverPicker = screen.findNode("serverPicker")
itemToDelete = serverPicker.content.getChild(serverPicker.itemFocused)
urlToDelete = itemToDelete.baseUrl
2023-06-01 12:43:27 +00:00
if isValid(urlToDelete)
2021-12-23 01:00:47 +00:00
DeleteFromServerList(urlToDelete)
serverPicker.content.removeChild(itemToDelete)
sidepanel.visible = false
serverPicker.setFocus(true)
2021-12-24 04:08:43 +00:00
end if
2021-07-09 20:08:32 +00:00
end if
end if
end while
2019-10-13 19:33:14 +00:00
2021-07-09 20:08:32 +00:00
' Just hide it when done, in case we need to come back
screen.visible = false
return ""
2020-03-21 21:22:26 +00:00
end function
function CreateUserSelectGroup(users = [])
2021-07-09 20:08:32 +00:00
if users.count() = 0
2020-03-21 21:22:26 +00:00
return ""
end if
2021-07-09 20:08:32 +00:00
group = CreateObject("roSGNode", "UserSelect")
2021-10-10 02:04:37 +00:00
m.global.sceneManager.callFunc("pushScene", group)
2021-07-09 20:08:32 +00:00
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
2020-03-21 21:22:26 +00:00
2021-07-09 20:08:32 +00:00
' Just hide it when done, in case we need to come back
group.visible = false
return ""
2020-03-21 21:22:26 +00:00
end function
2019-03-07 03:14:52 +00:00
2020-03-21 21:22:26 +00:00
function CreateSigninGroup(user = "")
2021-07-09 20:08:32 +00:00
' Get and Save Jellyfin user login credentials
2022-05-29 20:00:38 +00:00
group = CreateObject("roSGNode", "LoginScene")
2021-10-10 02:04:37 +00:00
m.global.sceneManager.callFunc("pushScene", group)
2021-07-09 20:08:32 +00:00
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
2019-03-07 03:14:52 +00:00
end if
2021-07-09 20:08:32 +00:00
password_field = CreateObject("roSGNode", "ConfigData")
password_field.label = tr("Password")
password_field.field = "password"
password_field.type = "password"
2023-06-01 12:43:27 +00:00
registryPassword = get_setting("password")
if isValid(registryPassword)
password_field.value = registryPassword
2021-07-09 20:08:32 +00:00
end if
2021-12-30 03:51:39 +00:00
' Add checkbox for saving credentials
2021-12-30 03:55:02 +00:00
checkbox = group.findNode("onOff")
2021-12-30 03:51:39 +00:00
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]
2022-05-29 20:00:38 +00:00
quickConnect = group.findNode("quickConnect")
2022-05-31 03:00:03 +00:00
' Quick Connect only supported for server version 10.8+ right now...
2023-06-01 12:43:27 +00:00
if versionChecker(m.global.session.server.version, "10.8.0")
2022-05-31 03:00:03 +00:00
' Add option for Quick Connect
quickConnect.text = tr("Quick Connect")
quickConnect.observeField("buttonSelected", port)
else
quickConnect.visible = false
end if
2021-12-30 03:51:39 +00:00
2021-07-09 20:08:32 +00:00
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"
2023-11-05 01:48:39 +00:00
startMediaLoadingSpinner()
2021-07-09 20:08:32 +00:00
' Validate credentials
2023-06-01 12:43:27 +00:00
activeUser = get_token(username.value, password.value)
if isValid(activeUser)
2023-09-23 21:06:36 +00:00
print "activeUser=", activeUser
2021-12-30 03:51:39 +00:00
if checkbox.checkedState[0] = true
2023-09-23 21:06:36 +00:00
' save credentials
session.user.Login(activeUser, true)
2023-09-14 15:52:32 +00:00
set_user_setting("token", activeUser.token)
2023-09-16 23:40:57 +00:00
set_user_setting("username", username.value)
2023-09-23 21:06:36 +00:00
else
session.user.Login(activeUser)
2021-12-30 03:51:39 +00:00
end if
2021-07-09 20:08:32 +00:00
return "true"
end if
2023-11-05 01:48:39 +00:00
stopLoadingSpinner()
2021-07-09 20:08:32 +00:00
print "Login attempt failed..."
group.findNode("alert").text = tr("Login attempt failed.")
2022-05-29 20:00:38 +00:00
else if node = "quickConnect"
json = initQuickConnect()
if json = invalid
group.findNode("alert").text = tr("Quick Connect not available.")
2022-05-30 05:00:43 +00:00
else
' Server user is talking to is at least 10.8 and has quick connect enabled...
m.quickConnectDialog = createObject("roSGNode", "QuickConnectDialog")
2023-11-11 16:00:53 +00:00
m.quickConnectDialog.saveCredentials = checkbox.checkedState[0]
2022-05-30 05:00:43 +00:00
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
2022-05-29 20:00:38 +00:00
end if
else if msg.getField() = "authenticated"
2022-05-30 12:12:43 +00:00
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
2022-05-30 12:13:53 +00:00
end if
2021-07-09 20:08:32 +00:00
end if
end if
end while
2019-10-13 19:33:14 +00:00
2021-07-09 20:08:32 +00:00
' Just hide it when done, in case we need to come back
group.visible = false
return ""
2020-03-21 21:22:26 +00:00
end function
2019-03-07 03:14:52 +00:00
2020-03-03 23:01:13 +00:00
function CreateHomeGroup()
2021-07-09 20:08:32 +00:00
' Main screen after logging in. Shows the user's libraries
group = CreateObject("roSGNode", "Home")
2021-09-14 02:03:32 +00:00
group.overhangTitle = tr("Home")
group.optionsAvailable = true
2021-07-09 20:08:32 +00:00
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" },
2023-09-14 15:52:32 +00:00
{ "title": "Change user", "id": "change_user" },
2021-07-09 20:08:32 +00:00
{ "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
2022-05-02 06:40:00 +00:00
' Add settings option to menu
2022-05-01 10:51:28 +00:00
o = CreateObject("roSGNode", "OptionsButton")
o.title = "Settings"
o.id = "settings"
o.observeField("optionSelected", m.port)
new_options.push(o)
2021-07-09 20:08:32 +00:00
' 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
2023-06-01 12:43:27 +00:00
user_node.value = m.global.session.user.id
2021-07-09 20:08:32 +00:00
new_options.push(user_node)
sidepanel.options = new_options
return group
2019-10-12 20:21:34 +00:00
end function
2019-03-07 03:14:52 +00:00
2023-04-19 13:59:14 +00:00
function CreateMovieDetailsGroup(movie as object) as dynamic
' validate movie node
if not isValid(movie) or not isValid(movie.id) then return invalid
2023-02-04 06:39:09 +00:00
startLoadingSpinner()
2023-04-19 13:59:14 +00:00
' 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
2021-07-09 20:08:32 +00:00
group = CreateObject("roSGNode", "MovieDetails")
2023-09-16 21:18:03 +00:00
group.observeField("quickPlayNode", m.port)
2021-09-14 02:03:32 +00:00
group.overhangTitle = movie.title
group.optionsAvailable = false
2023-04-19 13:59:14 +00:00
group.trailerAvailable = false
' push scene asap (to prevent extra button presses when retriving series/movie info)
2021-10-16 20:03:10 +00:00
m.global.sceneManager.callFunc("pushScene", group)
2023-03-16 01:23:58 +00:00
group.itemContent = movieMetaData
2023-04-19 13:59:14 +00:00
' local trailers
2023-06-01 12:43:27 +00:00
trailerData = api.users.GetLocalTrailers(m.global.session.user.id, movie.id)
2022-09-03 07:31:15 +00:00
if isValid(trailerData)
group.trailerAvailable = trailerData.Count() > 0
end if
2023-04-19 13:59:14 +00:00
' watch for button presses
2021-07-09 20:08:32 +00:00
buttons = group.findNode("buttons")
for each b in buttons.getChildren(-1, 0)
b.observeField("buttonSelected", m.port)
end for
2023-04-19 13:59:14 +00:00
' setup and load movie extras
2022-03-13 08:46:03 +00:00
extras = group.findNode("extrasGrid")
extras.observeField("selectedItem", m.port)
2023-03-16 01:23:58 +00:00
extras.callFunc("loadParts", movieMetaData.json)
2023-04-19 13:59:14 +00:00
' done building MovieDetails view
2023-02-04 06:39:09 +00:00
stopLoadingSpinner()
2021-07-09 20:08:32 +00:00
return group
2019-10-12 20:21:34 +00:00
end function
2019-03-08 03:47:10 +00:00
2023-05-07 01:26:02 +00:00
function CreateSeriesDetailsGroup(seriesID as string) as dynamic
2023-04-19 13:59:14 +00:00
' validate series node
2023-05-07 01:26:02 +00:00
if not isValid(seriesID) or seriesID = "" then return invalid
2023-04-19 13:59:14 +00:00
2023-02-04 06:39:09 +00:00
startLoadingSpinner()
2023-04-19 13:59:14 +00:00
' get series meta data
2023-05-07 01:26:02 +00:00
seriesMetaData = ItemMetaData(seriesID)
2023-04-19 13:59:14 +00:00
' validate series meta data
if not isValid(seriesMetaData)
stopLoadingSpinner()
return invalid
end if
2022-12-22 23:02:13 +00:00
' Get season data early in the function so we can check number of seasons.
2023-05-07 01:26:02 +00:00
seasonData = TVSeasons(seriesID)
2022-12-22 23:02:13 +00:00
' Divert to season details if user setting goStraightToEpisodeListing is enabled and only one season exists.
2023-06-01 12:43:27 +00:00
if m.global.session.user.settings["ui.tvshows.goStraightToEpisodeListing"] = true and seasonData.Items.Count() = 1
2023-02-04 06:39:09 +00:00
stopLoadingSpinner()
2023-05-07 01:26:02 +00:00
return CreateSeasonDetailsGroupByID(seriesID, seasonData.Items[0].id)
2022-12-22 23:02:13 +00:00
end if
2023-04-19 13:59:14 +00:00
' start building SeriesDetails view
2021-07-09 20:08:32 +00:00
group = CreateObject("roSGNode", "TVShowDetails")
2021-09-14 02:03:32 +00:00
group.optionsAvailable = false
2023-04-19 13:59:14 +00:00
' push scene asap (to prevent extra button presses when retriving series/movie info)
2021-10-16 20:03:10 +00:00
m.global.sceneManager.callFunc("pushScene", group)
2023-04-19 13:59:14 +00:00
group.itemContent = seriesMetaData
group.seasonData = seasonData
' watch for button presses
2021-07-09 20:08:32 +00:00
group.observeField("seasonSelected", m.port)
2023-09-16 21:18:03 +00:00
group.observeField("quickPlayNode", m.port)
2023-04-19 13:59:14 +00:00
' setup and load series extras
2022-03-13 08:46:03 +00:00
extras = group.findNode("extrasGrid")
extras.observeField("selectedItem", m.port)
2023-04-19 13:59:14 +00:00
extras.callFunc("loadParts", seriesMetaData.json)
' done building SeriesDetails view
2023-02-04 06:39:09 +00:00
stopLoadingSpinner()
2021-07-09 20:08:32 +00:00
return group
2019-12-07 02:49:37 +00:00
end function
2019-04-14 04:47:27 +00:00
2022-05-14 22:30:22 +00:00
' Shows details on selected artist. Bio, image, and list of available albums
2023-04-19 13:59:14 +00:00
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)
2022-05-15 04:14:42 +00:00
2022-09-27 01:26:17 +00:00
if (musicData = invalid or musicData.Items.Count() = 0) and (appearsOnData = invalid or appearsOnData.Items.Count() = 0)
2022-05-15 04:14:42 +00:00
' Just songs under artists...
2022-07-19 00:42:22 +00:00
group = CreateObject("roSGNode", "AlbumView")
2023-04-19 13:59:14 +00:00
group.pageContent = ItemMetaData(artist.id)
2022-10-02 18:23:42 +00:00
' Lookup songs based on artist id
2023-04-19 13:59:14 +00:00
songList = GetSongsByArtist(artist.id)
2022-10-02 18:23:42 +00:00
if not isValid(songList)
' Lookup songs based on folder parent / child relationship
2023-04-19 13:59:14 +00:00
songList = MusicSongList(artist.id)
2022-10-02 18:23:42 +00:00
end if
if not isValid(songList)
return invalid
end if
group.albumData = songList
2022-05-21 20:45:01 +00:00
group.observeField("playSong", m.port)
2022-05-22 02:04:41 +00:00
group.observeField("playAllSelected", m.port)
2022-06-08 13:08:05 +00:00
group.observeField("instantMixSelected", m.port)
2022-05-15 04:14:42 +00:00
else
2022-05-22 00:03:24 +00:00
' User has albums under artists
2022-07-19 00:42:22 +00:00
group = CreateObject("roSGNode", "ArtistView")
2023-04-19 13:59:14 +00:00
group.pageContent = ItemMetaData(artist.id)
2022-05-15 12:30:55 +00:00
group.musicArtistAlbumData = musicData
2022-09-27 01:26:17 +00:00
group.musicArtistAppearsOnData = appearsOnData
2023-04-19 13:59:14 +00:00
group.artistOverview = ArtistOverview(artist.name)
2022-09-27 01:26:17 +00:00
2022-05-15 04:14:42 +00:00
group.observeField("musicAlbumSelected", m.port)
2022-07-19 02:28:06 +00:00
group.observeField("playArtistSelected", m.port)
group.observeField("instantMixSelected", m.port)
2022-09-27 01:26:17 +00:00
group.observeField("appearsOnSelected", m.port)
2022-05-15 04:14:42 +00:00
end if
2022-05-14 02:35:50 +00:00
2023-09-16 21:18:03 +00:00
group.observeField("quickPlayNode", m.port)
2022-05-15 04:14:42 +00:00
m.global.sceneManager.callFunc("pushScene", group)
2022-05-14 03:46:05 +00:00
return group
end function
2022-05-14 22:30:22 +00:00
' Shows details on selected album. Description text, image, and list of available songs
2023-04-19 13:59:14 +00:00
function CreateAlbumView(album as object) as dynamic
' validate album node
if not isValid(album) or not isValid(album.id) then return invalid
2022-07-19 00:42:22 +00:00
group = CreateObject("roSGNode", "AlbumView")
2022-05-14 03:46:05 +00:00
m.global.sceneManager.callFunc("pushScene", group)
2022-05-21 20:45:01 +00:00
group.pageContent = ItemMetaData(album.id)
group.albumData = MusicSongList(album.id)
2022-05-14 03:46:05 +00:00
2022-05-15 02:30:29 +00:00
' Watch for user clicking on a song
2022-05-21 20:45:01 +00:00
group.observeField("playSong", m.port)
2022-05-14 02:35:50 +00:00
2022-05-15 19:10:21 +00:00
' Watch for user click on Play button on album
group.observeField("playAllSelected", m.port)
2022-06-08 13:08:05 +00:00
' Watch for user click on Instant Mix button on album
group.observeField("instantMixSelected", m.port)
2022-05-14 02:35:50 +00:00
return group
end function
2023-02-25 16:43:36 +00:00
' Shows details on selected playlist. Description text, image, and list of available items
2023-04-19 13:59:14 +00:00
function CreatePlaylistView(playlist as object) as dynamic
' validate playlist node
if not isValid(playlist) or not isValid(playlist.id) then return invalid
2023-02-25 16:43:36 +00:00
group = CreateObject("roSGNode", "PlaylistView")
m.global.sceneManager.callFunc("pushScene", group)
2023-04-19 13:59:14 +00:00
group.pageContent = ItemMetaData(playlist.id)
group.albumData = PlaylistItemList(playlist.id)
2023-02-25 16:43:36 +00:00
' 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
2023-04-19 13:59:14 +00:00
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
2023-02-04 06:39:09 +00:00
startLoadingSpinner()
2023-04-19 13:59:14 +00:00
' 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
2021-07-09 20:08:32 +00:00
group = CreateObject("roSGNode", "TVEpisodes")
2021-09-14 02:03:32 +00:00
group.optionsAvailable = false
2023-04-19 13:59:14 +00:00
' push scene asap (to prevent extra button presses when retriving series/movie info)
2021-10-16 20:03:10 +00:00
m.global.sceneManager.callFunc("pushScene", group)
2023-04-19 13:59:14 +00:00
group.seasonData = seasonMetaData.json
2021-07-09 20:08:32 +00:00
group.objects = TVEpisodes(series.id, season.id)
2023-10-15 02:21:08 +00:00
group.episodeObjects = group.objects
group.extrasObjects = TVSeasonExtras(season.id)
2023-04-19 13:59:14 +00:00
' watch for button presses
2023-11-18 14:38:36 +00:00
group.observeField("selectedItem", m.port)
2021-07-09 20:08:32 +00:00
group.observeField("quickPlayNode", m.port)
2023-04-19 13:59:14 +00:00
' finished building SeasonDetails view
2023-02-04 06:18:32 +00:00
stopLoadingSpinner()
2021-07-09 20:08:32 +00:00
return group
2019-12-07 02:49:37 +00:00
end function
2019-03-11 01:24:50 +00:00
2023-04-19 13:59:14 +00:00
function CreateSeasonDetailsGroupByID(seriesID as string, seasonID as string) as dynamic
' validate parameters
if seriesID = "" or seasonID = "" then return invalid
2023-02-04 06:39:09 +00:00
startLoadingSpinner()
2023-04-19 13:59:14 +00:00
' get season meta data
seasonMetaData = ItemMetaData(seasonID)
' validate season meta data
if not isValid(seasonMetaData)
stopLoadingSpinner()
return invalid
end if
' start building SeasonDetails view
2022-11-05 00:37:54 +00:00
group = CreateObject("roSGNode", "TVEpisodes")
group.optionsAvailable = false
2023-04-19 13:59:14 +00:00
' push scene asap (to prevent extra button presses when retriving series/movie info)
2022-11-05 00:37:54 +00:00
m.global.sceneManager.callFunc("pushScene", group)
2023-04-19 13:59:14 +00:00
group.seasonData = seasonMetaData.json
2022-11-05 00:37:54 +00:00
group.objects = TVEpisodes(seriesID, seasonID)
2023-10-15 02:21:08 +00:00
group.episodeObjects = group.objects
group.extrasObjects = TVSeasonExtras(seasonID)
2023-04-19 13:59:14 +00:00
' watch for button presses
2022-11-05 00:37:54 +00:00
group.observeField("episodeSelected", m.port)
group.observeField("quickPlayNode", m.port)
2023-04-19 13:59:14 +00:00
' finished building SeasonDetails view
2023-02-04 06:39:09 +00:00
stopLoadingSpinner()
2022-11-05 00:37:54 +00:00
return group
end function
2023-04-19 13:59:14 +00:00
function CreateItemGrid(libraryItem as object) as dynamic
' validate libraryItem
if not isValid(libraryItem) then return invalid
2021-07-09 20:08:32 +00:00
group = CreateObject("roSGNode", "ItemGrid")
group.parentItem = libraryItem
2021-09-14 02:03:32 +00:00
group.optionsAvailable = true
2021-07-09 20:08:32 +00:00
group.observeField("selectedItem", m.port)
2023-09-15 20:16:26 +00:00
group.observeField("quickPlayNode", m.port)
2021-07-09 20:08:32 +00:00
return group
2020-05-31 13:46:33 +00:00
end function
2023-04-19 13:59:14 +00:00
function CreateMovieLibraryView(libraryItem as object) as dynamic
' validate libraryItem
if not isValid(libraryItem) then return invalid
2022-09-24 00:16:52 +00:00
group = CreateObject("roSGNode", "MovieLibraryView")
group.parentItem = libraryItem
group.optionsAvailable = true
2021-07-09 20:08:32 +00:00
group.observeField("selectedItem", m.port)
2023-09-15 20:16:26 +00:00
group.observeField("quickPlayNode", m.port)
2021-07-09 20:08:32 +00:00
return group
2020-05-31 13:46:33 +00:00
end function
2023-04-19 13:59:14 +00:00
function CreateMusicLibraryView(libraryItem as object) as dynamic
' validate libraryItem
if not isValid(libraryItem) then return invalid
2022-12-10 05:06:56 +00:00
group = CreateObject("roSGNode", "MusicLibraryView")
group.parentItem = libraryItem
group.optionsAvailable = true
group.observeField("selectedItem", m.port)
2023-09-16 21:18:03 +00:00
group.observeField("quickPlayNode", m.port)
2021-07-09 20:08:32 +00:00
return group
2020-05-31 13:46:33 +00:00
end function
2019-10-13 20:52:34 +00:00
function CreateSearchPage()
2021-07-09 20:08:32 +00:00
' Search + Results Page
2022-09-05 06:50:13 +00:00
group = CreateObject("roSGNode", "searchResults")
2023-10-29 02:36:35 +00:00
group.observeField("quickPlayNode", m.port)
2022-09-05 06:50:13 +00:00
options = group.findNode("searchSelect")
2021-07-09 20:08:32 +00:00
options.observeField("itemSelected", m.port)
2019-03-14 22:50:20 +00:00
2021-07-09 20:08:32 +00:00
return group
2019-10-13 20:52:34 +00:00
end function
2019-03-14 22:50:20 +00:00
2023-04-19 13:59:14 +00:00
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
2019-10-13 22:10:23 +00:00
2023-02-04 06:39:09 +00:00
startMediaLoadingSpinner()
2021-07-09 20:08:32 +00:00
' Video is Playing
2022-12-08 18:33:08 +00:00
video = VideoPlayer(video_id, mediaSourceId, audio_stream_idx, defaultSubtitleTrackFromVid(video_id), forceTranscoding, showIntro, allowResumeDialog)
2022-07-13 06:51:47 +00:00
2021-07-09 20:08:32 +00:00
if video = invalid then return invalid
2023-03-26 16:13:55 +00:00
video.allowCaptions = true
2022-07-09 08:28:15 +00:00
if video.errorMsg = "introaborted" then return video
2021-07-09 20:08:32 +00:00
video.observeField("selectSubtitlePressed", m.port)
2022-09-06 04:38:37 +00:00
video.observeField("selectPlaybackInfoPressed", m.port)
2021-07-09 20:08:32 +00:00
video.observeField("state", m.port)
2023-02-04 06:39:09 +00:00
stopLoadingSpinner()
2021-07-09 20:08:32 +00:00
return video
2019-10-12 21:00:07 +00:00
end function
2021-12-24 04:07:35 +00:00
2023-04-19 13:59:14 +00:00
function CreatePersonView(personData as object) as dynamic
' validate personData node
if not isValid(personData) or not isValid(personData.id) then return invalid
2023-02-04 06:39:09 +00:00
startLoadingSpinner()
2023-04-19 13:59:14 +00:00
' 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
2022-03-13 08:46:03 +00:00
person = CreateObject("roSGNode", "PersonDetails")
2023-04-19 13:59:14 +00:00
' push scene asap (to prevent extra button presses when retriving series/movie info)
2022-03-13 08:46:03 +00:00
m.global.SceneManager.callFunc("pushScene", person)
2023-04-19 13:59:14 +00:00
person.itemContent = personMetaData
2022-03-13 08:46:03 +00:00
person.setFocus(true)
2023-04-19 13:59:14 +00:00
' watch for button presses
2022-03-13 08:46:03 +00:00
person.observeField("selectedItem", m.port)
person.findNode("favorite-button").observeField("buttonSelected", m.port)
2023-04-19 13:59:14 +00:00
' finished building Person View
stopLoadingSpinner()
2022-03-13 08:46:03 +00:00
return person
end function
2023-05-07 01:26:02 +00:00
'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
m.global.sceneManager.callFunc("optionDialog", tr("Playback Options"), [], resumeData)
end sub