Skip to content
Donate

API layering guide

JellyRock builds API requests and image URLs in three layers. Use the highest layer that does what you need: it handles more for you.

LayerWhereWhat it adds
3: domain helperssource/utils/itemImageUrl.bs, source/utils/rowItemImage.bs, source/api/imageHelpers.bsReads what it needs from a JellyfinBaseItem or JellyfinUser node, and falls back to other images
2: image URLssource/api/image.bsChecks the image tag, and sets default sizes and quality
1: API clientsource/api/ApiClient.bsBuilds each endpoint’s request for the server’s API version, and adds default query parameters

Call it through GetApi().<method>(). ApiClient builds requests for the Jellyfin endpoints and picks the right form for the server’s API version (V1 or V2). See Jellyfin server versioning for what the versions mean.

Builders that fetch items pass your parameters through injectApiParams() (in source/utils/misc.bs), which adds:

ParameterDefaultWhy
EnableImageTypes"Primary,Backdrop,Logo,Thumb"Asks for every image type the app shows
ImageTypeLimit1One image per type
fieldsYour fields, plus those belowThe app needs them on every item

It adds these to fields when they are missing:

  • PrimaryImageAspectRatio, which makes the server return ImageTags.
  • Chapters.
  • Trickplay, on V2 servers (Jellyfin 10.9 and later).

Your own values override the image defaults. BuildGetItemRawRequest skips all of this.

One builder sets a default of its own:

BuilderParameterDefaultWhy
BuildGetResumeItemsRequestMediaTypes"Video"MediaTypes is the only filter /UserItems/Resume accepts. Since Jellyfin 12.0 a folder also counts as resumable when something inside it is in progress, so without the filter the results include seasons and series as well as episodes (#784). To change it, pass a comma-separated string. buildParams drops an array (buildparams-no-array-support), which leaves the default.
  • You need full control of the parameters. Values you pass override the defaults.
  • You are writing a layer 2 or layer 3 helper.
  • The endpoint has no helper above it.

The two methods add no defaults and check nothing. Use the one that matches what the image belongs to:

MethodForEndpoint
GetImageURL(id, type, index, params)Items (movies, episodes and so on)/items/{id}/images/{type}/{index}
GetUserImageURL(id, type, index, params)User avatarsV1: /users/{id}/images/{type}/{index}
V2: /UserImage?userId={id}
url = GetApi().GetUserImageURL(userId, "primary", 0, {
maxHeight: 300,
maxWidth: 300,
quality: 90
})
' V1: "http://server:8096/users/abc123/images/primary/0?maxHeight=300..."
' V2: "http://server:8096/UserImage?userId=abc123&type=primary..."

Import pkg:/source/api/image.bs. Its two functions return a URL string, or "" when there is nothing to show.

FunctionForDefaults
ImageURL(id, version, params, serverURL)Item imagesmaxHeight 384, maxWidth 196, quality 90
UserImageURL(id, params)User avatarsmaxHeight 300, maxWidth 300, quality 90

Both check the tag. If params has a tag (or Tag) that is invalid or empty, they return "", so the app never requests an image the server doesn’t have. Both also return "" when no server URL is set.

UserImageURL calls layer 1’s GetUserImageURL. ImageURL builds the item URL itself, and takes the server URL as its last argument when the caller already has it.

  • You have an ID and a tag, but no node.
  • You want the default sizes, or the tag check, without a layer 3 helper.
url = UserImageURL(userId, {
tag: user.primaryImageTag,
maxHeight: 36,
maxWidth: 36
})
' "" when the tag is invalid, otherwise the URL with the defaults filled in

These take a JellyfinBaseItem or JellyfinUser node, read the IDs and tags from it, and fall back to other images when the first choice is missing. Item helpers take a size from the imageSize namespace (source/constants/imageSize.bs).

FunctionTakesTries, in order
getItemPosterUrl(item, size)JellyfinBaseItemItem primary, parent primary, series primary
getItemParentPosterUrl(item, size)JellyfinBaseItemParent primary, series primary, skipping the item’s own image
getItemWidePosterUrl(item, size)JellyfinBaseItemItem thumb, item backdrop, parent thumb, parent backdrop
getItemBackdropUrl(item, size)JellyfinBaseItemItem backdrop, parent backdrop
getItemParentWidePosterUrl(item, size)JellyfinBaseItemParent thumb, parent backdrop
getItemImageUrl(item, imageType, size)JellyfinBaseItemOnly the type you ask for, such as "Logo"
getProgramImageUrl(programItem, channelItem, size)Two JellyfinBaseItem nodesThe Live TV program’s image, then its channel’s
getRowItemImageUrl(item, slotWidth, posterHeight, userSettings, …)JellyfinBaseItemPicks one of these from the item type, the slot size and the user’s settings
GetUserAvatarURL(user, maxHeight, maxWidth)JellyfinUserPrimary only, with the tag check
  • You have a JellyfinBaseItem or JellyfinUser node.
  • You want the fallbacks.
  1. You have a JellyfinBaseItem or JellyfinUser node: layer 3.
  2. You have an ID and a tag, or you want the default sizes: layer 2.
  3. You need full control of the parameters, or you are writing a helper: layer 1.
import "pkg:/source/api/imageHelpers.bs"
sub loadUserImage()
userImage.uri = GetUserAvatarURL(m.global.user, 36, 36)
if userImage.uri = ""
' No avatar: show the generic person icon
userImage.uri = "pkg:/images/icons/person_36px_$$RES$$.png"
end if
end sub
import "pkg:/source/utils/itemImageUrl.bs"
sub loadItemPoster(item as object)
poster.uri = getItemPosterUrl(item, imageSize.POSTER_LG)
end sub
import "pkg:/source/api/image.bs"
sub loadCustomImage(itemId, imageTag)
url = ImageURL(itemId, "Primary", {
tag: imageTag,
maxHeight: 100,
maxWidth: 100,
quality: 85
})
if url <> ""
poster.uri = url
end if
end sub

Don’t use the item endpoint for a user’s image. It requests the wrong resource.

' Wrong: item endpoint for a user image
userImage.uri = GetApi().GetImageURL(userId, "primary", 0, params)
' Right: the user endpoint, or better, the helper
userImage.uri = GetApi().GetUserImageURL(userId, "primary", 0, params)
userImage.uri = GetUserAvatarURL(user, 36, 36)

Don’t skip the tag check. Layer 1 builds the URL even when the tag is invalid, and the server answers 404.

' Wrong: 404 when the tag is invalid
url = GetApi().GetImageURL(id, "primary", 0, { tag: possiblyInvalidTag })
' Right: "" when the tag is invalid
url = ImageURL(id, "primary", { tag: possiblyInvalidTag })
  • Layer 1: the endpoint paths for V1 and V2, in tests/source/unit/api/sdk.versioning.spec.bs.
  • Layer 2: the tag check and the defaults, in tests/source/unit/api/ImageURL.spec.bs.
  • Layer 3: what each helper reads from the node, and its fallbacks.

When you move code to the right layer, find what the image belongs to (a user or an item) and whether you have a node, pick the layer from the list above, and test against a V1 and a V2 server.