source_ShowScenes.bs

import "pkg:/source/api/ApiClient.bs"

function LoginFlow()
  'Collect Jellyfin server and user information
  start_login:

  serverUrl = get_setting("server")
  if isValid(serverUrl)
    print "Previous server connection saved to registry"
    ' Pass originalUrl to preserve user's input for re-discovery on each connection
    startOver = not server.UpdateURL(serverUrl, 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"
  ' Always ensure server select is on the stack before user select (for back button)
  if startOver or invalidServer
    ' Need to show server select interactively
    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()
  else
    ' Server is valid - push placeholder to maintain consistent stack depth
    ' This ensures the scene stack has the same depth regardless of whether
    ' server selection UI was shown, preventing stack corruption on cleanup
    ' Using Group because it has a visible field (ContentNode does not)
    print "Server valid, pushing placeholder to stack"
    placeholderNode = CreateObject("roSGNode", "Group")
    placeholderNode.visible = false
    m.global.sceneManager.callFunc("pushScene", placeholderNode)
  end if

  localUser = m.global.user

  activeUser = get_setting("active_user")
  if not isValid(activeUser)
    print "No active user found in registry"
    user_select:
    SendPerformanceBeacon("AppDialogInitiate") ' Roku Performance monitoring - Dialog Starting

    publicUsers = GetPublicUsers()
    numPubUsers = 0
    if isValid(publicUsers) then numPubUsers = publicUsers.count()

    savedUsers = getSavedUsers()
    numSavedUsers = savedUsers.count()

    if numPubUsers > 0 or numSavedUsers > 0
      publicUsersNodes = []
      publicUserIds = []
      ' load public users
      if numPubUsers > 0
        for each item in publicUsers
          userData = CreateObject("roSGNode", "PublicUserData")
          userData.id = item.Id
          userData.name = item.Name
          if isValidAndNotEmpty(item.PrimaryImageTag)
            userData.ImageURL = UserImageURL(userData.id, { "tag": item.PrimaryImageTag })
          end if
          publicUsersNodes.push(userData)
          publicUserIds.push(userData.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.server.id
            ' only show unique userids on screen.
            if not arrayHasValue(publicUserIds, savedUser.Id)
              userData = CreateObject("roSGNode", "PublicUserData")
              userData.id = savedUser.Id

              if isValid(savedUser.username)
                userData.name = savedUser.username
              end if

              if isValidAndNotEmpty(savedUser.primaryImageTag)
                userData.ImageURL = UserImageURL(userData.id, { "tag": savedUser.primaryImageTag })
              end if

              publicUsersNodes.push(userData)
            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"
        ' User wants to change server - clear all scenes and restart
        server.Delete()
        unset_setting("server")
        m.global.sceneManager.callFunc("clearScenes")
        goto start_login
      else if userSelected <> ""
        startLoadingSpinner()
        print "A public user was selected with username=" + userSelected
        localUser.name = userSelected

        ' save userid to session
        for each userData in publicUsersNodes
          if userData.name = userSelected
            localUser.id = userData.id
            exit for
          end if
        end for
        ' try to login with token from registry
        myToken = get_user_setting("authToken")
        if isValid(myToken)
          ' check if token is valid
          print "Auth token found in registry for selected user"
          localUser.authToken = myToken
          print "Attempting to use API with auth token"
          currentUser = AboutMe()
          if not isValid(currentUser)
            print "Auth token is no longer valid - deleting token"
            unset_user_setting("authToken")
            unset_user_setting("username")
            unset_user_setting("primaryImageTag")
          else
            print "Success! Auth token is still valid"
            user.Login(currentUser, true)
            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!"
          user.Login(userData, true)
          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
        server.Delete()
        unset_setting("server")
        goto start_login
      end if
    end if
  else
    print "Active user found in registry"
    localUser.id = activeUser

    myUsername = get_user_setting("username")
    myAuthToken = get_user_setting("authToken")
    myPrimaryImageTag = get_user_setting("primaryImageTag")
    if isValid(myAuthToken) and isValid(myUsername)
      print "Auth token found in registry"
      localUser.authToken = myAuthToken
      localUser.name = myUsername
      if isValidAndNotEmpty(myPrimaryImageTag)
        localUser.primaryImageTag = myPrimaryImageTag
      end if
      print "Attempting to use API with auth token"
      currentUser = AboutMe()
      if not isValid(currentUser)
        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!"
          user.Login(userData, true)
          return true
        else
          print "Auth failed. Password required"
          print "delete token and restart login flow"
          unset_user_setting("authToken")
          unset_user_setting("username")
          if isValid(myPrimaryImageTag)
            unset_user_setting("primaryImageTag")
          end if
          goto start_login
        end if
      else
        print "Success! Auth token is still valid"
        user.Login(currentUser, true)
      end if
    else
      print "No auth token found in registry"
    end if
  end if

  if not isValid(localUser.id) or not isValid(localUser.authToken)
    print "Login failed, restart flow"
    unset_setting("active_user")
    user.Logout()
    goto start_login
  end if

  m.global.sceneManager.callFunc("clearScenes")

  return true
end function

sub SaveServerList()
  ' Save this server to the list of previously-used servers shown in the server picker.
  ' baseUrl: canonical URL (lowercase) — used for deduplication against SSDP-discovered servers.
  ' originalUrl: user-entered URL from registry — shown in the picker and pre-filled on re-selection,
  '              so inferServerUrl() can re-discover the correct protocol on each connection.
  ' id: Jellyfin server ID — primary deduplication key, robust to URL changes (e.g. HTTP→HTTPS).
  globalServer = m.global.server
  serverUrl = globalServer.serverUrl
  serverId = globalServer.id
  serverName = globalServer.name
  originalUrl = get_setting("server") ' set correctly by server.UpdateURL() before this is called

  if isValid(serverUrl)
    serverUrl = LCase(serverUrl) ' canonical URL always lowercase for comparison
  end if
  if not isValidAndNotEmpty(originalUrl)
    originalUrl = serverUrl ' fallback: use canonical if original is somehow missing
  end if

  savedServers = { serverList: [] }
  saved = get_setting("saved_servers")
  if isValid(saved)
    parsed = ParseJson(saved)
    if isValid(parsed) and isValid(parsed.serverList)
      savedServers = parsed
    end if
  end if

  ' Check for an existing entry (ID-based first; URL fallback for old entries without id)
  for i = 0 to savedServers.serverList.Count() - 1
    item = savedServers.serverList[i]
    isMatch = false
    if isValidAndNotEmpty(serverId) and isValidAndNotEmpty(item.id)
      isMatch = (item.id = serverId)
    else if LCase(item.baseUrl) = serverUrl
      isMatch = true
    end if

    if isMatch
      ' Update in-place: refresh mutable server identity fields (name, id, baseUrl, originalUrl).
      ' iconUrl/iconWidth/iconHeight are static app branding defaults and are not changed.
      savedServers.serverList[i].name = serverName
      savedServers.serverList[i].id = serverId
      savedServers.serverList[i].baseUrl = serverUrl ' keep canonical URL current (e.g. HTTP→HTTPS)
      savedServers.serverList[i].originalUrl = originalUrl
      set_setting("saved_servers", FormatJson(savedServers))
      return
    end if
  end for

  ' No existing entry found — append a new one
  savedServers.serverList.Push({
    name: serverName,
    id: serverId,
    baseUrl: serverUrl,
    originalUrl: originalUrl,
    iconUrl: "pkg:/images/branding/logo-icon120.jpg",
    iconWidth: 120,
    iconHeight: 120
  })
  set_setting("saved_servers", FormatJson(savedServers))
end sub

sub DeleteFromServerList(idOrUrl as string)
  ' idOrUrl should be the server's id when available (passed from itemToDelete.id).
  ' Falls back to a canonical baseUrl for legacy entries that predate the id field.
  ' ID match is tried first so deletion is correct even when the saved entry's baseUrl
  ' differs from the picker item's baseUrl (e.g. a saved HTTPS entry matched via SSDP
  ' on HTTP — the picker item carries the SSDP baseUrl, not the saved one).
  saved = get_setting("saved_servers")
  if not isValid(saved) then return

  savedServers = ParseJson(saved)
  newServers = { serverList: [] }
  normalizedInput = LCase(idOrUrl) ' for URL fallback comparison (baseUrls are always lowercase)
  for each item in savedServers.serverList
    keepEntry = true
    if isValidAndNotEmpty(item.id) and item.id = idOrUrl
      keepEntry = false ' ID match — remove this entry
    else if item.baseUrl = normalizedInput
      keepEntry = false ' URL fallback — remove this entry
    end if
    if keepEntry
      newServers.serverList.Push(item)
    end if
  end for
  set_setting("saved_servers", FormatJson(newServers))
end sub

' Roku Performance monitoring
sub SendPerformanceBeacon(signalName as string)
  if m.global.appLoaded = false
    m.scene.signalBeacon(signalName)
  end if
end sub

function CreateServerGroup()
  globalServer = m.global.server
  ' Capture the URL before any updates to detect changes during discovery/redirects
  previousServerUrl = globalServer.serverUrl
  screen = CreateObject("roSGNode", "SetServerScreen")
  screen.optionsAvailable = true
  m.global.sceneManager.callFunc("pushScene", screen)
  port = CreateObject("roMessagePort")

  if isValid(globalServer.serverUrl)
    screen.serverUrl = globalServer.serverUrl
  end if

  buttons = screen.findNode("buttons")
  buttons.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"
      nodeName = msg.getNode()

      ' print "roSGNodeEvent: msg.getNode() =", msg.getNode()
      ' print "roSGNodeEvent: msg.getData() =", msg.getData()
      ' print "roSGNodeEvent: msg.getField() =", msg.getField()
      ' print "roSGNodeEvent: msg.getRoSGNode() =", msg.getRoSGNode()
      ' print "roSGNodeEvent: msg.getInfo() =", msg.getInfo()
      if nodeName = "buttons"
        buttonIndex = msg.getData()
        buttonGroup = msg.getRoSGNode()
        buttonSelected = buttonGroup.getChild(buttonIndex)

        if buttonSelected.id = "submit"
          m.scene.isLoading = true

          originalUrl = screen.serverUrl
          serverUrl = inferServerUrl(originalUrl)

          ' Pass originalUrl so it gets persisted (not the canonical redirect URL)
          ' This allows re-discovery on each connection for multi-network access
          isConnected = server.UpdateURL(serverUrl, originalUrl)
          serverInfoResult = invalid
          if isConnected
            serverInfoResult = ServerInfo()
            'If this is a different server from what we know, reset username/password setting
            ' Compare using canonical URL from global state against pre-connection URL
            canonicalUrl = m.global.server.serverUrl
            if previousServerUrl <> canonicalUrl
              set_setting("username", "")
              set_setting("password", "")
            end if
          end if
          m.scene.isLoading = false

          if isConnected = false or not isValid(serverInfoResult) or (isValid(serverInfoResult.Error) and serverInfoResult.Error)
            ' 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
            screen.visible = false
            if isValid(serverInfoResult.serverName)
              return serverInfoResult.ServerName + " (Saved)"
            else
              return "Saved"
            end if
          end if
        end if
      else if nodeName = "delete_saved"
        serverPicker = screen.findNode("serverPicker")
        itemToDelete = serverPicker.content.getChild(serverPicker.itemFocused)
        ' Prefer id for deletion — robust when the picker item's baseUrl differs from
        ' the saved entry's baseUrl (e.g. SSDP HTTP item merged from an HTTPS saved entry)
        idOrUrl = itemToDelete.id
        if not isValidAndNotEmpty(idOrUrl)
          idOrUrl = itemToDelete.baseUrl
        end if
        if isValidAndNotEmpty(idOrUrl)
          DeleteFromServerList(idOrUrl)
          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("buttons").observeField("buttonSelected", port)
  group.observeField("backPressed", port)
  while true
    msg = wait(0, port)
    ' print "roSGNodeEvent: msg.getNode() =", msg.getNode()
    ' print "roSGNodeEvent: msg.getData() =", msg.getData()
    ' print "roSGNodeEvent: msg.getField() =", msg.getField()
    ' print "roSGNodeEvent: msg.getRoSGNode() =", msg.getRoSGNode()
    ' print "roSGNodeEvent: msg.getInfo() =", msg.getInfo()
    if type(msg) = "roSGScreenEvent" and msg.isScreenClosed()
      group.visible = false
      return -1
    else if isNodeEvent(msg, "backPressed")
      group.visible = false
      return "backPressed"
    else if type(msg) = "roSGNodeEvent" and msg.getField() = "userSelected"
      return msg.GetData()
    else if type(msg) = "roSGNodeEvent" and msg.getField() = "buttonSelected"
      ' Manual Login button pressed
      if msg.getData() = 0 ' button index
        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(userName = "")
  ' 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 userName = "" and isValid(get_setting("username"))
    username_field.value = get_setting("username")
  else
    username_field.value = userName
  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
  quickConnect = group.findNode("quickConnect")
  ' Quick Connect only supported for server version 10.8+ right now...
  if versionChecker(m.global.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

  buttons = group.findNode("buttons")
  buttons.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)
    ' print "roSGNodeEvent: msg.getNode() =", msg.getNode()
    ' print "roSGNodeEvent: msg.getData() =", msg.getData()
    ' print "roSGNodeEvent: msg.getField() =", msg.getField()
    ' print "roSGNodeEvent: msg.getRoSGNode() =", msg.getRoSGNode()
    ' print "roSGNodeEvent: msg.getInfo() =", msg.getInfo()
    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"
      nodeName = msg.getNode()
      if nodeName = "buttons"
        buttonIndex = msg.getData()
        buttonGroup = msg.getRoSGNode()
        buttonSelected = buttonGroup.getChild(buttonIndex)

        if buttonSelected.id = "submit"
          startLoadingSpinner()
          ' Validate credentials
          activeUser = get_token(userName.value, password.value)
          if isValid(activeUser)
            print "activeUser=", activeUser
            if checkbox.checkedState[0] = true
              ' save credentials
              user.Login(activeUser, true)
              set_user_setting("authToken", activeUser.token)
              set_user_setting("username", userName.value)
              if isValidAndNotEmpty(activeUser.json.PrimaryImageTag)
                set_user_setting("primaryImageTag", activeUser.json.PrimaryImageTag)
              end if
            else
              user.Login(activeUser)
            end if
            return "true"
          end if
          stopLoadingSpinner()
          print "Login attempt failed..."
          group.findNode("alert").text = tr("Login attempt failed.")
        else if buttonSelected.id = "quickConnect"
          json = initQuickConnect()
          if not isValid(json)
            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
        end if

        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 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 = []

  o = CreateObject("roSGNode", "OptionsButton")
  o.title = tr("Search")
  o.id = "goto_search"
  o.observeField("optionSelected", m.port)
  new_options.push(o)
  o = invalid

  ' Add settings option to menu
  o = CreateObject("roSGNode", "OptionsButton")
  o.title = tr("Settings")
  o.id = "settings"
  o.observeField("optionSelected", m.port)
  new_options.push(o)
  o = invalid

  o = CreateObject("roSGNode", "OptionsButton")
  o.title = tr("Change user")
  o.id = "change_user"
  o.observeField("optionSelected", m.port)
  new_options.push(o)
  o = invalid

  o = CreateObject("roSGNode", "OptionsButton")
  o.title = tr("Change server")
  o.id = "change_server"
  o.observeField("optionSelected", m.port)
  new_options.push(o)
  o = invalid

  o = CreateObject("roSGNode", "OptionsButton")
  o.title = tr("Sign out")
  o.id = "sign_out"
  o.observeField("optionSelected", m.port)
  new_options.push(o)

  sidepanel.options = new_options

  return group
end function

function CreateItemDetailsGroup(item as object) as dynamic
  if not isValid(item) or not isValid(item.id) then return invalid

  startLoadingSpinner()

  itemData = ItemDetailsMetaData(item.id)
  if not isValid(itemData)
    stopLoadingSpinner()
    return invalid
  end if

  group = CreateObject("roSGNode", "ItemDetails")
  group.observeField("quickPlayNode", m.port)
  group.observeField("refreshItemDetailsData", m.port)
  group.overhangTitle = ""
  group.optionsAvailable = false
  group.trailerAvailable = false

  ' Push scene asap to prevent extra button presses while fetching data
  m.global.sceneManager.callFunc("pushScene", group)
  group.itemContent = itemData

  ' Type-specific post-load setup
  if item.type = "Movie" or item.type = "Video" or item.type = "MusicVideo"
    trailerData = GetApi().GetLocalTrailers(item.id)
    if isValid(trailerData)
      group.trailerAvailable = trailerData.Count() > 0
    end if
  end if

  ' Watch for button presses
  buttons = group.findNode("buttons")
  buttons.observeField("buttonSelected", m.port)

  ' Set up and load item extras
  extras = group.findNode("extrasGrid")
  extras.observeField("selectedItem", m.port)
  ' type and loadParts are called once by itemContentChanged() in ItemDetails.bs on first content load,
  ' and again on explicit refresh via the refreshExtrasData field toggle.

  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 (not isValid(musicData) or musicData.Items.Count() = 0) and (not isValid(appearsOnData) 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)

  buttons = group.findNode("buttons")
  buttons.observeField("buttonSelected", 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)

  buttons = group.findNode("buttons")
  buttons.observeField("buttonSelected", 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

'Opens dialog asking user if they want to resume video or start playback over only on the home screen
sub playbackOptionDialog(time as longinteger)

  resumeData = [
    tr("Resume playing at ") + ticksToHuman(time) + ".",
    tr("Start over from the beginning.")
  ]

  stopLoadingSpinner()
  m.global.sceneManager.callFunc("optionDialog", tr("Playback Options"), [], resumeData)
end sub