JellyRock Versioning Systems Overview
This document provides a high-level overview of how JellyRock handles multiple versioning systems to maintain compatibility across different Jellyfin server versions and Roku device capabilities.
Overview
Section titled “Overview”JellyRock supports Jellyfin servers from 10.7.0 through the latest version. To achieve this, the app implements several versioning layers that work together seamlessly.
Versioning Systems
Section titled “Versioning Systems”1. API Endpoint Versioning (apiVersion)
Section titled “1. API Endpoint Versioning (apiVersion)”Jellyfin 10.9 introduced breaking changes to user API endpoints by removing /Users/{userId}/ path prefixes. JellyRock transparently handles both endpoint styles through a dispatch layer.
How it works:
apiVersion = 1: 10.7.x - 10.8.x (legacy paths with/Users/{id}/prefix)apiVersion = 2: 10.9+ (top-level paths withuserIdquery parameter)ApiClientclass routes to the appropriate version (sdkV1orsdkV2) based onm.global.server.apiVersion- Static endpoints (shows, artists, movies, etc.) use
sdk.*directly without version dispatch
Key Files:
source/api/ApiClient.bs- Dispatcher that routes to appropriate version and injects defaultssource/api/sdk.bs- Base SDK with static endpoints (shows, artists, items, etc.)source/api/sdkV1.bs- 10.7.x - 10.8.x user endpoint implementationssource/api/sdkV2.bs- 10.9+ user endpoint implementations
2. Device Profile Versioning
Section titled “2. Device Profile Versioning”Device profiles describe what media formats the Roku device can play. Different server versions expect different profile structures.
How it works:
V1 Profile: 10.7.x - 10.8.x (includesIdentificationobject,SupportedMediaTypes,ResponseProfiles)V2 Profile: 10.9+ (simplified structure, supportsVideoRangeType)- Profile is generated based on detected API version
Key Differences:
V1includes DLNA related fields (Identification,SupportedMediaTypes)V2adds support forVideoRangeTypeforHDR/DoVidetectionV1does NOT supportVideoRangeTypein codec conditions (causes 400 errors)
Key Files:
source/utils/deviceCapabilities.bs- Generates the appropriate profile (V1vsV2selected internally based onm.global.server.apiVersion)
3. Field Availability Versioning (BaseItemDto)
Section titled “3. Field Availability Versioning (BaseItemDto)”Different Jellyfin versions return different fields in API responses. JellyRock handles this gracefully.
How it works:
- Fields added in 10.9+ are checked with
isValid()before accessing - Fields not available return gracefully with defaults
ApiClientautomatically injects required fields (EnableImageTypes,ImageTypeLimit)
Notable Field Differences:
| Field | 10.7.0 | 10.9+ | Handling |
|---|---|---|---|
Trickplay | ❌ | ✅ | Safe with isValid() check |
HasLyrics | ❌ | ✅ | Safe with ?? operator |
NormalizationGain | ❌ | ✅ | Safe with ?? operator |
VideoRangeType | ❌ | ✅ | Safe with isValid() checks |
ImageTags | ✅ | ✅ | Always requested via ApiClient |
SupportsSync | ✅ | ❌ | Not used in codebase |
Note: Some fields like ImageTags and BackdropImageTags are not in the ItemFields enum but are returned when EnableImageTypes is specified.
4. Version-Gated API Endpoints
Section titled “4. Version-Gated API Endpoints”Some Jellyfin API endpoints are only available on specific server versions. These are guarded by direct version checks using versionChecker() rather than the apiVersion dispatch system.
| Endpoint | Min Version | Guard Function | Purpose |
|---|---|---|---|
GET /MediaSegments/{itemId} | 10.10.0 | supportsMediaSegments() | Fetch intro/outro/recap/preview/commercial segments for skip functionality |
How it works:
- Guard functions call
versionChecker(m.global.server.version, "x.y.z")to compare the raw server version string - Callers check the guard before making the API request — the endpoint simply isn’t called on older servers
- No
apiVersiondispatch needed because these are top-level paths, not user-scoped endpoints
Endpoints that gracefully degrade (no explicit guard): some post-floor
endpoints have no version guard but are safe because a missing endpoint returns a
404 and the caller checks isValid() before use — e.g. Audio/{itemId}/Lyrics
(10.9+, no lyrics shown on older servers) and the Quick Connect probe /QuickConnect/Enabled
(10.8+, fail-open). These are not bugs, but they do trip the server-upgrade
pipeline’s floor-coverage check (we call an endpoint the 10.7.0 floor spec lacks).
Machine-readable registry: every post-floor endpoint and its old-server
handling (guard / dispatch-sibling / graceful-degradation) is recorded in
jellyfin-endpoint-availability.yml — the
server-upgrade pipeline’s validated disposition ledger
(server-upgrade-automation.md,
Phase 6). The table above is the human-readable mirror; the YAML is the source the
floor check consumes, and npm run lint:endpoint-availability validates each
entry’s guard/sibling claim against current code so a removed guard resurfaces the
finding. When you add a version-gated or post-floor endpoint, add a registry entry
(the lint will tell you if you forgot — the finding will flag needsInvestigation).
Media Segments (10.10.0+):
The MediaSegments API provides segment timing data (intro, outro, recap, preview, commercial, unknown) for video items. JellyRock fetches segments during video content loading and supports three action modes per segment type: auto-skip, show skip button, or do nothing. All segment types default to “show skip button” (AskToSkip). User action preferences are loaded from the server’s DisplayPreferences CustomPrefs (key format: segmentTypeAction__[Type]) with optional per-device overrides via JellyRock settings.
Key Files:
source/utils/mediaSegments.bs-supportsMediaSegments()guard,resolveSegmentAction(),findActiveSegment()source/enums/MediaSegmentType.bs- Segment type enum (Intro, Outro, Commercial, Preview, Recap, Unknown)source/enums/MediaSegmentAction.bs- Action mode enum (None, AskToSkip, Skip)source/api/ApiClient.bs-BuildGetMediaSegmentsRequest()request buildersource/api/items.bs-GetMediaSegments()helper (guards withsupportsMediaSegments())components/ItemGrid/LoadVideoContentTask.bs- Fetches segments after metadata loadcomponents/video/VideoNotification.bs- Reusable notification component for skip promptscomponents/video/VideoPlayerView.bs- Segment detection and action handling during playback
5. Authentication Compatibility
Section titled “5. Authentication Compatibility”Most authentication flows work across all versions, with one quirk for Quick Connect.
Quick Connect:
The two QC dispatches operate on different version boundaries — the SDK handles them separately:
| Concern | 10.7.x | 10.8.x | 10.9.0+ | Boundary |
|---|---|---|---|---|
AuthenticateWithQuickConnect body | { "Token": secret } | { "Secret": secret } | { "Secret": secret } | versionChecker(version, "10.8.0") |
/QuickConnect/Initiate HTTP method | GET | GET | POST | getApiVersionFromGlobal() >= 2 |
/QuickConnect/Connect | GET ?secret= | same | same | n/a |
/QuickConnect/Enabled (gating probe) | missing | present | present | fail-open in QuickConnectEnabledTask |
source/api/sdk.bs dispatches both at call time, so callers
(source/api/userAuth.bs, components/quickConnect/*) are version-agnostic.
The Token→Secret split happens at 10.8.0 (a finer boundary than the
apiVersion 1→2 split at 10.9.0), so it uses raw versionChecker rather than
the apiVersion integer.
Username/password authentication works identically across all versions.
6. Client Interface Versioning
Section titled “6. Client Interface Versioning”The GetApi() client provides a unified interface that hides version complexity:
' Same code works on all server versions; ApiClient routes V1/V2 internally.req = GetApi().BuildGetItemRequest(itemId, { fields: "Overview" })res = fetchRes(req, "myReq")if isValid(res) and res.ok then item = res.jsonAutomatic handling:
- Image parameters injected automatically (
EnableImageTypes,ImageTypeLimit) - Version-specific fields added conditionally (e.g., Trickplay for 10.9+)
UserIdautomatically retrieved from global state- API version automatically detected and routed
How Versioning Works Together
Section titled “How Versioning Works Together”User Action → GetApi().BuildGetItemRequest(...) ↓ ApiClient (dispatcher) ↓ Check m.global.server.apiVersion ↓ ├── apiVersion = 1 → /users/{userId}/items/{itemId} (V1 path) └── apiVersion = 2 → /Items/{itemId}?userId=... (V2 path) ↓ Static endpoints (shows, items, etc.) → sdk.* (single shape across versions) ↓ Device Profile (getDeviceProfile()) ↓ Check m.global.server.apiVersion ↓ ├── apiVersion = 1 → getDeviceProfileV1() └── apiVersion = 2 → getDeviceProfileV2()Version Detection
Section titled “Version Detection”Server version detection happens at login:
resolveApiVersion()checks server version string- Returns
1for 10.7.x - 10.8.x - Returns
2for 10.9+ - Stored in
m.global.server.apiVersion
All code references this value to determine behavior.
Files by Versioning System
Section titled “Files by Versioning System”| System | Key Files |
|---|---|
| API Endpoints | source/api/ApiClient.bs (dispatcher), source/api/sdk.bs (static), source/api/sdkV1.bs (V1 user), source/api/sdkV2.bs (V2 user) |
| Device Profile | source/utils/deviceCapabilities.bs (V1/V2 selection internal) |
| Field Handling | source/data/JellyfinDataTransformer.bs, source/api/ApiClient.bs |
| Version Detection | source/utils/misc.bs (resolveApiVersion), source/utils/session.bs |
| Version-Gated Endpoints | source/utils/mediaSegments.bs (supportsMediaSegments), source/api/items.bs (GetMediaSegments) |
Adding Support for New Server Versions
Section titled “Adding Support for New Server Versions”Guided path: the
/new-api-versionskill wraps this whole recipe (boundary map +resolveApiVersion()twin, thesdkVN.bsshim, dispatch branches, device profile, manifest clamp, docs + validators) and stops at each verify gate. Use it so a tier split can’t land half-built.
If a future Jellyfin release introduces breaking changes — including a cross-major jump like 12.0.0 (Jellyfin has discussed dropping the 10. prefix), which the version logic already handles since comparison is numeric-per-segment and the active tier is unbounded above:
- API Changes: Create
source/api/sdkV3.bswith new endpoint paths - Profile Changes: Add a
V3branch insource/utils/deviceCapabilities.bs(V1/V2are already internal selectors;V3follows the same pattern) - Update Detection: Modify
resolveApiVersion()to return3for the new minimum (e.g.12.0.0+). This must stay in lockstep with the boundary map —npm run lint:apiversion-consistency(scripts/lint/apiversion-consistency-check.js) statically parsesresolveApiVersion()and fails CI if its guards drift fromjellyfin-version-boundaries.yml. This is what lets you verify a tier split offline, with no Roku hardware. - Update Dispatchers: Add
apiVersion >= 3branches inApiClient.bs - Forward Compatibility: Existing
>= 2checks automatically fall through toV2until overridden
Also update the server-upgrade-automation pipeline
Section titled “Also update the server-upgrade-automation pipeline”The release-detection pipeline (server-upgrade-automation.md) is generalized to N tiers via range math, so its diff / join / floor / registry logic needs no rewrite when V3 lands — but it has two data/config touch-points that DO need updating, plus the manifest must be regenerated:
- Tier boundary map: in
jellyfin-version-boundaries.yml, flip tier2tostatus: frozenwith a concretemaxServer(the last 10.x release before 11.0), and add tier3withstatus: active,maxServer: null. (The loader requires exactly one active tier, and it must be the unbounded one.) - Manifest tier clamp: in
scripts/generate/api-usage-manifest.js, the cross-function clamp that pinssdkV1.bs → max ≤ 1/sdkV2.bs → min ≥ 2needs asdkV3.bs → min ≥ 3line, and thesdkV2.bsline gains amax ≤ 2clamp (V2 becomes the frozen middle tier). Then regenerate:npm run docs:api-manifest. ThegetApiVersion() >= 3branches added in the dispatcher step above are picked up automatically (the extractor reads the literalN).
After those, regenerate/commit a fresh baseline fingerprint at the next acknowledged release as usual — the floor (10.7.0) stays put forever (we support the oldest servers indefinitely), so the backward + symmetry + endpoint-availability checks need no change.
Reacting proactively (RC + master/unstable)
Section titled “Reacting proactively (RC + master/unstable)”You don’t have to wait for a stable release to find out what breaks. The server-upgrade pipeline can diff against a release candidate or a master/unstable build, so you can start tier work (above) before the final ships:
- Triage an RC or master build:
/server-upgrade <from> <to>where<to>is an RC (10.12.0-rc1), the literalunstable/master(resolves to the latest immutable datestamped build), or an explicit datestamp. It investigates locally and writes a.claude/handoffs/note — it files no GitHub issues (durable filing is the stable flow when the final lands). - Re-diff as it changes: RCs and master move. After triaging
rc1, set thejellyfin-server-rcsignal’slatest_acknowledged = <base>-rc1; whenrc2lands,/server-upgradediffsrc1 → rc2, surfacing only the delta since your proactive work. For master, the pinned datestamp lives in the handoff and becomes the next<from>. - Why ephemeral: pre-release fingerprints are built in-memory from the permanent archive (
--fetch), never committed — RC/master builds are throwaway anchors. See server-upgrade-automation.md → “Pre-release channels”.
When that triage shows a breaking shift warranting a new tier, the follow-up is /new-api-version, which automates the recipe above. RCs can still change before release — always re-run /server-upgrade against the final stable once it ships.
Related Documentation
Section titled “Related Documentation”docs/user/jellyfin-server-feature-matrix.md- User-facing feature support by server versiondocs/dev/new-user-setting.md- How to add version-aware settingsdocs/dev/registry-migrations.md- Handling data migrations across versions