source_utils_stillWatching.bs

import "pkg:/source/utils/misc.bs"

' "Are you still watching?": when to ask (issue #982).
'
' The question is whether anyone is at the remote. Roku can say when a remote button was last
' pressed (roDeviceInfo.TimeSinceLastKeypress), so the rule is: ask before the next video when
' too many videos in a row played with no button pressed, or at least two did and too long has
' passed since the last press. A press while a video plays means someone was there, so that
' video does not count.
'
' The presets match jellyfin-web's and Android TV's (count of videos, minutes). Web keeps its
' choice in the browser's localStorage rather than on the server, so there is no web value to
' read; the default is web's default.
'
' PlayerHostView owns the session (newStillWatchingSession) for as long as the player route is
' mounted, and calls these at each automatic advance and each sign of activity.

' How long the prompt waits for an answer before the video pauses.
const STILL_WATCHING_RESPONSE_SECONDS = 30

' The time limit applies only from this many unattended videos in a row, so one long video (a
' movie) never uses up the whole limit by itself.
const STILL_WATCHING_MIN_VIDEOS_FOR_TIME_LIMIT = 2

' TimeSinceLastKeypress counts whole seconds while UpTime has fractions, so a press within this
' long after a video started cannot be told from the press that started it. Only a press later
' than this counts as "during" the video.
const STILL_WATCHING_KEYPRESS_PRECISION_SECONDS = 1

' The thresholds for a playbackStillWatching value, or invalid when the prompt is off.
' An empty or unrecognized value is the default preset, so a bad registry value never
' silently disables the check.
'
' @param {string} option - playbackStillWatching ("disabled", "short", "default", "long", "veryLong")
' @returns {dynamic} { count, seconds } or invalid
function stillWatchingThresholds(option as string) as dynamic
  if option = "disabled" then return invalid
  if option = "short" then return { count: 2, seconds: 60 * 60 }
  if option = "long" then return { count: 5, seconds: 150 * 60 }
  if option = "veryLong" then return { count: 8, seconds: 240 * 60 }
  return { count: 3, seconds: 90 * 60 }
end function

' A new session: the player route mounted, or the viewer skipped to another item. Either way a
' button press (or a voice / app command) started the video now playing.
'
' @param {float} nowSeconds - UpTime(0)
' @returns {object} { unattendedCount, itemStartedAt, lastActivityAt }
function newStillWatchingSession(nowSeconds as float) as object
  return { unattendedCount: 0, itemStartedAt: nowSeconds, lastActivityAt: nowSeconds }
end function

' A video ended and the queue is moving on to the next one by itself. Records whether the video
' that ended was unattended and returns whether to ask before the next one plays.
'
' Idle time is the shorter of "since the last button press" and "since the last activity": a
' voice or Jellyfin app command presses no button.
'
' @param {object} session - from newStillWatchingSession
' @param {dynamic} thresholds - from stillWatchingThresholds (invalid = off)
' @param {integer} secondsSinceKeypress - roDeviceInfo.TimeSinceLastKeypress()
' @param {float} nowSeconds - UpTime(0)
' @returns {boolean} true to ask
function stillWatchingOnAdvance(session as object, thresholds as dynamic, secondsSinceKeypress as integer, nowSeconds as float) as boolean
  idleSeconds = secondsSinceKeypress
  sinceActivity = nowSeconds - session.lastActivityAt
  if sinceActivity < idleSeconds then idleSeconds = sinceActivity

  if idleSeconds + STILL_WATCHING_KEYPRESS_PRECISION_SECONDS < nowSeconds - session.itemStartedAt
    session.unattendedCount = 0
  else
    session.unattendedCount = session.unattendedCount + 1
  end if
  session.itemStartedAt = nowSeconds

  if not isValid(thresholds) then return false
  if session.unattendedCount >= thresholds.count then return true
  return session.unattendedCount >= STILL_WATCHING_MIN_VIDEOS_FOR_TIME_LIMIT and idleSeconds >= thresholds.seconds
end function

' Someone is there without pressing a button: a voice or Jellyfin app command, the prompt
' answered, or playback resumed after the prompt paused it.
'
' @param {object} session - from newStillWatchingSession
' @param {float} nowSeconds - UpTime(0)
sub stillWatchingOnActivity(session as object, nowSeconds as float)
  session.unattendedCount = 0
  session.lastActivityAt = nowSeconds
end sub