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, the 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
- Source
Methods
(static) AssocArrayEqual(Array1, Array2) → {boolean}
| Name | Type | Description |
|---|---|---|
Array1 | object | |
Array2 | object |
- Source
- Type:
- boolean
(static) addOrdinalSuffix(day) → {string}
| Name | Type | Description |
|---|---|---|
day | integer |
- Source
- 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.
- Source
- Type:
- object
(static) arrayHasValue(arr, value) → {boolean}
Check if a specific value is inside of an array
| Name | Type | Description |
|---|---|---|
arr | object | |
value | dynamic |
- Source
- 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.
| Name | Type | Description |
|---|---|---|
playStart | integer | epoch seconds when the program started (0 = unknown) |
playDuration | integer | program length in seconds (0 = unknown) |
nowSeconds | integer | current epoch seconds (injected for testability) |
- Source
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.
| Name | Type | Description |
|---|---|---|
value | dynamic | a numeric field off a media object, a settings node, or a literal |
- Source
- 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.
| Name | Type | Attributes | Default | Description |
|---|---|---|---|---|
message | string | The message to display | ||
toastType | string | <optional> | "error" | "error", "success", or "info" |
- Source
- Type:
- void
(static) filterNodeArray(nodeArray, nodeKey, excludeArray) → {object}
Filters an array of nodes, excluding any whose nodeKey value appears in excludeArray
| Name | Type | Description |
|---|---|---|
nodeArray | object | Array of nodes to filter |
nodeKey | string | Field name to check on each node |
excludeArray | object | Array of values to exclude |
- Source
Filtered array with excluded nodes removed
- Type:
- object
(static) findNodeBySubtype(node, subtype) → {dynamic}
| Name | Type | Description |
|---|---|---|
node | dynamic | |
subtype | dynamic |
- Source
- Type:
- dynamic
(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.
| Name | Type | Description |
|---|---|---|
time | object | a LOCAL roDateTime (caller must have applied toLocalTime()) |
- Source
- Type:
- string
(static) formatIsoDateVideo(isoDateString) → {string}
convert iso date string to a human readable date string eg. "March 13th, 2014"
| Name | Type | Description |
|---|---|---|
isoDateString | string |
- Source
- Type:
- string
(static) formatTime(time) → {string}
Format time as 12 or 24 hour format based on system clock setting The app's clock (Clock.bs) does not use this; it formats times shown on screens and in dialogs
| Name | Type | Description |
|---|---|---|
time | dynamic |
- Source
- 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. Callers use it to resolve "what's on screen".
- Source
- 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.
- Source
API version (default: 2 if not set, unset (0), or invalid)
- Type:
- integer
(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. Those are the cases the reader can't infer at a glance. Only for now-anchored times; see getTimePeriod() for absolute ones.
| Name | Type | Description |
|---|---|---|
time | object | a LOCAL roDateTime (caller must have applied toLocalTime()) |
- Source
- Type:
- string
(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
| Name | Type | Description |
|---|---|---|
mediaStreams | roArray | Array of media streams from playback info |
- Source
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.
- Source
- Global splash screen setting value ("enabled" or "disabled")
- Type:
- string
(static) getMsgPicker(msg, subnodeopt) → {object}
| Name | Type | Attributes | Default | Description |
|---|---|---|---|---|
msg | dynamic | |||
subnode | string | <optional> | "" |
- Source
- 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 (for example main + commentary, both English: issue #500). Falls back to Index + 1 mapping when availableAudioTracks is not available or no ordinal match can be found (for example before playback starts or for non-MKV containers).
| Name | Type | Attributes | Default | Description |
|---|---|---|---|---|
jellyfinAudioIndex | integer | The Jellyfin audio stream index | ||
audioStreamsArray | dynamic | Jellyfin fullAudioData array (must have .index and .language fields) | ||
availableAudioTracks | dynamic | <optional> | invalid | Roku's availableAudioTracks array (has .track and .language) |
- Source
- 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().
| Name | Type | Description |
|---|---|---|
time | object |
- Source
- Type:
- string
(static) inArray(haystack, needle) → {boolean}
Search string array for search value. Return if it's found
| Name | Type | Description |
|---|---|---|
haystack | dynamic | |
needle | dynamic |
- Source
- 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
| Name | Type | Description |
|---|---|---|
url | string |
- Source
- 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
| Name | Type | Description |
|---|---|---|
params | object | Original query parameters from caller |
apiVersion | integer | API version (1 for pre-10.9, 2+ for 10.9+) |
imageDefaults | object | Default image parameters (for example { EnableImageTypes: "Primary,Backdrop,Logo,Thumb", ImageTypeLimit: 1 }) |
- Source
New params object with defaults merged and version-specific fields added
- Type:
- object
(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 real 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. That was enough friction that the playback report grew a byte-identical private copy rather than pay it.
| Name | Type | Description |
|---|---|---|
value | dynamic | any value, including Invalid |
- Source
- 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
| Name | Type | Description |
|---|---|---|
root | dynamic | high-level object to test property chain against |
propertyPath | string | chain of properties under root object to test |
- Source
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"
| Name | Type | Description |
|---|---|---|
systemInfo | object |
- Source
- Type:
- boolean
(static) isLocalhost(url) → {boolean}
Returns true if the string is a loopback, such as 'localhost' or '127.0.0.1'
| Name | Type | Description |
|---|---|---|
url | string |
- Source
- Type:
- boolean
(static) isNodeEvent(msg, field) → {boolean}
| Name | Type | Description |
|---|---|---|
msg | dynamic | |
field | string |
- Source
- Type:
- boolean
(static) isValid(input) → {boolean}
Returns whether or not passed value is valid
| Name | Type | Description |
|---|---|---|
input | dynamic |
- Source
- 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)
| Name | Type | Description |
|---|---|---|
input | dynamic |
- Source
- 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).
| Name | Type | Description |
|---|---|---|
obj | object |
- Source
- Type:
- object
(static) leftPad(base, fill, length) → {string}
| Name | Type | Description |
|---|---|---|
base | string | |
fill | string | |
length | integer |
- Source
- 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 (Height and Width) and buildPlaybackInfoPostData in source/api/items.bs (the audio stream's Channels and the transcoding profile's MaxAudioChannels) still carry their own inline copies 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(parsed.someInt) -> "Integer" read out of an AA from ParseJson type(aa.someInt) -> "roInteger" boxed, read out of an AA literal type(node.someIntField) -> "roInt" read off a SceneGraph node field
(The ParseJson row, and the AA-literal and node rows again, measured 2026-10-11 on a Stick 4K, including an AA read back through an assocarray node field.)
All three are listed because this function is handed all three: MediaStream fields arrive from ParseJson ("Integer") or read back through a node field ("roInt"), user settings arrive from a JellyfinUserSettings node, and callers and specs pass literals, an AA literal among them ("roInteger"). 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.
| Name | Type | Description |
|---|---|---|
value | dynamic | a numeric field off a media object, a settings node, or a literal |
- Source
- 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, that is, 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.
| Name | Type | Description |
|---|---|---|
timeHours | integer | |
nowHours | integer |
- Source
- 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
| Name | Type | Description |
|---|---|---|
url | string |
- Source
- 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 in PlayerHostView and the playback-error Details readout in 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.
| Name | Type | Description |
|---|---|---|
cachedPlaybackInfo | dynamic | the cached ItemPostPlaybackInfo response |
- Source
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)
| Name | Type | Description |
|---|---|---|
serverVersion | string |
- Source
- 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.
| Name | Type | Description |
|---|---|---|
globalSplashSetting | dynamic | JellyRock global splash screen setting ("enabled", "disabled", or invalid) |
serverSplashEnabled | dynamic | Server's splashscreenEnabled setting (boolean or invalid) |
- Source
- 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.
| Name | Type | Description |
|---|---|---|
view | object |
- Source
- Type:
- void
(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 (a Chapter starts playback); the caller handles those via the playback path (quickPlayNode).
| Name | Type | Description |
|---|---|---|
item | object |
- Source
- 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"
| Name | Type | Description |
|---|---|---|
totalSeconds | integer | |
addLeadingMinuteZero | boolean |
- Source
- Type:
- string
(static) setFieldTextValue(field, value) → {void}
| Name | Type | Description |
|---|---|---|
field | dynamic | |
value | dynamic |
- Source
- Type:
- void
(static) shuffleArray(array) → {object}
Takes an array of data, shuffles the order, then returns the array uses the Fisher-Yates shuffling algorithm
| Name | Type | Description |
|---|---|---|
array | object |
- Source
- 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.
| Name | Type | Attributes | Default | Description |
|---|---|---|---|---|
disableRemote | boolean | <optional> | true | |
loadingText | string | <optional> | "" |
- Source
- Type:
- void
(static) stopLoadingSpinner() → {void}
- Source
- Type:
- void
(static) textOrEmpty(input) → {string}
Text from a field that may 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.
| Name | Type | Description |
|---|---|---|
input | dynamic | any value, including Invalid |
- Source
- the text, or "" when there is nothing there
- Type:
- string
(static) ticksToHuman(ticks) → {string}
| Name | Type | Description |
|---|---|---|
ticks | longinteger |
- Source
- Type:
- string
(static) ticksToMinutes(ticks) → {longinteger}
Converts ticks to number of minutes eg. 3661 totalSeconds = 61
| Name | Type | Description |
|---|---|---|
ticks | longinteger |
- Source
- Type:
- longinteger
(static) toBoolean(value) → {dynamic}
convert value to boolean and return value
| Name | Type | Description |
|---|---|---|
value | dynamic |
- Source
- Type:
- dynamic
(static) toString(input) → {string}
| Name | Type | Description |
|---|---|---|
input | dynamic |
- Source
- 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.
| Name | Type | Description |
|---|---|---|
input | string |
- Source
- Type:
- dynamic
(static) versionChecker(versionToCheck, minVersionAccepted) → {dynamic}
Returns whether or not a version number (for example 10.7.7) is greater or equal to some minimum version allowed (for example 10.8.0)
| Name | Type | Description |
|---|---|---|
versionToCheck | string | |
minVersionAccepted | string |
- Source
- Type:
- dynamic