source_ShowScenes.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"
    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()
    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
          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

              if isValid(savedUser.primaryImageTag)
                user.ImageURL = UserImageURL(user.id, { "tag": savedUser.primaryImageTag })
              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")
        myPrimaryImageTag = get_user_setting("primaryimagetag")
        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")
            if isValid(myPrimaryImageTag)
              unset_user_setting("primaryimagetag")
            end if
          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")
    myPrimaryImageTag = get_user_setting("primaryimagetag")
    if isValid(myAuthToken) and isValid(myUsername)
      print "Auth token found in registry"
      session.user.Update("authToken", myAuthToken)
      session.user.Update("name", myUsername)
      if isValid(myPrimaryImageTag)
        session.user.Update("primaryImageTag", myPrimaryImageTag)
      end if

      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")
          if isValid(myPrimaryImageTag)
            unset_user_setting("primaryimagetag")
          end if
          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/branding/logo-icon120.jpg", iconWidth: 120, iconHeight: 120 }] }))
    else
      savedServers.serverList.Push({ name: m.serverSelection, baseUrl: server, iconUrl: "pkg:/images/branding/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.appLoaded = 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)
            if isValid(activeUser.json.PrimaryImageTag)
              set_user_setting("primaryimagetag", activeUser.json.PrimaryImageTag)
            end if
          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 = []

  o = CreateObject("roSGNode", "OptionsButton")
  o.title = tr("Search")
  o.id = "goto_search"
  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)
  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)

  ' 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.observeField("refreshMovieDetailsData", 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 seasonData <> invalid and m.global.session.user.settings["ui.tvshows.goStraightToEpisodeListing"] 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)

  group.observeField("refreshSeasonDetailsData", m.port)

  ' 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