source_utils_quickplay.bs

import "pkg:/source/utils/nodeHelpers.bs"
import "pkg:/source/utils/streamSelection.bs"
import "pkg:/source/utils/versionResume.bs"

' Render-thread-safe Quick Play helpers.
' API-calling quickplay logic lives in QuickPlayTask (components/tasks/QuickPlayTask.bs).
namespace quickplay

  ' Takes an array of items and adds to global queue.
  ' Also shuffles the playlist if asked
  sub pushToQueue(queueArray as object, shufflePlay = false as boolean)
    if isValidAndNotEmpty(queueArray)
      for each item in queueArray
        m.global.queueManager.callFunc("push", nodeHelpers.createQueueItem(item))
      end for
      ' shuffle the playlist if asked
      if shufflePlay and m.global.queueManager.callFunc("getCount") > 1
        m.global.queueManager.callFunc("toggleShuffle")
      end if
    end if
  end sub

  ' A single video file: a movie, episode, recording, video or music video.
  sub video(itemNode as object)
    if not isValid(itemNode) or not isValid(itemNode.id) or itemNode.id = "" then return

    ' Create a queue-specific item to avoid modifying the UI node
    ' This prevents the quickPlayNode field observer from firing twice
    queueItem = nodeHelpers.createQueueItem(itemNode)
    if not isValid(queueItem) then return

    ' Select the video source. On a per-version server the choice needs each version's own
    ' position, which cannot be read from here, so it is left to LoadVideoContentTask; without
    ' per-version positions any version resumes at the item's one position, so the best for the
    ' device is picked here. A source chosen here travels as mediaSourceId, which the loader
    ' uses without overwriting the base item ID needed for metadata/reporting.
    audioStreams = itemNode.audioStreams
    deferVersionToLoader = false
    if isValid(itemNode.mediaSourcesData) and isValidAndNotEmpty(itemNode.mediaSourcesData.mediaSources)
      mediaSources = itemNode.mediaSourcesData.mediaSources
      if mediaSources.Count() > 1
        if versionResume.isPerVersionServer(m.global.server.version)
          ' Which version resumes depends on every version's own position, and each of those
          ' is a request, impossible here, on the render thread with no fetching. So hand the
          ' choice to LoadVideoContentTask, which makes it with the positions in hand.
          '
          ' Standing down means actively clearing the id, not only declining to set one: the
          ' transformer pre-fills item.mediaSourceId with MediaSources[0].Id and
          ' createQueueItem copies it, so a quick play would otherwise arrive at the loader
          ' looking like an explicit pick and skip the choice altogether. The details screen's
          ' real pick still travels as selectedVideoStreamId and is still honored.
          deferVersionToLoader = true

          ' Free signal, no request. The loader needs to know this is a resume BEFORE it has
          ' chosen a version: startingPoint cannot say so when the item's own position is 0
          ' and an alternate holds one.
          queueItem.hasVersionInProgress = versionResume.anyVersionInProgress(itemNode.id, itemNode.playbackPositionTicks, mediaSources, true)
        else
          ' One position per item, so any version resumes at it: take the best for the device.
          bestSource = mediaSources[findBestVideoSource(mediaSources)]
          if isValidAndNotEmpty(bestSource.Id)
            queueItem.mediaSourceId = bestSource.Id
          end if
          ' Use the selected source's audio streams for best audio selection
          if isValid(bestSource.MediaStreams)
            audioStreams = []
            for each stream in bestSource.MediaStreams
              if isValid(stream.Type) and LCase(stream.Type) = "audio"
                audioStreams.push(stream)
              end if
            end for
          end if
        end if
      end if
    end if

    if deferVersionToLoader then queueItem.mediaSourceId = ""

    ' Get user session for audio selection
    localUser = m.global.user

    ' audioStreams is pre-filtered to audio-only by the transformer (or re-filtered above).
    ' findBestAudioStreamIndex filters by Type="audio" internally, so passing
    ' pre-filtered audio streams works correctly.
    ' Left at 0 when the version is the loader's to choose: 0 is what tells it to pick the
    ' audio track itself, and any index decided here would belong to the item's OWN version's
    ' stream list rather than the one it ends up playing. Likewise when the pick needs the
    ' series' original language, which only the loader can fetch.
    audioStreamIndex = 0
    if isValidAndNotEmpty(audioStreams) and not deferVersionToLoader and not needsSeriesOriginalLanguage(itemNode, localUser.settings, localUser.config)
      audioStreamIndex = selectAudioStreamIndex(audioStreams, localUser.settings, localUser.config, itemNode.originalLanguage ?? "")
    end if

    queueItem.selectedAudioStreamIndex = audioStreamIndex
    ' The item's saved position: on a per-version server the loader may resume another version
    ' at that version's own position instead.
    nodeHelpers.setResumeStart(queueItem, itemNode.playbackPositionTicks)

    m.global.queueManager.callFunc("push", queueItem)
  end sub

  ' A single audio file.
  sub audio(itemNode as object)
    if not isValid(itemNode) or not isValid(itemNode.id) then return

    ' Create a queue-specific item to avoid modifying the UI node
    queueItem = nodeHelpers.createQueueItem(itemNode)
    if not isValid(queueItem) then return

    m.global.queueManager.callFunc("push", queueItem)
  end sub

  ' A single photo.
  '
  ' Route to the /photo view. This runs on the QueueManager render thread (no router chain), so
  ' it signals the launch via
  ' m.global.photoLaunchRequest; JRScene turns that into navigateTo("/photo") with the AA as
  ' route context. itemsNode IS the single photo node; PhotoDetails reads it on mount.
  sub photo(itemNode as object)
    if not isValid(itemNode) or not isValid(itemNode.id) then return

    m.global.photoLaunchRequest = { itemsNode: itemNode, itemIndex: 0 }
  end sub

  ' Quick Play A TVChannel
  sub tvChannel(itemNode as object)
    if not isValid(itemNode) or not isValidAndNotEmpty(itemNode.id)
      print "ERROR: quickplay.tvChannel() - missing itemNode or id"
      stopLoadingSpinner()
      return
    end if

    ' Push TV channel queue item; keep spinner active until video content loads
    m.global.queueManager.callFunc("push", nodeHelpers.createQueueItem(itemNode))
  end sub

  ' Quick Play A Live Program
  sub program(itemNode as object)
    if not isValid(itemNode) or not isValidAndNotEmpty(itemNode.channelId)
      print "ERROR: quickplay.program() - missing ChannelId"
      stopLoadingSpinner()
      return
    end if

    ' Play the channel this program is on, not the program itself.
    ' Override id and type on the queue item so the playback pipeline targets the channel.
    ' Keep spinner active until video content loads.
    queueItem = nodeHelpers.createQueueItem(itemNode)
    queueItem.id = itemNode.channelId
    queueItem.type = "TvChannel"
    m.global.queueManager.callFunc("push", queueItem)
  end sub

end namespace