Members

(static, constant) MERIDIAN_INFERENCE_HORIZON_SECONDS

How far ahead a reader can still resolve a bare 12h time by comparing it to the clock. Dropping the suffix asserts "the nearest clock face showing these digits", which stops being the time we meant once the meridian comes back around: at 13:00 a 23-hour runtime ends at 12:00 pm — same half of the day, so the suffix rule alone would omit it and "Ends at 12:00" would read as noon today. Past this horizon the suffix always earns its place.

Default Value
  • 43200

Methods

(static) AssocArrayEqual(Array1, Array2) → {boolean}

Parameters:
NameTypeDescription
Array1object
Array2object
Returns:
Type: 
boolean

(static) addOrdinalSuffix(day) → {string}

Parameters:
NameTypeDescription
dayinteger
Returns:
Type: 
string

(static) appScene() → {object}

The app's Scene node, cached on first use.

Reaching the scene means walking up from m.top, so every caller that needed it had grown its own copy of this three-line cache. m is per-component-scope, so the cache is per component — which is what we want: each component resolves the scene once and reuses it.

Returns:
Type: 
object

(static) arrayHasValue(arr, value) → {boolean}

Check if a specific value is inside of an array

Parameters:
NameTypeDescription
arrobject
valuedynamic
Returns:
Type: 
boolean

(static) computeProgramBroadcastProgress(playStart, playDuration, nowSeconds) → {float}

Computes a broadcast program's elapsed-time percentage. Programs are live broadcasts, not streamed media — so "progress" is wall-clock elapsed, not UserData.PlayedPercentage. Returns 0 (not 100) for finished programs because JRPoster only cleanly removes the VideoProgressBar child when playedPercentage = 0.

Parameters:
NameTypeDescription
playStartinteger

epoch seconds when the program started (0 = unknown)

playDurationinteger

program length in seconds (0 = unknown)

nowSecondsinteger

current epoch seconds (injected for testability)

Returns:

float in (0, 100) while airing, or 0 when not airing / invalid

Type: 
float

(static) decimalOrZero(value) → {float}

A number from a field that may be a float, an integer, a string, or absent.

The FLOAT-PRESERVING counterpart to mediaNumber, and the distinction matters in both directions. mediaNumber truncates to a 32-bit Integer, which is right for Height / Channels / SampleRate and wrong for two kinds of field:

  • FRACTIONAL ones — frame rates, completion percentages — where truncating destroys the figure outright.
  • LARGE ones — a byte count passes 2^31 at ~2.1 GB, and RunTimeTicks for a three-hour film is ~1.08e11 — where truncating overflows.

Every caller today passes a media field, so this covers what actually reaches it in practice — an unhandled type (roSGNode, roDateTime, a function) would still fault on return value as float rather than fall back to 0.0.

Parameters:
NameTypeDescription
valuedynamic

a numeric field off a media object, a settings node, or a literal

Returns:
  • the numeric value, or 0.0 for invalid / AA / array / boolean input.
Type: 
float

(static) displayToast(message, toastTypeopt) → {void}

displayToast: Display a transient toast notification via the scene-level Toast component.

Parameters:
NameTypeAttributesDefaultDescription
messagestring

The message to display

toastTypestring<optional>
"error"

"error", "success", or "info"

Returns:
Type: 
void

(static) divCeiling(a, b) → {integer}

Parameters:
NameTypeDescription
ainteger
binteger
Returns:
Type: 
integer

(static) filterNodeArray(nodeArray, nodeKey, excludeArray) → {object}

Filters an array of nodes, excluding any whose nodeKey value appears in excludeArray

Parameters:
NameTypeDescription
nodeArrayobject

Array of nodes to filter

nodeKeystring

Field name to check on each node

excludeArrayobject

Array of values to exclude

Returns:

Filtered array with excluded nodes removed

Type: 
object

(static) findNodeBySubtype(node, subtype) → {dynamic}

Parameters:
NameTypeDescription
nodedynamic
subtypedynamic
Returns:
Type: 
dynamic

(static) findParentOfType(node, typeName) → {object}

Traverses up the node hierarchy to find a parent node of a specific type This is useful for finding the main component when handling button events

Parameters:
NameTypeDescription
nodeobject
typeNamestring
Returns:
Type: 
object

(static) findParentWithField(node, fieldName) → {object}

Traverses up the node hierarchy to find a parent node that has a specific field This is useful for finding the main component when handling button events

Parameters:
NameTypeDescription
nodeobject
fieldNamestring
Returns:
Type: 
object

(static) formatDisambiguatedTime(time) → {string}

The one way to render a time the reader will read off against an on-screen clock — the "Ends at" value in ItemDetails and in the playback OSD. Bare when the half of the day is inferable, suffixed when it isn't, so the same end time reads identically on both screens.

Parameters:
NameTypeDescription
timeobject

a LOCAL roDateTime (caller must have applied toLocalTime())

Returns:
Type: 
string

(static) formatIsoDateVideo(isoDateString) → {string}

convert iso date string to a human readable date string eg. "March 13th, 2014"

Parameters:
NameTypeDescription
isoDateStringstring
Returns:
Type: 
string

(static) formatTime(time) → {string}

Format time as 12 or 24 hour format based on system clock setting NOTE: This is NOT used by app's clock, only for displaying time in dialogs

Parameters:
NameTypeDescription
timedynamic
Returns:
Type: 
string

(static) getActiveView() → {object}

Returns the full-screen view currently on top — the router-mounted view tracked on m.global.activeRoutedView. The whole app is routed: pre-login, content, and playback are all router views in JRScene's outlet. Used by main.bs's playback/dialog/device code and by JRScene's options key to resolve "what's on screen".

Returns:
Type: 
object

(static) getApiVersionFromGlobal() → {integer}

Returns the current API version from global state with a safe default of 2. Works in both class and non-class contexts (components, namespaces, functions). This is the single source of truth for reading m.global.server.apiVersion.

Returns:

API version (default: 2 if not set, unset (0), or invalid)

Type: 
integer

(static) getButton(msg, subnodeopt) → {object}

Parameters:
NameTypeAttributesDefaultDescription
msgdynamic
subnodestring<optional>
"buttons"
Returns:
Type: 
object

(static) getDisambiguatingTimePeriod(time) → {string}

Returns "am"/"pm" ONLY when the suffix disambiguates the time against the current clock, empty string otherwise (and always empty on a 24h clock).

Why this exists: the clock the reader compares against renders bare in 12h mode — "10:32", never "10:32 pm" (see Clock.bs, used by both the overhang and the playback OSD). A time measured against that clock therefore doesn't need a meridian either, UNLESS it crosses noon or midnight, or sits beyond the horizon above — the cases the reader can't infer at a glance. Only for now-anchored times; see getTimePeriod() for absolute ones.

Parameters:
NameTypeDescription
timeobject

a LOCAL roDateTime (caller must have applied toLocalTime())

Returns:
Type: 
string

(static) getFirstAudioStreamIndex(streams) → {integer}

Returns the Jellyfin index of the first audio stream in the streams array

Parameters:
NameTypeDescription
streamsdynamic

Array of media streams from Jellyfin

Returns:
  • Jellyfin index of first audio stream, or 0 if not found
Type: 
integer

(static) getFirstVideoStream(mediaStreams) → {dynamic}

getFirstVideoStream: Finds the first video stream in MediaStreams array MediaStreams[0] is not always a video stream - it could be subtitle or audio

Parameters:
NameTypeDescription
mediaStreamsroArray

Array of media streams from playback info

Returns:

First video stream object, or invalid if not found

Type: 
dynamic

(static) getGlobalSplashScreenSetting() → {string}

getGlobalSplashScreenSetting: Gets the global splash screen setting from registry or default

Reads from global registry first. If not found (user never changed it), reads default from settings.json (single source of truth). Works before user login since global settings are device-wide.

Returns:
  • Global splash screen setting value ("enabled" or "disabled")
Type: 
string

(static) getMinutes(ticks) → {integer}

Converts ticks to minutes

Parameters:
NameTypeDescription
ticksdynamic
Returns:
Type: 
integer

(static) getMsgPicker(msg, subnodeopt) → {object}

Parameters:
NameTypeAttributesDefaultDescription
msgdynamic
subnodestring<optional>
""
Returns:
Type: 
object

(static) getRokuAudioTrackPosition(jellyfinAudioIndex, audioStreamsArray, availableAudioTracksopt) → {string}

Converts a Jellyfin audio stream index to a Roku audioTrack Track identifier. For MKV direct play, Roku exposes tracks using 1-based MKV track numbers that include ALL track types (video, audio, subtitle). Jellyfin's stream Index is 0-based and also counts all track types, so Roku Track = Jellyfin Index + 1.

When availableAudioTracks is provided (populated by Roku during active playback), matches by language with ordinal disambiguation: among same-language Jellyfin streams (in original order), find the position N of the chosen index, then return the Nth same-language Roku track. This avoids returning the wrong track when multiple streams share a language (e.g. main + commentary, both English — issue #500). Falls back to Index + 1 mapping when availableAudioTracks is not available or no ordinal match can be found (e.g., before playback starts or for non-MKV containers).

Parameters:
NameTypeAttributesDefaultDescription
jellyfinAudioIndexinteger

The Jellyfin audio stream index

audioStreamsArraydynamic

Jellyfin fullAudioData array (must have .index and .language fields)

availableAudioTracksdynamic<optional>
invalid

Roku's availableAudioTracks array (has .track and .language)

Returns:
  • Roku track identifier string for audioTrack field
Type: 
string

(static) getTimePeriod(time) → {string}

Returns "am" or "pm" for 12h clock, empty string for 24h. Use for times the reader has no on-screen reference for — a broadcast range that may be hours or days away. For a time anchored to "now", prefer formatDisambiguatedTime().

Parameters:
NameTypeDescription
timeobject
Returns:
Type: 
string

(static) inArray(haystack, needle) → {boolean}

Search string array for search value. Return if it's found

Parameters:
NameTypeDescription
haystackdynamic
needledynamic
Returns:
Type: 
boolean

(static) inferServerUrl(url) → {string}

take an incomplete url string and use it to make educated guesses about the complete url. then tests these guesses to see if it can find a jf server returns the url of the server it found, or an empty string

Parameters:
NameTypeDescription
urlstring
Returns:
Type: 
string

(static) injectApiParams(params, apiVersion, imageDefaults) → {object}

injectApiParams: Pure function to inject default image parameters and version-specific fields Separated from ApiClient for testability without network I/O

Parameters:
NameTypeDescription
paramsobject

Original query parameters from caller

apiVersioninteger

API version (1 for pre-10.9, 2+ for 10.9+)

imageDefaultsobject

Default image parameters (e.g., { EnableImageTypes: "Primary,Backdrop,Logo,Thumb", ImageTypeLimit: 1 })

Returns:

New params object with defaults merged and version-specific fields added

Type: 
object

(static) isAllValid(input) → {boolean}

Returns whether or not all items in passed array are valid

Parameters:
NameTypeDescription
inputobject
Returns:
Type: 
boolean

(static) isBooleanTrue(value) → {boolean}

True only for a real boolean true.

value = true faults &h18 on a non-boolean operand, so the TYPE has to be established before the comparison. A field the server omitted dereferences to Invalid, which is exactly the case a bare if stream.IsAnamorphic would fault on. ParseJson yields an unboxed "Boolean" and an AA literal a boxed "roBoolean" — both are genuine booleans, so both have to pass.

Lives here, beside isValid / mediaNumber / textOrEmpty, because it is the same kind of thing: a coercion for a field that may not be there. It used to live in fallbackFont.bs, which meant reaching it from anywhere else imported a module about FONTS to get a boolean guard — enough friction that the playback report grew a byte-identical private copy rather than pay it.

Parameters:
NameTypeDescription
valuedynamic

any value, including Invalid

Returns:
  • true when value is boolean true; false for every other type or value
Type: 
boolean

(static) isChainValid(root, propertyPath) → {boolean}

isChainValid: Returns whether or not all the properties in the passed property chain are valid. Stops evaluating at first found false value

Parameters:
NameTypeDescription
rootdynamic

high-level object to test property chain against

propertyPathstring

chain of properties under root object to test

Returns:

indicating if all properties in chain are valid

Type: 
boolean

(static) isJellyfinServer(systemInfo) → {boolean}

accepts the raw json string of /system/info/public and returns a boolean indicating if ProductName is "Jellyfin Server"

Parameters:
NameTypeDescription
systemInfoobject
Returns:
Type: 
boolean

(static) isLocalhost(url) → {boolean}

Returns true if the string is a loopback, such as 'localhost' or '127.0.0.1'

Parameters:
NameTypeDescription
urlstring
Returns:
Type: 
boolean

(static) isNodeEvent(msg, field) → {boolean}

Parameters:
NameTypeDescription
msgdynamic
fieldstring
Returns:
Type: 
boolean

(static) isValid(input) → {boolean}

Returns whether or not passed value is valid

Parameters:
NameTypeDescription
inputdynamic
Returns:
Type: 
boolean

(static) isValidAndNotEmpty(input) → {boolean}

Returns whether or not passed value is valid and not empty Accepts a string, or any countable type (arrays and lists)

Parameters:
NameTypeDescription
inputdynamic
Returns:
Type: 
boolean

(static) lastFocusedChild(obj) → {object}

Walk to the deepest focused descendant. Dialog helpers capture this before presenting, so closing can restore focus exactly where the user left it (source/utils/dialogs.bs presentOverlayDialog -> returnFocusTo).

Parameters:
NameTypeDescription
objobject
Returns:
Type: 
object

(static) leftPad(base, fill, length) → {string}

Parameters:
NameTypeDescription
basestring
fillstring
lengthinteger
Returns:
Type: 
string

(static) mediaNumber(value) → {integer}

A numeric field from a Jellyfin media object, as an integer.

Jellyfin's numeric fields (Height, Width, Channels, BitRate, Level) arrive as integers from ParseJson, but the app has always handled them arriving as strings too — isWithinH264HardwareCeiling, ItemPostPlaybackInfo's channel count and getDeviceProfile's MaxAudioChannels each carry their own inline copy of this coercion. This is that coercion, once.

Returns 0 for anything missing or non-numeric, so callers can compare without a validity check first — a stream that does not report a height has no height, and 0 is the honest reading of that for every comparison the report makes.

THE TYPE NAMES BELOW WERE MEASURED, NOT REASONED, and the same integer reports three different names depending on where it came from. On a Stick 4K (Roku OS 15.3.4):

type(2) -> "Integer" an unboxed literal type(aa.someInt) -> "roInteger" boxed, read out of an AA type(node.someIntField) -> "roInt" read off a SceneGraph node field

All three are listed because this function is handed all three: MediaStream fields arrive in AAs from ParseJson, user settings arrive from a JellyfinUserSettings node, and callers pass literals. Enumerating only the plausible-looking names is what produced the bug this function was extracted to fix — type(x) = "Integer" against a node field is ALWAYS false, so GetBitRateLimit discarded every user-set bitrate limit and silently substituted Roku's hardware ceiling.

NOT FOR BYTE COUNTS. The return type is a 32-bit Integer, so anything above 2,147,483,647 overflows — and MediaSourceInfo.Size is a byte count, which passes that at ~2.1 GB. Every other numeric media field (Height, Width, Channels, BitRate, Level, SampleRate, BitDepth) is comfortably inside the range. Format sizes from the raw value in floating point instead.

Parameters:
NameTypeDescription
valuedynamic

a numeric field off a media object, a settings node, or a literal

Returns:
  • the numeric value, or 0 for anything missing or non-numeric
Type: 
integer

(static) meridianDiffers(timeHours, nowHours) → {boolean}

True when two times fall in opposite halves of the day, i.e. one of them is on the far side of noon or midnight from the other. This is exactly when a 12h time needs an am/pm suffix to be readable against the other: same half means the suffix carries no information. Hours are 0-23 local. Injected (rather than read from roDateTime) so this stays unit-testable.

Parameters:
NameTypeDescription
timeHoursinteger
nowHoursinteger
Returns:
Type: 
boolean

(static) parseUrl(url) → {object}

Returns an array from a url = [ url, proto, host, port, subdir+params ] If port or subdir are not found, an empty string will be added to the array Proto must be declared or array will be empty

Parameters:
NameTypeDescription
urlstring
Returns:
Type: 
object

(static) primaryMediaSource(cachedPlaybackInfo) → {dynamic}

The MediaSource this playback is actually using, off the cached /PlaybackInfo response.

ONE definition because two surfaces describe the same streams — the playback-info report (PlayerHostView) and the playback-error Details readout (VideoPlayerView) — and two copies of "which source are we describing" is exactly how they would come to disagree about one stream.

MediaSources[0] is correct rather than merely convenient: the app asks the server for ONE source (the selected mediaSourceId), so the response carries the one that was negotiated for this play session, not a menu to pick from.

Parameters:
NameTypeDescription
cachedPlaybackInfodynamic

the cached ItemPostPlaybackInfo response

Returns:

the MediaSource AA, or invalid when nothing is cached

Type: 
dynamic

(static) resolveApiVersion(serverVersion) → {integer}

Maps a Jellyfin server version string to an API version integer used by the SDK dispatcher. V1: Covers pre-10.9 servers (10.7.x, 10.8.x) — uses /Users/{userId}/ path prefix V2: Covers 10.9+ servers — endpoints moved to top-level, userId passed as query param Returns 1 if serverVersion is empty/invalid (safe fallback to legacy paths)

Parameters:
NameTypeDescription
serverVersionstring
Returns:
Type: 
integer

(static) resolveSplashScreen(globalSplashSetting, serverSplashEnabled) → {boolean}

resolveSplashScreen: Resolves whether to show the splash screen on User Select screen

Checks JellyRock global setting. If "disabled", never shows splash. Otherwise follows server branding configuration. Ensures a valid boolean is always returned.

Parameters:
NameTypeDescription
globalSplashSettingdynamic

JellyRock global splash screen setting ("enabled", "disabled", or invalid)

serverSplashEnableddynamic

Server's splashscreenEnabled setting (boolean or invalid)

Returns:
  • Resolved splash screen enabled state (guaranteed boolean)
Type: 
boolean

(static) restoreFocus(view) → {void}

Restore focus to a view's saved descendant (lastFocus) if it's still valid, else focus the view root. The canonical focus-restore rule shared by the JRScreen lifecycle bridge (onScreenShown / handleFocus) and main.bs's side-panel handlers. No-op on an invalid view.

Parameters:
NameTypeDescription
viewobject
Returns:
Type: 
void

(static) roundNumber(f) → {integer}

Rounds number to nearest integer

Parameters:
NameTypeDescription
ffloat
Returns:
Type: 
integer

(static) routeForItem(item) → {object}

Maps a selected content item to its route — a named-route arg { name, params } for sgRouter.navigateTo. Pure (no router call) so it can live here in commonly-imported misc.bs; the routed view that owns the selection calls navigateTo itself (passing the rich node as context to avoid a re-fetch). Returns invalid for items that are NOT view navigation (Chapter → playback); the caller handles those via the playback path (quickPlayNode).

Parameters:
NameTypeDescription
itemobject
Returns:
Type: 
object

(static) secondsToTimestamp(totalSeconds, addLeadingMinuteZero) → {string}

Converts seconds to a human readable timestamp. Used for progress bar during playback eg. 3661 seconds = "01:01:01"

Parameters:
NameTypeDescription
totalSecondsinteger
addLeadingMinuteZeroboolean
Returns:
Type: 
string

(static) setFieldTextValue(field, value) → {void}

Parameters:
NameTypeDescription
fielddynamic
valuedynamic
Returns:
Type: 
void

(static) shuffleArray(array) → {object}

Takes an array of data, shuffles the order, then returns the array uses the Fisher-Yates shuffling algorithm

Parameters:
NameTypeDescription
arrayobject
Returns:
Type: 
object

(static) startLoadingSpinner(disableRemoteopt, loadingTextopt) → {void}

startLoadingSpinner: Start a loading spinner and attach it to the main JRScene. Displays an invisible ProgressDialog node by default to disable keypresses while loading.

Parameters:
NameTypeAttributesDefaultDescription
disableRemoteboolean<optional>
true
loadingTextstring<optional>
""
Returns:
Type: 
void

(static) stopLoadingSpinner() → {void}

Returns:
Type: 
void

(static) textOrEmpty(input) → {string}

Text from a field that may simply not exist.

toString() faults with a Type Mismatch on Invalid, because it falls through to Str(), so it can only be used where the value is known to be present. Optional fields — most of a Jellyfin MediaStream, every field a newer server added — need this instead: absent reads as "", which is what a caller building display text out of them wants anyway.

Parameters:
NameTypeDescription
inputdynamic

any value, including Invalid

Returns:
  • the text, or "" when there is nothing there
Type: 
string

(static) ticksToHuman(ticks) → {string}

Parameters:
NameTypeDescription
tickslonginteger
Returns:
Type: 
string

(static) ticksToMinutes(ticks) → {longinteger}

Converts ticks to number of minutes eg. 3661 totalSeconds = 61

Parameters:
NameTypeDescription
tickslonginteger
Returns:
Type: 
longinteger

(static) toBoolean(value) → {dynamic}

convert value to boolean and return value

Parameters:
NameTypeDescription
valuedynamic
Returns:
Type: 
dynamic

(static) toString(input) → {string}

Parameters:
NameTypeDescription
inputdynamic
Returns:
Type: 
string

(static) urlCandidates(input) → {dynamic}

this is the "educated guess" logic for inferServerUrl that generates a list of complete url's as candidates for the tests in inferServerUrl. takes an incomplete url as an arg and returns a list of extrapolated full urls.

Parameters:
NameTypeDescription
inputstring
Returns:
Type: 
dynamic

(static) versionChecker(versionToCheck, minVersionAccepted) → {dynamic}

Returns whether or not a version number (e.g. 10.7.7) is greater or equal to some minimum version allowed (e.g. 10.8.0)

Parameters:
NameTypeDescription
versionToCheckstring
minVersionAcceptedstring
Returns:
Type: 
dynamic