source_Main.bs

sub Main (args as dynamic) as void
  printRegistry()
  ' The main function that runs when the application is launched.
  m.screen = CreateObject("roSGScreen")
  m.port = CreateObject("roMessagePort")
  m.screen.setMessagePort(m.port)

  ' Write screen tracker for screensaver
  WriteAsciiFile("tmp:/scene.temp", "")
  MoveFile("tmp:/scene.temp", "tmp:/scene")

  m.global = m.screen.getGlobalNode()
  setGlobals()

  session.Init()

  ' migrate registry if needed
  m.wasMigrated = false
  runGlobalMigrations()
  runRegistryUserMigrations()
  ' update LastRunVersion now that migrations are finished
  if m.global.app.version <> m.global.app.lastRunVersion
    set_setting("LastRunVersion", m.global.app.version)
  end if
  if m.wasMigrated then printRegistry()

  m.scene = m.screen.CreateScene("JRScene")
  m.screen.show() ' vscode_rale_tracker_entry
  'vscode_rdb_on_device_component_entry

  ' setup global nodes now that the screen has been shown
  setGlobalNodes()

  app_start:
  ' First thing to do is validate the ability to use the API
  if not LoginFlow() then return

  ' remove login scenes from the stack
  m.global.sceneManager.callFunc("clearScenes")

  ' load home page
  group = CreateHomeGroup()
  group.callFunc("loadLibraries")
  stopLoadingSpinner()
  m.global.sceneManager.callFunc("pushScene", group)

  m.scene.observeField("exit", m.port)

  ' Downloads and stores a fallback font to tmp:/
  configEncoding = api.system.GetConfigurationByName("encoding")

  if isValid(configEncoding) and isValid(configEncoding.EnableFallbackFont)
    if configEncoding.EnableFallbackFont
      re = CreateObject("roRegex", "Name.:.(.*?).,.Size", "s")
      filename = APIRequest("FallbackFont/Fonts").GetToString()
      if isValid(filename)
        filename = re.match(filename)
        if isValid(filename) and filename.count() > 0
          filename = filename[1]
          APIRequest("FallbackFont/Fonts/" + filename).gettofile("tmp:/font")
        end if
      end if
    end if
  end if

  ' update lastRunVersion but only on prod
  if not m.global.app.isDev
    ' has the current user ran this version before?
    usersLastRunVersion = m.global.session.user.settings.lastRunVersion
    if not isValid(usersLastRunVersion) or not versionChecker(usersLastRunVersion, m.global.app.version)
      set_user_setting("LastRunVersion", m.global.app.version)
    end if
  end if

  ' Handle input messages
  input = CreateObject("roInput")
  input.SetMessagePort(m.port)

  device = CreateObject("roDeviceInfo")
  device.setMessagePort(m.port)
  device.EnableScreensaverExitedEvent(true)
  device.EnableAppFocusEvent(true)
  device.EnableLowGeneralMemoryEvent(true)
  device.EnableLinkStatusEvent(true)
  device.EnableCodecCapChangedEvent(true)
  device.EnableAudioGuideChangedEvent(true)

  ' Check if we were sent content to play with the startup command (Deep Link)
  if isValidAndNotEmpty(args.mediaType) and isValidAndNotEmpty(args.contentId)

    deepLinkVideo = {
      id: args.contentId,
      type: "video"
    }

    m.global.queueManager.callFunc("push", deepLinkVideo)
    m.global.queueManager.callFunc("playQueue")
  end if

  ' This is the core logic loop. Mostly for transitioning between scenes
  ' This now only references m. fields so could be placed anywhere, in theory
  ' "group" is always "whats on the screen"
  ' m.scene's children is the "previous view" stack
  while true
    msg = wait(0, m.port)
    if type(msg) = "roSGScreenEvent" and msg.isScreenClosed()
      print "CLOSING SCREEN"
      return
    else if isNodeEvent(msg, "exit")
      return
    else if isNodeEvent(msg, "closeSidePanel")
      group = m.global.sceneManager.callFunc("getActiveScene")
      if group.lastFocus <> invalid
        group.lastFocus.setFocus(true)
      else
        group.setFocus(true)
      end if
    else if isNodeEvent(msg, "quickPlayNode")
      ' measure processing time
      timeSpan = CreateObject("roTimespan")

      group = m.global.sceneManager.callFunc("getActiveScene")
      reportingNode = msg.getRoSGNode()
      itemNode = invalid
      if isValid(reportingNode)
        itemNode = reportingNode.quickPlayNode
        reportingNodeType = reportingNode.subtype()
        print "Quick Play reporting node type=", reportingNodeType
        ' prevent double fire bug
        if isValid(reportingNodeType) and (reportingNodeType = "Home" or reportingNodeType = "TVEpisodes")
          reportingNode.quickPlayNode = invalid
        end if
      end if
      print "Quick Play started. itemNode=", itemNode
      if isValid(itemNode) and isValid(itemNode.id) and itemNode.id <> ""
        ' if itemNode.json <> invalid
        '   print "itemNode.json=", itemNode.json
        ' end if
        ' make sure there is a type and convert type to lowercase
        itemType = invalid
        if isValid(itemNode.type) and itemNode.type <> ""
          itemType = Lcase(itemNode.type)
        else
          ' grab type from json and convert to lowercase
          if isValid(itemNode.json) and isValid(itemNode.json.type)
            itemType = Lcase(itemNode.json.type)
          end if
        end if
        print "Quick Play itemNode type=", itemType

        ' can't play the item without knowing what type it is
        if isValid(itemType)
          startLoadingSpinner()
          m.global.queueManager.callFunc("clear") ' empty queue/playlist
          m.global.queueManager.callFunc("resetShuffle") ' turn shuffle off

          if itemType = "episode" or itemType = "recording" or itemType = "movie" or itemType = "video"
            quickplay.video(itemNode)
            ' restore focus
            if LCase(group.subtype()) = "tvepisodes"
              if isValid(group.lastFocus)
                group.lastFocus.setFocus(true)
              end if
            end if
          else if itemType = "audio"
            quickplay.audio(itemNode)
          else if itemType = "musicalbum"
            quickplay.album(itemNode)
          else if itemType = "musicartist"
            quickplay.artist(itemNode)
          else if itemType = "series"
            quickplay.series(itemNode)
          else if itemType = "season"
            quickplay.season(itemNode)
          else if itemType = "boxset"
            quickplay.boxset(itemNode)
          else if itemType = "collectionfolder"
            quickplay.collectionFolder(itemNode)
          else if itemType = "playlist"
            quickplay.playlist(itemNode)
          else if itemType = "userview"
            quickplay.userView(itemNode)
          else if itemType = "folder"
            quickplay.folder(itemNode)
          else if itemType = "musicvideo"
            quickplay.musicVideo(itemNode)
          else if itemType = "person"
            quickplay.person(itemNode)
          else if itemType = "tvchannel"
            quickplay.tvChannel(itemNode)
          else if itemType = "program"
            quickplay.program(itemNode)
          else if itemType = "photo"
            quickplay.photo(itemNode)
          else if itemType = "photoalbum"
            quickplay.photoAlbum(itemNode)
          end if
          m.global.queueManager.callFunc("playQueue")
        end if
      end if
      elapsed = timeSpan.TotalMilliseconds() / 1000
      print "Quick Play finished loading in " + elapsed.toStr() + " seconds."
    else if isNodeEvent(msg, "refreshSeasonDetailsData")
      startLoadingSpinner()

      currentScene = m.global.sceneManager.callFunc("getActiveScene")

      if isValid(currentScene) and isValid(currentScene.objects) and isValid(currentScene.seasonData)
        currentEpisode = m.global.queueManager.callFunc("getCurrentItem")

        if isValid(currentScene.objects.Items) and isValid(currentEpisode) and isValid(currentEpisode.id)
          ' Find the object in the scene's data and update its json data
          for i = 0 to currentScene.objects.Items.count() - 1
            if LCase(currentScene.objects.Items[i].id) = LCase(currentEpisode.id)

              data = api.users.GetItem(m.global.session.user.id, currentEpisode.id)
              if isValid(data)
                currentScene.objects.Items[i].json = data
                m.global.queueManager.callFunc("setTopStartingPoint", data.UserData.PlaybackPositionTicks)
              end if
              exit for
            end if
          end for
        end if

        seasonMetaData = ItemMetaData(currentScene.seasonData.id)
        if isValid(seasonMetaData) then currentScene.seasonData = seasonMetaData.json
        currentScene.episodeObjects = currentScene.objects
        currentScene.callFunc("updateSeason")
      end if

      stopLoadingSpinner()
    else if isNodeEvent(msg, "refreshMovieDetailsData")
      startLoadingSpinner()

      currentScene = m.global.sceneManager.callFunc("getActiveScene")

      if isValid(currentScene) and isValid(currentScene.itemContent) and isValid(currentScene.itemContent.id)
        data = api.users.GetItem(m.global.session.user.id, currentScene.itemContent.id)
        if isValid(data)
          currentScene.itemContent.json = data
          ' Set updated starting point for the queue item
          m.global.queueManager.callFunc("setTopStartingPoint", data.UserData.PlaybackPositionTicks)

          ' Refresh movie detail data
          movieMetaData = ItemMetaData(currentScene.itemContent.id)
          if isValid(movieMetaData)
            ' Redraw movie poster
            currentScene.newPosterImageURI = movieMetaData.posterURL
          end if
        end if
      end if

      stopLoadingSpinner()
    else if isNodeEvent(msg, "selectedItem")
      ' If you select a library from ANYWHERE, follow this flow
      selectedItem = msg.getData()
      if isValid(selectedItem)
        startLoadingSpinner()
        selectedItemType = selectedItem.type

        if selectedItemType = "CollectionFolder"
          if selectedItem.collectionType = "movies"
            group = CreateMovieLibraryView(selectedItem)
          else if selectedItem.collectionType = "music"
            group = CreateMusicLibraryView(selectedItem)
          else
            group = CreateItemGrid(selectedItem)
          end if
          m.global.sceneManager.callFunc("pushScene", group)
        else if selectedItemType = "Folder" and selectedItem.json.type = "Genre"
          ' User clicked on a genre folder
          if selectedItem.json.MovieCount > 0
            group = CreateMovieLibraryView(selectedItem)
          else
            group = CreateItemGrid(selectedItem)
          end if
          m.global.sceneManager.callFunc("pushScene", group)
        else if selectedItemType = "Folder" and selectedItem.json.type = "MusicGenre"
          group = CreateMusicLibraryView(selectedItem)
          m.global.sceneManager.callFunc("pushScene", group)
        else if selectedItemType = "UserView" or selectedItemType = "Folder" or selectedItemType = "Channel" or selectedItemType = "Boxset"
          group = CreateItemGrid(selectedItem)
          m.global.sceneManager.callFunc("pushScene", group)
        else if selectedItemType = "Episode" or LCase(selectedItemType) = "recording"
          ' User has selected a TV episode or Recording they want us to play
          audio_stream_idx = 0
          if isValid(selectedItem.selectedAudioStreamIndex) and selectedItem.selectedAudioStreamIndex > 0
            audio_stream_idx = selectedItem.selectedAudioStreamIndex
          end if

          selectedItem.selectedAudioStreamIndex = audio_stream_idx
          ' Display playback options dialog
          if selectedItem.json.userdata.PlaybackPositionTicks > 0
            m.global.queueManager.callFunc("hold", selectedItem)
            playbackOptionDialog(selectedItem.json.userdata.PlaybackPositionTicks, selectedItem.json)
          else
            m.global.queueManager.callFunc("clear")
            m.global.queueManager.callFunc("push", selectedItem)
            m.global.queueManager.callFunc("playQueue")
          end if

        else if selectedItemType = "Series"
          group = CreateSeriesDetailsGroup(selectedItem.json.id)
        else if selectedItemType = "Season"
          if isValid(selectedItem.json) and isValid(selectedItem.json.SeriesId) and isValid(selectedItem.id)
            group = CreateSeasonDetailsGroupByID(selectedItem.json.SeriesId, selectedItem.id)
          else
            stopLoadingSpinner()
            message_dialog(tr("Error loading Season"))
          end if
        else if selectedItemType = "Movie"
          ' open movie detail page
          group = CreateMovieDetailsGroup(selectedItem)
        else if selectedItemType = "Person"
          CreatePersonView(selectedItem)
        else if selectedItemType = "TvChannel" or selectedItemType = "Video" or selectedItemType = "Program"
          ' User selected a Live TV channel / program
          ' Show Channel Loading spinner
          dialog = createObject("roSGNode", "ProgressDialog")
          dialog.title = tr("Loading Channel Data")
          m.scene.dialog = dialog

          ' User selected a program. Play the channel the program is on
          if LCase(selectedItemType) = "program"
            selectedItem.id = selectedItem.json.ChannelId
          end if

          ' Display playback options dialog
          showPlaybackOptionDialog = false

          if isValid(selectedItem.json)
            if isValid(selectedItem.json.userdata)
              if isValid(selectedItem.json.userdata.PlaybackPositionTicks)
                if selectedItem.json.userdata.PlaybackPositionTicks > 0
                  showPlaybackOptionDialog = true
                end if
              end if
            end if
          end if

          if showPlaybackOptionDialog
            dialog.close = true
            m.global.queueManager.callFunc("hold", selectedItem)
            playbackOptionDialog(selectedItem.json.userdata.PlaybackPositionTicks, selectedItem.json)
          else
            m.global.queueManager.callFunc("clear")
            m.global.queueManager.callFunc("push", selectedItem)
            m.global.queueManager.callFunc("playQueue")
            dialog.close = true
          end if

        else if selectedItemType = "Photo"
          ' only handle selection if it's from the home screen
          if selectedItem.isSubType("HomeData")
            print "a photo was selected from the home screen"
            print "selectedItem=", selectedItem

            quickplay.photo(selectedItem)
          end if
        else if selectedItemType = "PhotoAlbum"
          print "a photo album was selected"
          print "selectedItem=", selectedItem

          ' grab all photos inside photo album
          photoAlbumData = api.users.GetItemsByQuery(m.global.session.user.id, {
            "parentId": selectedItem.id,
            "includeItemTypes": "Photo",
            "Recursive": true
          })
          print "photoAlbumData=", photoAlbumData

          if isValid(photoAlbumData) and isValidAndNotEmpty(photoAlbumData.items)
            photoPlayer = CreateObject("roSgNode", "PhotoDetails")
            photoPlayer.itemsArray = photoAlbumData.items
            photoPlayer.itemIndex = 0
            m.global.sceneManager.callfunc("pushScene", photoPlayer)
          end if
        else if selectedItemType = "MusicArtist"
          group = CreateArtistView(selectedItem.json)
          if not isValid(group)
            stopLoadingSpinner()
            message_dialog(tr("Unable to find any albums or songs belonging to this artist"))
          end if
        else if selectedItemType = "MusicAlbum"
          group = CreateAlbumView(selectedItem.json)
        else if selectedItemType = "MusicVideo"
          group = CreateMovieDetailsGroup(selectedItem)
        else if selectedItemType = "Playlist"
          group = CreatePlaylistView(selectedItem.json)
        else if selectedItemType = "Audio"
          m.global.queueManager.callFunc("clear")
          m.global.queueManager.callFunc("resetShuffle")
          m.global.queueManager.callFunc("push", selectedItem.json)
          m.global.queueManager.callFunc("playQueue")
        else
          ' TODO - switch on more node types
          stopLoadingSpinner()
          message_dialog("This type is not yet supported: " + selectedItemType + ".")
        end if
      end if
    else if isNodeEvent(msg, "movieSelected")
      ' If you select a movie from ANYWHERE, follow this flow
      startLoadingSpinner()
      node = getMsgPicker(msg, "picker")
      group = CreateMovieDetailsGroup(node)
    else if isNodeEvent(msg, "seriesSelected")
      ' If you select a TV Series from ANYWHERE, follow this flow
      startLoadingSpinner()
      node = getMsgPicker(msg, "picker")
      group = CreateSeriesDetailsGroup(node.id)
    else if isNodeEvent(msg, "seasonSelected")
      ' If you select a TV Season from ANYWHERE, follow this flow
      startLoadingSpinner()
      ptr = msg.getData()
      ' ptr is for [row, col] of selected item... but we only have 1 row
      series = msg.getRoSGNode()
      if isValid(ptr) and ptr.count() >= 2 and isValid(ptr[1]) and isValid(series) and isValid(series.seasonData) and isValid(series.seasonData.items)
        node = series.seasonData.items[ptr[1]]
        group = CreateSeasonDetailsGroup(series.itemContent, node)
      end if
    else if isNodeEvent(msg, "musicAlbumSelected")
      ' If you select a Music Album from ANYWHERE, follow this flow
      startLoadingSpinner()
      ptr = msg.getData()
      albums = msg.getRoSGNode()
      node = albums.musicArtistAlbumData.items[ptr]
      group = CreateAlbumView(node)
      if not isValid(group)
        stopLoadingSpinner()
      end if
    else if isNodeEvent(msg, "appearsOnSelected")
      ' If you select a Music Album from ANYWHERE, follow this flow
      startLoadingSpinner()
      ptr = msg.getData()
      albums = msg.getRoSGNode()
      node = albums.musicArtistAppearsOnData.items[ptr]
      group = CreateAlbumView(node)
      if not isValid(group)
        stopLoadingSpinner()
      end if
    else if isNodeEvent(msg, "playSong")
      ' User has selected audio they want us to play
      startLoadingSpinner()
      selectedIndex = msg.getData()
      screenContent = msg.getRoSGNode()

      m.global.queueManager.callFunc("resetShuffle")
      m.global.queueManager.callFunc("set", screenContent.albumData.items)
      m.global.queueManager.callFunc("setPosition", selectedIndex)
      m.global.queueManager.callFunc("playQueue")
    else if isNodeEvent(msg, "playItem")
      ' User has selected audio they want us to play
      startLoadingSpinner()
      selectedIndex = msg.getData()
      screenContent = msg.getRoSGNode()

      m.global.queueManager.callFunc("resetShuffle")
      m.global.queueManager.callFunc("set", screenContent.albumData.items)
      m.global.queueManager.callFunc("setPosition", selectedIndex)
      m.global.queueManager.callFunc("playQueue")
    else if isNodeEvent(msg, "playAllSelected")
      ' User has selected playlist of of audio they want us to play
      screenContent = msg.getRoSGNode()
      startLoadingSpinner()

      m.global.queueManager.callFunc("clear")
      m.global.queueManager.callFunc("resetShuffle")
      m.global.queueManager.callFunc("set", screenContent.albumData.items)
      m.global.queueManager.callFunc("playQueue")
    else if isNodeEvent(msg, "playArtistSelected")
      ' User has selected playlist of of audio they want us to play
      startLoadingSpinner()
      screenContent = msg.getRoSGNode()

      m.global.queueManager.callFunc("clear")
      m.global.queueManager.callFunc("resetShuffle")
      m.global.queueManager.callFunc("set", CreateArtistMix(screenContent.pageContent.id).Items)
      m.global.queueManager.callFunc("playQueue")

    else if isNodeEvent(msg, "instantMixSelected")
      ' User has selected instant mix
      ' User has selected playlist of of audio they want us to play
      screenContent = msg.getRoSGNode()
      startLoadingSpinner()

      viewHandled = false

      ' Create instant mix based on selected album
      if isValid(screenContent.albumData)
        if isValid(screenContent.albumData.items)
          if screenContent.albumData.items.count() > 0
            m.global.queueManager.callFunc("clear")
            m.global.queueManager.callFunc("resetShuffle")
            m.global.queueManager.callFunc("set", CreateInstantMix(screenContent.albumData.items[0].id).Items)
            m.global.queueManager.callFunc("playQueue")

            viewHandled = true
          end if
        end if
      end if

      if not viewHandled
        ' Create instant mix based on selected artist
        m.global.queueManager.callFunc("clear")
        m.global.queueManager.callFunc("resetShuffle")
        m.global.queueManager.callFunc("set", CreateInstantMix(screenContent.pageContent.id).Items)
        m.global.queueManager.callFunc("playQueue")
      end if

    else if isNodeEvent(msg, "search_value")
      query = msg.getRoSGNode().search_value
      group.findNode("SearchBox").visible = false
      options = group.findNode("SearchSelect")
      options.visible = true
      options.setFocus(true)

      dialog = createObject("roSGNode", "ProgressDialog")
      dialog.title = tr("Loading Search Data")
      m.scene.dialog = dialog
      results = SearchMedia(query)
      dialog.close = true
      options.itemData = results
      options.query = query
    else if isNodeEvent(msg, "itemSelected")
      ' Search item selected
      startLoadingSpinner()
      node = getMsgPicker(msg)
      ' TODO - swap this based on target.mediatype
      ' types: [ Series (Show), Episode, Movie, Audio, Person, Studio, MusicArtist, Recording ]
      if node.type = "Series"
        group = CreateSeriesDetailsGroup(node.id)
      else if node.type = "Movie"
        group = CreateMovieDetailsGroup(node)
      else if node.type = "MusicArtist"
        group = CreateArtistView(node.json)
      else if node.type = "MusicAlbum"
        group = CreateAlbumView(node.json)
      else if node.type = "MusicVideo"
        group = CreateMovieDetailsGroup(node)
      else if node.type = "Audio"
        m.global.queueManager.callFunc("clear")
        m.global.queueManager.callFunc("resetShuffle")
        m.global.queueManager.callFunc("push", node.json)
        m.global.queueManager.callFunc("playQueue")
      else if node.type = "Person"
        group = CreatePersonView(node)
      else if node.type = "TvChannel"
        group = CreateVideoPlayerGroup(node.id)
        m.global.sceneManager.callFunc("pushScene", group)
      else if node.type = "Episode"
        group = CreateVideoPlayerGroup(node.id)
        m.global.sceneManager.callFunc("pushScene", group)
      else if LCase(node.type) = "recording"
        group = CreateVideoPlayerGroup(node.id)
        m.global.sceneManager.callFunc("pushScene", group)
      else if node.type = "Audio"
        selectedIndex = msg.getData()
        screenContent = msg.getRoSGNode()
        m.global.queueManager.callFunc("clear")
        m.global.queueManager.callFunc("resetShuffle")
        m.global.queueManager.callFunc("push", screenContent.albumData.items[node.id])
        m.global.queueManager.callFunc("playQueue")
      else
        ' TODO - switch on more node types
        stopLoadingSpinner()
        message_dialog("This type is not yet supported: " + node.type + ".")
      end if
    else if isNodeEvent(msg, "buttonSelected")
      ' If a button is selected, we have some determining to do
      btn = getButton(msg)
      group = m.global.sceneManager.callFunc("getActiveScene")

      if isValid(btn) and btn.id = "play-button"
        if not isValid(group) then return

        ' User chose Play button from movie detail view
        startLoadingSpinner()
        ' Check if a specific Audio Stream was selected
        audio_stream_idx = 0
        if isValid(group.selectedAudioStreamIndex)
          audio_stream_idx = group.selectedAudioStreamIndex
        end if

        if isValid(group.itemContent)
          group.itemContent.selectedAudioStreamIndex = audio_stream_idx
          group.itemContent.id = group.selectedVideoStreamId

          ' Display playback options dialog
          if group.itemContent.json.userdata.PlaybackPositionTicks > 0
            m.global.queueManager.callFunc("hold", group.itemContent)
            playbackOptionDialog(group.itemContent.json.userdata.PlaybackPositionTicks, group.itemContent.json)
          else
            m.global.queueManager.callFunc("clear")
            m.global.queueManager.callFunc("push", group.itemContent)
            m.global.queueManager.callFunc("playQueue")
          end if
        end if

        if isValid(group.lastFocus) and isValid(group.lastFocus.id) and group.lastFocus.id = "main_group"
          buttons = group.findNode("buttons")
          if isValid(buttons)
            group.lastFocus = group.findNode("buttons")
          end if
        end if

        if isValid(group.lastFocus)
          group.lastFocus.setFocus(true)
        end if

      else if btn <> invalid and btn.id = "trailer-button"
        ' User chose to play a trailer from the movie detail view
        startLoadingSpinner()
        dialog = createObject("roSGNode", "ProgressDialog")
        dialog.title = tr("Loading trailer")
        m.scene.dialog = dialog

        trailerData = api.users.GetLocalTrailers(m.global.session.user.id, group.id)

        if isValid(trailerData) and isValid(trailerData[0]) and isValid(trailerData[0].id)
          m.global.queueManager.callFunc("clear")
          m.global.queueManager.callFunc("set", trailerData)
          m.global.queueManager.callFunc("playQueue")
          dialog.close = true
        else
          stopLoadingSpinner()
        end if

        if isValid(group) and isValid(group.lastFocus)
          group.lastFocus.setFocus(true)
        end if
      else if btn <> invalid and btn.id = "watched-button"
        movie = group.itemContent
        if isValid(movie) and isValid(movie.watched) and isValid(movie.id)
          if movie.watched
            UnmarkItemWatched(movie.id)
          else
            MarkItemWatched(movie.id)
          end if
          movie.watched = not movie.watched
        end if
      else if btn <> invalid and btn.id = "favorite-button"
        movie = group.itemContent
        if movie.favorite
          UnmarkItemFavorite(movie.id)
        else
          MarkItemFavorite(movie.id)
        end if
        movie.favorite = not movie.favorite
      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 = "OKDialog"
          dialog.unobserveField("buttonSelected")
          dialog.close = true
        end if
      end if
    else if isNodeEvent(msg, "optionSelected")
      button = msg.getRoSGNode()
      group = m.global.sceneManager.callFunc("getActiveScene")
      if button.id = "goto_search" and isValid(group)
        ' Exit out of the side panel
        panel = group.findNode("options")
        panel.visible = false
        if isValid(group.lastFocus)
          group.lastFocus.setFocus(true)
        else
          group.setFocus(true)
        end if
        group = CreateSearchPage()
        m.global.sceneManager.callFunc("pushScene", group)
        group.findNode("SearchBox").findNode("search_Key").setFocus(true)
        group.findNode("SearchBox").findNode("search_Key").active = true
      else if button.id = "change_server"
        startLoadingSpinner()
        unset_setting("server")
        session.server.Delete()
        SignOut(false)
        m.global.sceneManager.callFunc("clearScenes")
        goto app_start
      else if button.id = "change_user"
        startLoadingSpinner()
        SignOut(false)
        m.global.sceneManager.callFunc("clearScenes")
        goto app_start
      else if button.id = "sign_out"
        startLoadingSpinner()
        SignOut()
        m.global.sceneManager.callFunc("clearScenes")
        goto app_start
      else if button.id = "settings"
        ' Exit out of the side panel
        panel = group.findNode("options")
        panel.visible = false
        if isValid(group) and isValid(group.lastFocus)
          group.lastFocus.setFocus(true)
        else
          group.setFocus(true)
        end if
        m.global.sceneManager.callFunc("settings")
      end if
    else if isNodeEvent(msg, "selectSubtitlePressed")
      node = m.scene.focusedChild
      if node.focusedChild <> invalid and node.focusedChild.isSubType("JRVideo")
        trackSelected = selectSubtitleTrack(node.Subtitles, node.SelectedSubtitle)
        if trackSelected <> invalid and trackSelected <> -2
          changeSubtitleDuringPlayback(trackSelected)
        end if
      end if
    else if isNodeEvent(msg, "selectPlaybackInfoPressed")
      node = m.scene.focusedChild
      if node.focusedChild <> invalid and node.focusedChild.isSubType("JRVideo")
        info = GetPlaybackInfo()
        show_dialog(tr("Playback Information"), info)
      end if
    else if isNodeEvent(msg, "state")
      node = msg.getRoSGNode()
      if isValid(node) and isValid(node.state)
        if node.selectedItemType = "TvChannel" and node.state = "finished"
          video = CreateVideoPlayerGroup(node.id)
          m.global.sceneManager.callFunc("pushScene", video)
          m.global.sceneManager.callFunc("deleteSceneAtIndex", 2)
        else if node.state = "finished"
          node.control = "stop"

          ' If node allows retrying using Transcode Url, give that shot
          if isValid(node.retryWithTranscoding) and node.retryWithTranscoding
            retryVideo = CreateVideoPlayerGroup(node.Id, invalid, node.audioIndex, true, false)
            m.global.sceneManager.callFunc("popScene")
            if isValid(retryVideo)
              m.global.sceneManager.callFunc("pushScene", retryVideo)
            end if
          else if not isValid(node.showID)
            m.global.sceneManager.callFunc("popScene")
          else
            autoPlayNextEpisode(node.id, node.showID)
          end if
        end if
      end if
    else if type(msg) = "roDeviceInfoEvent"
      event = msg.GetInfo()

      if event.exitedScreensaver = true
        m.global.sceneManager.callFunc("resetTime")
        group = m.global.sceneManager.callFunc("getActiveScene")
        if isValid(group)
          ' refresh the current view
          if group.isSubType("JRScreen")
            group.callFunc("OnScreenShown")
          end if
        end if
      else if isValid(event.audioGuideEnabled)
        tmpGlobalDevice = m.global.device
        tmpGlobalDevice.AddReplace("isaudioguideenabled", event.audioGuideEnabled)

        ' update global device array
        m.global.setFields({ device: tmpGlobalDevice })
      else if isValid(event.Mode)
        ' Indicates the current global setting for the Caption Mode property, which may be one of the following values:
        ' "On"
        ' "Off"
        ' "Instant replay"
        ' "When mute" (Only returned for a TV; this option is not available on STBs).
        print "event.Mode = ", event.Mode
        if isValid(event.Mute)
          print "event.Mute = ", event.Mute
        end if
      else if isValid(event.linkStatus)
        ' True if the device currently seems to have an active network connection.
        print "event.linkStatus = ", event.linkStatus
      else if isValid(event.generalMemoryLevel)
        ' This event will be sent first when the OS transitions from "normal" to "low" state and will continue to be sent while in "low" or "critical" states.
        '   - "normal" means that the general memory is within acceptable levels
        '   - "low" means that the general memory is below acceptable levels but not critical
        '   - "critical" means that general memory are at dangerously low level and that the OS may force terminate the application
        print "event.generalMemoryLevel = ", event.generalMemoryLevel
        session.Update("memoreyLevel", event.generalMemoryLevel)
      else if isValid(event.audioCodecCapabilityChanged)
        ' The audio codec capability has changed if true.
        print "event.audioCodecCapabilityChanged = ", event.audioCodecCapabilityChanged

        postTask = createObject("roSGNode", "PostTask")
        postTask.arrayData = getDeviceCapabilities()
        postTask.apiUrl = "/Sessions/Capabilities/Full"
        postTask.control = "RUN"
      else if isValid(event.videoCodecCapabilityChanged)
        ' The video codec capability has changed if true.
        print "event.videoCodecCapabilityChanged = ", event.videoCodecCapabilityChanged

        postTask = createObject("roSGNode", "PostTask")
        postTask.arrayData = getDeviceCapabilities()
        postTask.apiUrl = "/Sessions/Capabilities/Full"
        postTask.control = "RUN"
      else if isValid(event.appFocus)
        ' It is set to False when the System Overlay (such as the confirm partner button HUD or the caption control overlay) takes focus and True when the channel regains focus
        print "event.appFocus = ", event.appFocus
      else
        print "Unhandled roDeviceInfoEvent:"
        print msg.GetInfo()
      end if
    else if type(msg) = "roInputEvent"
      if msg.IsInput()
        info = msg.GetInfo()
        if info.DoesExist("mediatype") and info.DoesExist("contentid")
          inputEventVideo = {
            id: info.contentId,
            type: "video"
          }

          m.global.queueManager.callFunc("clear")
          m.global.queueManager.callFunc("push", inputEventVideo)
          m.global.queueManager.callFunc("playQueue")
        end if
      end if
    else if isNodeEvent(msg, "dataReturned")
      popupNode = msg.getRoSGNode()
      stopLoadingSpinner()
      if isValid(popupNode) and isValid(popupNode.returnData)
        selectedItem = m.global.queueManager.callFunc("getHold")
        m.global.queueManager.callFunc("clearHold")

        if isValidAndNotEmpty(selectedItem) and isValid(selectedItem[0])
          if popupNode.returnData.indexselected = 0
            'Resume video from resume point
            startLoadingSpinner()
            startingPoint = 0

            if isValid(selectedItem[0].json) and isValid(selectedItem[0].json.UserData) and isValid(selectedItem[0].json.UserData.PlaybackPositionTicks)
              if selectedItem[0].json.UserData.PlaybackPositionTicks > 0
                startingPoint = selectedItem[0].json.UserData.PlaybackPositionTicks
              end if
            end if

            selectedItem[0].startingPoint = startingPoint
            m.global.queueManager.callFunc("clear")
            m.global.queueManager.callFunc("push", selectedItem[0])
            m.global.queueManager.callFunc("playQueue")
          else if popupNode.returnData.indexselected = 1
            'Start Over from beginning selected, set position to 0
            startLoadingSpinner()
            selectedItem[0].startingPoint = 0
            m.global.queueManager.callFunc("clear")
            m.global.queueManager.callFunc("push", selectedItem[0])
            m.global.queueManager.callFunc("playQueue")
          else if popupNode.returnData.indexselected = 2
            ' User chose Go to series
            CreateSeriesDetailsGroup(selectedItem[0].json.SeriesId)
          else if popupNode.returnData.indexselected = 3
            ' User chose Go to season
            if isValid(selectedItem[0].json) and isValid(selectedItem[0].json.SeriesId) and isValid(selectedItem[0].json.seasonID)
              CreateSeasonDetailsGroupByID(selectedItem[0].json.SeriesId, selectedItem[0].json.seasonID)
            else
              stopLoadingSpinner()
              message_dialog(tr("Error loading Season"))
            end if

          else if popupNode.returnData.indexselected = 4
            ' User chose Go to episode
            CreateMovieDetailsGroup(selectedItem[0])
          end if
        end if
      end if
    else
      print "Unhandled " type(msg)
      print msg
    end if
  end while

end sub