Methods

(static) buildReplayRoutes(stashed) → {object}

Pure: turn a guard-stashed path into the ordered route chain to replay. Home is always first (back-stack root); a /play path additionally inserts its Details screen between Home and the player (the details path is the play path minus the trailing "/play").

Parameters:
NameTypeDescription
stasheddynamic
Returns:
Type: 
object

(static) deepLinkDetailsRoute(deepLink) → {string}

Build the /details route for a deep link. The :type is just a display hint (ItemDetails resolves the REAL type from the fetched item). The ?deeplink=query does double duty: it marks this as an untrusted deep-link entry (so ItemDetails validates the id, sending an invalid one back to Home per cert), AND it carries the intent — "open" lands on the springboard; "play"/"shuffle"/"trailer" auto-launch the matching action once the item resolves. The intent is the SENDER'S explicit choice (the route decides), never inferred from the resolved item. The "Playing" toast is fired by the player on mount using the REAL resolved title (ItemDetails sets m.global.deepLinkOpeningTitle) — not the sender's param, which may be a placeholder. So nothing is toasted here.

Parameters:
NameTypeDescription
deepLinkobject
Returns:
Type: 
string

(static) findSavedServerByGuid(guid) → {object}

Look up a saved server entry by its Jellyfin server id (GUID). Returns the entry ({ name, id, baseUrl, originalUrl, ... }) or invalid. The pure match logic lives in config.bs (findServerInList) next to the saved_servers shape it matches.

Parameters:
NameTypeDescription
guidstring
Returns:
Type: 
object

(static) isDeepLinkPlayerView(subtype) → {boolean}

Active routed views that ARE a media player (a deep link arriving over one must tear it down before launching the next, not stack on top).

Parameters:
NameTypeDescription
subtypestring
Returns:
Type: 
boolean

(static) isPlausibleDeepLinkItemId(id) → {boolean}

A deep-link itemId is concatenated raw into the details route AND the metadata-fetch URL ("/Items/"), neither of which is URL-encoded. Reject an id carrying URL-structural characters (/ ? # & = | % or whitespace) so a crafted contentId can't reshape either — a whitelist of the characters real Jellyfin ids (GUIDs, hex +/- dashes) actually use. Deliberately NOT a strict GUID match: the goal is to block injection, not to assume an exact id format.

Parameters:
NameTypeDescription
idstring
Returns:
Type: 
boolean

(static) isPlausibleDeepLinkToken(value) → {boolean}

Safe-token check for deep-link fields that ride UNENCODED into the details route — mediaType in the PATH ("/details//...") and action in the QUERY ("?deeplink="). Both come from sender-controlled launch params. Blocks the URL-structural characters (/ ? # & = | % and whitespace) a crafted value would use to inject extra path/query segments, while still allowing clean-but-unknown values through — a future Jellyfin item type or a forward-compat action verb degrades gracefully downstream (ItemDetails resolves the real type; an unknown action is treated as "open"). Mirrors isPlausibleDeepLinkItemId's whitelist approach.

Parameters:
NameTypeDescription
valuestring
Returns:
Type: 
boolean

(static) notifyDeepLinkDeferred() → {void}

Toast shown when a deep link arrives but the user must authenticate first — it explains why nothing happened yet and that the content will open after sign-in. No-op if nothing stashed.

Returns:
Type: 
void

RUNTIME (app already signed in): resolve the stashed deep link's server and act.

  • no/absent server, or it's the active one -> replay now.
  • a different SAVED server -> prompt to switch.
  • an unknown server -> can't connect; toast + discard. When NOT signed in (user at pre-login), do nothing: the stash rides until login and validates on whatever server they sign into.
Returns:
Type: 
void

(static) onServerProbeDone(reachable) → {void}

Continuation of performServerSwitch once the off-thread reachability probe returns. On an unreachable target, surface the error and discard the deep link. On success, reuse the Change-Server machinery (resetRouter -> point at the new server -> SignOut -> reenterLogin); the stashed deep link survives the teardown and replays once the new server's login completes.

Parameters:
NameTypeDescription
reachableboolean
Returns:
Type: 
void

(static) parseDeepLinkContentId(raw) → {object}

Parse a Roku deep-link contentId into { itemId, serverGuid, action }. contentId is the only deep-link field that survives BOTH Roku ingress paths intact (cold-start /launch standardizes only contentId+mediaType; /input forwards arbitrary params but /launch does not), and Roku forbids "&" inside it — so the whole contract rides inside contentId, pipe-delimited (Roku's own documented convention, e.g. series=x|Season=1). We mint it as "id=|serverId=|action=" (key names mirror Jellyfin web's ?id=&serverId=), and still accept a bare itemId (manual ECP tests, legacy). serverGuid is "" when absent (=> the active server). action defaults to "open" (land on the page, no autoplay) and is normalized to lower case; the consumer treats any UNKNOWN action as "open" too, so the contract is forward-compatible — a future verb degrades gracefully on an older build. Duplicate keys are LAST-WINS ("id=a|id=b" => "b") — an emergent property of the loop, pinned by a spec so a refactor can't silently change it. A value may itself contain "=" (we split each pair on the FIRST "=" only), so a key=value whose value carries an "=" is preserved, not dropped.

Parameters:
NameTypeDescription
rawstring
Returns:
Type: 
object

(static) performServerSwitch(entry) → {void}

Perform the confirmed switch. Pre-flight the target FIRST (non-destructive) so an offline server never strands the user. The probe runs on a Task thread (ServerReachableTask) so the up-to-8s reachability check can't freeze remote input; the switch itself continues in onServerProbeDone once the verdict lands back on the main loop.

Parameters:
NameTypeDescription
entryobject
Returns:
Type: 
void

(static) promptServerSwitch(entry) → {void}

Ask before switching servers (it signs the user out of the current one). The choice comes back through SceneManager.returnData -> main.bs's isDataReturned handler (the same bridge the exit dialog uses), which calls performServerSwitch on confirm.

Parameters:
NameTypeDescription
entryobject
Returns:
Type: 
void

(static) replayAfterLogin() → {void}

Read + clear whatever is stashed and replay it after a successful login (or immediately when already authed). Deep link wins over a guard path. Always navigates Home first so it is the back-stack root.

Returns:
Type: 
void

(static) replayDeepLinkRuntime() → {void}

Runtime replay (app already signed in, deep link for the active server). Unlike the post-login path above, this does NOT prepend Home — an in-app cast shouldn't flash through the Home screen.

  • already on the target item's LOADED details -> launch the action in place (no fetch, no re-mount); for "open" we're already showing it, so nothing to do. The itemContent guard matters: a lingering empty details would otherwise swallow the cast.
  • otherwise -> hand the intent to JRScene.resolveDeepLink, which VALIDATES the id (a fetch) BEFORE navigating. An invalid id never disturbs the active session — it just toasts. A valid id then navigates (replacing an active player for a playback cast, else a plain push).
Returns:
Type: 
void

Stash a deep link (cold-start or runtime) as a structured intent for replayAfterLogin. Parses the contentId; records itemId + mediaType (+ serverGuid for the server resolver). Replaces the old queue-seed approach — ItemDetails now builds the queue from the validated item (real type + bookmark), so no queue work happens here. itemName is an OPTIONAL display title the sender (a casting client) may include so the server-switch prompt can name what's being cast — we can't fetch it ourselves before the switch (it lives on a server we're not yet authed to). Empty for bare/unknown senders.

Parameters:
NameTypeAttributesDefaultDescription
rawContentIdstring
mediaTypestring
itemNamestring<optional>
""
Returns:
Type: 
void

(static) steerColdStartDeepLinkServer() → {void}

COLD start: if the stashed deep link names a saved server, point the login at it BEFORE beginLogin runs (no prompt — there is no active session to disrupt). An unknown/absent GUID is left alone: login proceeds normally and the deep link validates on whatever server we land on (graceful error -> Home if the item isn't there).

Returns:
Type: 
void

(static) wouldStackOverActivePlayer(action, activeSubtype) → {boolean}

True when a non-playback ("open") deep link would stack a details screen on TOP of an active media player, and so must be dropped rather than enacted. jellyfin-web's display-mirroring fires a DisplayContent ("open") on EVERY item-detail browse while JellyRock is the cast target, and the Roku OS can hand us an "open" deep link mid-playback — neither is an intent to interrupt playback, so the controller's browsing must never yank the cast target off the video. A PLAYBACK action (play/shuffle/trailer/instantmix) is exempt: it legitimately REPLACES the player (replacePlayer below). activeSubtype is the active routed view's subtype ("" / invalid when there's no active view). Pure so the rule is unit-testable without a live node.

Parameters:
NameTypeDescription
actionstring
activeSubtypedynamic
Returns:
Type: 
boolean