source_utils_mediaSegments.bs
import "pkg:/source/enums/MediaSegmentAction.bs"
import "pkg:/source/utils/misc.bs"
' supportsMediaSegments: Checks if the connected Jellyfin server supports the MediaSegments API.
'
' The /MediaSegments/{itemId} endpoint was introduced in Jellyfin 10.10.0.
' Servers below 10.10.0 do not have this endpoint and will return 404.
'
' @returns {boolean} - true if server supports media segments
function supportsMediaSegments() as boolean
return versionChecker(m.global.server.version, "10.10.0")
end function
' resolveSegmentAction: Resolves the action to take for a given media segment type.
'
' Checks JellyRock per-type override setting first, then falls back to web client
' DisplayPreferences CustomPrefs value, then falls back to built-in defaults.
'
' Built-in defaults:
' All types = AskToSkip
'
' @param {string} segmentType - MediaSegmentType value ("Intro", "Outro", etc.)
' @param {object} userSettings - JellyfinUserSettings node (JellyRock settings)
' @param {object} userConfig - JellyfinUserConfiguration node (web client settings)
' @returns {string} - MediaSegmentAction value ("None", "AskToSkip", "Skip")
function resolveSegmentAction(segmentType as string, userSettings as object, userConfig as object) as string
defaultValue = MediaSegmentAction.ASK_TO_SKIP
' Try web client setting from config node (loaded from DisplayPreferences CustomPrefs)
configFieldKey = "segmentAction" + segmentType
if isValid(userConfig) and isValid(userConfig[configFieldKey]) and userConfig[configFieldKey] <> ""
defaultValue = userConfig[configFieldKey]
end if
' Check for JellyRock override setting
settingKey = "playbackSegmentAction" + segmentType
if isValid(userSettings) and isValid(userSettings[settingKey]) and userSettings[settingKey] <> ""
overrideValue = userSettings[settingKey]
if overrideValue = "skip"
return MediaSegmentAction.SKIP
else if overrideValue = "askToSkip"
return MediaSegmentAction.ASK_TO_SKIP
else if overrideValue = "none"
return MediaSegmentAction.NONE
end if
' "webclient" or unrecognized -> fall through to web client value
end if
return defaultValue
end function
' resolveEffectiveSegmentAction: Resolves the action for a segment, accounting for
' whether it has already been auto-skipped this playback session.
'
' A Skip-configured segment auto-seeks only on its first pass. Once skipped, rewinding
' back into it must NOT re-skip (that would trap the user in a rewind loop) — but the
' user should still get the *manual* Skip button. So an already-skipped Skip segment
' degrades to AskToSkip; all other actions pass through unchanged.
'
' @param {string} segmentType - MediaSegmentType value ("Intro", "Outro", etc.)
' @param {object} userSettings - JellyfinUserSettings node (JellyRock settings)
' @param {object} userConfig - JellyfinUserConfiguration node (web client settings)
' @param {boolean} alreadySkipped - true if this segment already auto-skipped this session
' @returns {string} - MediaSegmentAction value ("None", "AskToSkip", "Skip")
function resolveEffectiveSegmentAction(segmentType as string, userSettings as object, userConfig as object, alreadySkipped as boolean) as string
action = resolveSegmentAction(segmentType, userSettings, userConfig)
if action = MediaSegmentAction.SKIP and alreadySkipped then return MediaSegmentAction.ASK_TO_SKIP
return action
end function
' findActiveSegment: Finds the media segment containing the current playback position.
'
' Linear scan over the segments array (typically 2-5 items).
' Returns the first segment where StartTicks <= position < EndTicks.
'
' @param {roArray} segments - Array of MediaSegmentDto objects ({Id, Type, StartTicks, EndTicks})
' @param {float} positionSeconds - Current playback position in seconds
' @returns {dynamic} The active MediaSegmentDto or invalid if not inside any segment
function findActiveSegment(segments as object, positionSeconds as float) as dynamic
if not isValid(segments) then return invalid
for each segment in segments
startSeconds = segmentTicksToSeconds(segment.StartTicks)
endSeconds = segmentTicksToSeconds(segment.EndTicks)
if positionSeconds >= startSeconds and positionSeconds < endSeconds
return segment
end if
end for
return invalid
end function
' findSegmentById: Finds a media segment by its Id.
'
' Linear scan over the segments array (typically 2-5 items).
' Prefer this over caching segment objects, since the segments array can be
' replaced during content loading (audio/subtitle track changes).
'
' @param {roArray} segments - Array of MediaSegmentDto objects
' @param {string} segmentId - The segment Id to find
' @returns {dynamic} The matching MediaSegmentDto or invalid if not found
function findSegmentById(segments as object, segmentId as string) as dynamic
if not isValid(segments) or segmentId = "" then return invalid
for each segment in segments
if (segment.Id ?? "") = segmentId
return segment
end if
end for
return invalid
end function
' segmentTicksToSeconds: Converts Jellyfin ticks (100-nanosecond units) to seconds.
'
' @param {dynamic} ticks - Tick count (10,000,000 ticks = 1 second)
' @returns {float} - Position in seconds
function segmentTicksToSeconds(ticks as dynamic) as float
if not isValid(ticks) then return 0
return ticks / 10000000#
end function