import "pkg:/source/api/sdk.bs"
import "pkg:/source/api/sdkV1.bs"
import "pkg:/source/api/sdkV2.bs"
import "pkg:/source/utils/misc.bs"
' Note: getApiVersionFromGlobal() is imported from misc.bs and serves as the
' single source of truth for API version detection across the entire app.
' ApiClient provides a centralized, stateful wrapper around the Jellyfin API.
' It automatically injects image parameters and version-specific fields for consistent,
' bulletproof item fetching across all Jellyfin server versions (10.7.0+).
'
' The singleton pattern ensures one instance is shared across the entire app.
class ApiClient
' Cache global reference so m.global works inside class methods
private global = invalid
sub new()
m.global = GetGlobalAA().global
end sub
' Default image parameters for all item endpoints
' These ensure backdrop, logo, and thumb images are always requested
private imageDefaults = {
EnableImageTypes: "Primary,Backdrop,Logo,Thumb",
ImageTypeLimit: 1
}
' Get current user ID from global state
private function getUserId() as string
return m.global.user.id
end function
' Get API version from global state via helper
' Uses getApiVersionFromGlobal() as single source of truth for safe API version reading
private function getApiVersion() as integer
return getApiVersionFromGlobal()
end function
' ═══════════════════════════════════════════════════════════════════════════
' USER ENDPOINTS - Version-aware (V1/V2 routing)
' When adding V3 support, add else-if branch in each method below
' ═══════════════════════════════════════════════════════════════════════════
' Get a single item by ID with automatic image and version field injection
' @param itemId - The Jellyfin item ID
' Build a request AA to get a single item with automatic image/version field injection.
' @param itemId - The item ID
' @param params - Optional query parameters (fields, etc.)
' @returns Request AA: { method, url } or invalid if no user
function BuildGetItemRequest(itemId as string, params = {} as object) as dynamic
userId = m.getUserId()
if userId = "" then return invalid
mergedParams = m.injectDefaults(params)
if m.getApiVersion() >= 2
mergedParams.userId = userId
return m.validatedReq("GET", buildURL(Substitute("/Items/{0}", itemId), mergedParams))
end if
return m.validatedReq("GET", buildURL(Substitute("/users/{0}/items/{1}", userId, itemId), mergedParams))
end function
' Build a request AA to get a single item WITHOUT image/version field injection.
' @param itemId - The item ID
' @param params - Optional query parameters (passed through as-is)
' @returns Request AA: { method, url } or invalid if no user
function BuildGetItemRawRequest(itemId as string, params = {} as object) as dynamic
userId = m.getUserId()
if userId = "" then return invalid
if m.getApiVersion() >= 2
params.userId = userId
return m.validatedReq("GET", buildURL(Substitute("/Items/{0}", itemId), params))
end if
return m.validatedReq("GET", buildURL(Substitute("/users/{0}/items/{1}", userId, itemId), params))
end function
' Build a request AA for GetItemsByQuery, for use with fetchRes() or fetchJson().
' @param params - Query parameters (limit, sortBy, filters, etc.)
' @returns Request AA: { method, url } or invalid if no user
function BuildGetItemsByQueryRequest(params = {} as object) as dynamic
userId = m.getUserId()
if userId = "" then return invalid
mergedParams = m.injectDefaults(params)
if m.getApiVersion() >= 2
queryParams = { userId: userId }
queryParams.append(mergedParams)
return m.validatedReq("GET", buildURL("/items/", queryParams))
end if
return m.validatedReq("GET", buildURL(Substitute("/users/{0}/items", userId), mergedParams))
end function
' Build a request AA for GetResumeItems (Continue Watching), for use with fetchRes() or fetchJson().
' Mirrors GetResumeItems(): applies injectDefaults, version-aware endpoint.
' @param params - Optional query parameters
' @returns Request AA: { method, url } or invalid if no userId
function BuildGetResumeItemsRequest(params = {} as object) as dynamic
userId = m.getUserId()
if userId = "" then return invalid
mergedParams = m.injectDefaults(params)
if m.getApiVersion() >= 2
queryParams = { userId: userId }
queryParams.append(mergedParams)
return m.validatedReq("GET", buildURL("/UserItems/Resume", queryParams))
end if
return m.validatedReq("GET", buildURL(Substitute("/users/{0}/items/resume", userId), mergedParams))
end function
' Build a request AA for GetLatestMedia, for use with fetchRes() or fetchJson().
' Both V1 and V2 return a bare array of BaseItemDto (not paginated).
' V1: /users/{id}/items/latest (10.7.x–10.8.x)
' V2: /Items/Latest?userId= (10.9+)
' @param params - Optional query parameters (ParentId, Limit, etc.)
' @returns Request AA: { method, url } or invalid if no userId
function BuildGetLatestMediaRequest(params = {} as object) as dynamic
userId = m.getUserId()
if userId = "" then return invalid
mergedParams = m.injectDefaults(params)
if m.getApiVersion() >= 2
queryParams = { userId: userId }
queryParams.append(mergedParams)
return m.validatedReq("GET", buildURL("/Items/Latest", queryParams))
end if
return m.validatedReq("GET", buildURL(Substitute("/users/{0}/items/latest", userId), mergedParams))
end function
' ═══════════════════════════════════════════════════════════════════════════
' RAW ACCESS - No image injection (for playback/internal use)
' ═══════════════════════════════════════════════════════════════════════════
' ═══════════════════════════════════════════════════════════════════════════
' SHOWS ENDPOINTS
' ═══════════════════════════════════════════════════════════════════════════
' Build a request AA for GetSeasons, for use with fetchRes() or fetchJson().
' Mirrors GetSeasons(): applies injectDefaults and injects UserId.
' @param seriesId - The series ID
' @param params - Optional query parameters
' @returns Request AA: { method, url } or invalid if no userId
function BuildGetSeasonsRequest(seriesId as string, params = {} as object) as dynamic
userId = m.getUserId()
if userId = "" then return invalid
mergedParams = m.injectDefaults(params)
mergedParams.UserId = userId
return m.validatedReq("GET", buildURL(Substitute("/shows/{0}/seasons", seriesId), mergedParams))
end function
' Build a request AA for GetEpisodes, for use with fetchRes() or fetchJson().
' @param seriesId - The series ID
' @param params - Optional query parameters (StartItemId, Limit, etc.)
' @returns Request AA: { method, url } or invalid if no user
function BuildGetEpisodesRequest(seriesId as string, params = {} as object) as dynamic
userId = m.getUserId()
if userId = "" then return invalid
mergedParams = m.injectDefaults(params)
mergedParams.UserId = userId
return m.validatedReq("GET", buildURL(Substitute("/shows/{0}/episodes", seriesId), mergedParams))
end function
' ═══════════════════════════════════════════════════════════════════════════
' ARTIST ENDPOINTS
' ═══════════════════════════════════════════════════════════════════════════
' Build a request AA for GetArtistByName, for use with fetchJson().
' @param name - Artist name (will be URI-encoded)
' @param params - Optional query parameters
' @returns Request AA: { method, url } or invalid if server URL not set
function BuildGetArtistByNameRequest(name as string, params = {} as object) as dynamic
return m.validatedReq("GET", buildURL(Substitute("/artists/{0}", name.EncodeUriComponent()), params))
end function
' Build a request AA to fetch all artists, for use with fetchRes() or fetchJson().
' Mirrors GetArtists(): applies injectDefaults and injects UserId.
' @param params - Query parameters
' @returns Request AA: { method, url } or invalid if no userId
function BuildGetArtistsRequest(params = {} as object) as dynamic
userId = m.getUserId()
if userId = "" then return invalid
mergedParams = m.injectDefaults(params)
mergedParams.UserId = userId
return m.validatedReq("GET", buildURL("/artists", mergedParams))
end function
' Build a request AA for GetPersons, for use with fetchRes() or fetchJson().
' Mirrors GetPersons(): applies injectDefaults and injects UserId.
' @param params - Query parameters (e.g. IsFavorite, searchTerm)
' @returns Request AA: { method, url } or invalid if no userId
function BuildGetPersonsRequest(params = {} as object) as dynamic
userId = m.getUserId()
if userId = "" then return invalid
mergedParams = m.injectDefaults(params)
mergedParams.UserId = userId
return m.validatedReq("GET", buildURL("/persons", mergedParams))
end function
' Build a request AA to fetch all album artists, for use with fetchRes() or fetchJson().
' Mirrors GetAlbumArtists(): applies injectDefaults and injects UserId.
' @param params - Query parameters
' @returns Request AA: { method, url } or invalid if no userId
function BuildGetAlbumArtistsRequest(params = {} as object) as dynamic
userId = m.getUserId()
if userId = "" then return invalid
mergedParams = m.injectDefaults(params)
mergedParams.UserId = userId
return m.validatedReq("GET", buildURL("/artists/albumartists", mergedParams))
end function
' Build a request AA to get similar artists, for use with fetchRes() or fetchJson().
' @param itemId - The artist item ID
' @param params - Optional query parameters (userId, limit, etc.)
' @returns Request AA: { method, url } or invalid if server URL not set
function BuildGetArtistSimilarRequest(itemId as string, params = {} as object) as dynamic
return m.validatedReq("GET", buildURL(Substitute("/Artists/{0}/Similar", itemId), params))
end function
' ═══════════════════════════════════════════════════════════════════════════
' PLAYLIST ENDPOINTS
' ═══════════════════════════════════════════════════════════════════════════
' Build a request AA for GetPlaylistItems, for use with fetchRes() or fetchJson().
' @param playlistId - The playlist ID
' @param params - Optional query parameters
' @returns Request AA: { method, url } or invalid if no user
function BuildGetPlaylistItemsRequest(playlistId as string, params = {} as object) as dynamic
userId = m.getUserId()
if userId = "" then return invalid
mergedParams = {}
mergedParams.append(params)
mergedParams.UserId = userId
return m.validatedReq("GET", buildURL(Substitute("/playlists/{0}/items", playlistId), mergedParams))
end function
' ═══════════════════════════════════════════════════════════════════════════
' ITEMS ENDPOINTS
' ═══════════════════════════════════════════════════════════════════════════
' Build a request AA for GetInstantMix, for use with fetchJson().
' @param itemId - The item ID
' @param params - Optional query parameters (Limit, etc.)
' @returns Request AA: { method, url } or invalid if no user
function BuildGetInstantMixRequest(itemId as string, params = {} as object) as dynamic
userId = m.getUserId()
if userId = "" then return invalid
mergedParams = {}
mergedParams.append(params)
mergedParams.UserId = userId
return m.validatedReq("GET", buildURL(Substitute("/items/{0}/instantmix", itemId), mergedParams))
end function
' Build a request AA for GetIntros, for use with fetchJson().
' @param itemId - The item ID
' @returns Request AA: { method, url } or invalid if no user
function BuildGetIntrosRequest(itemId as string) as dynamic
userId = m.getUserId()
if userId = "" then return invalid
if m.getApiVersion() >= 2
return m.validatedReq("GET", buildURL(Substitute("/Items/{0}/Intros", itemId)))
end if
return m.validatedReq("GET", buildURL(Substitute("/users/{0}/items/{1}/intros", userId, itemId)))
end function
' Build a request AA for GetMediaSegments, for use with fetchJson().
' Only available on Jellyfin 10.10.0+ servers.
' @param itemId - The item ID
' @param includeSegmentTypes - Optional comma-separated segment types to filter (e.g., "Intro,Outro")
' @returns Request AA: { method, url } or invalid if server URL not set
function BuildGetMediaSegmentsRequest(itemId as string, includeSegmentTypes = "" as string) as dynamic
params = {}
if includeSegmentTypes <> ""
params.includeSegmentTypes = includeSegmentTypes
end if
return m.validatedReq("GET", buildURL(Substitute("/MediaSegments/{0}", itemId), params))
end function
' Build a request AA for GetItemLyrics, for use with fetchJson().
' @param itemId - The Audio item ID
' @returns Request AA: { method, url } or invalid if server URL not set
function BuildGetItemLyricsRequest(itemId as string) as dynamic
return m.validatedReq("GET", buildURL(Substitute("Audio/{0}/Lyrics", itemId)))
end function
' ═══════════════════════════════════════════════════════════════════════════
' PLAYBACK ENDPOINTS
' ═══════════════════════════════════════════════════════════════════════════
' Build a request AA for PostPlaybackInfo, for use with fetchJson().
' @param itemId - The item ID
' @param postData - Request body data (DeviceProfile, MediaSourceId, etc.)
' @returns Request AA: { method, url, body } or invalid if server URL not set
function BuildPostPlaybackInfoRequest(itemId as string, postData as object) as dynamic
url = buildURL(Substitute("/items/{0}/playbackinfo", itemId))
if not isValid(url) then return invalid
return { method: "POST", url: url, body: FormatJson(postData), headers: { "Content-Type": "application/json" } }
end function
' Build a request AA for GetLocalTrailers, for use with submitApiRequest() or fetchJson().
' Version-aware: V2 uses /Items/{id}/LocalTrailers, V1 uses /users/{userId}/items/{id}/localtrailers.
' @param itemId - The item ID
' @returns Request AA: { method, url } or invalid if no userId (V1 only)
function BuildGetLocalTrailersRequest(itemId as string) as dynamic
if m.getApiVersion() >= 2
return m.validatedReq("GET", buildURL(Substitute("/Items/{0}/LocalTrailers", itemId), {}))
end if
userId = m.getUserId()
if userId = "" then return invalid
return m.validatedReq("GET", buildURL(Substitute("/users/{0}/items/{1}/localtrailers", userId, itemId), {}))
end function
' Build a request AA for GetSpecialFeatures, for use with fetchRes() or fetchJson().
' Mirrors GetSpecialFeatures(): version-aware endpoint.
' @param itemId - The item ID
' @returns Request AA: { method, url } or invalid if no userId (V1 only)
function BuildGetSpecialFeaturesRequest(itemId as string) as dynamic
if m.getApiVersion() >= 2
return m.validatedReq("GET", buildURL(Substitute("/Items/{0}/SpecialFeatures", itemId), {}))
end if
userId = m.getUserId()
if userId = "" then return invalid
return m.validatedReq("GET", buildURL(Substitute("/users/{0}/items/{1}/specialfeatures", userId, itemId), {}))
end function
' Build a request AA to mark an item as favorite, for use with SubmitSideEffect().
' @param itemId - The item ID
' @returns Request AA: { method, url } or invalid if no user
function BuildMarkFavoriteRequest(itemId as string) as dynamic
userId = m.getUserId()
if userId = "" then return invalid
if m.getApiVersion() >= 2
return m.validatedReq("POST", buildURL(Substitute("/UserFavoriteItems/{0}", itemId), { userId: userId }))
end if
return m.validatedReq("POST", buildURL(Substitute("users/{0}/favoriteitems/{1}", userId, itemId)))
end function
' Build a request AA to unmark an item as favorite, for use with SubmitSideEffect().
' @param itemId - The item ID
' @returns Request AA: { method, url } or invalid if no user
function BuildUnmarkFavoriteRequest(itemId as string) as dynamic
userId = m.getUserId()
if userId = "" then return invalid
if m.getApiVersion() >= 2
return m.validatedReq("DELETE", buildURL(Substitute("/UserFavoriteItems/{0}", itemId), { userId: userId }))
end if
return m.validatedReq("DELETE", buildURL(Substitute("users/{0}/favoriteitems/{1}", userId, itemId)))
end function
' Build a request AA to mark an item as played, for use with SubmitSideEffect().
' Includes default DatePlayed (now) and PlaybackPositionTicks (0) matching Jellyfin API expectations.
' @param itemId - The item ID
' @returns Request AA: { method, url } or invalid if no user
function BuildMarkPlayedRequest(itemId as string) as dynamic
userId = m.getUserId()
if userId = "" then return invalid
params = {
"DatePlayed": CreateObject("roDateTime").ToISOString(),
"PlaybackPositionTicks": 0
}
if m.getApiVersion() >= 2
params.userId = userId
return m.validatedReq("POST", buildURL(Substitute("/UserPlayedItems/{0}", itemId), params))
end if
return m.validatedReq("POST", buildURL(Substitute("users/{0}/playeditems/{1}", userId, itemId), params))
end function
' Build a request AA to mark an item as unplayed, for use with SubmitSideEffect().
' @param itemId - The item ID
' @returns Request AA: { method, url } or invalid if no user
function BuildUnmarkPlayedRequest(itemId as string) as dynamic
userId = m.getUserId()
if userId = "" then return invalid
if m.getApiVersion() >= 2
return m.validatedReq("DELETE", buildURL(Substitute("/UserPlayedItems/{0}", itemId), { userId: userId }))
end if
return m.validatedReq("DELETE", buildURL(Substitute("users/{0}/playeditems/{1}", userId, itemId)))
end function
' Build a request AA to delete an item, for use with SubmitSideEffect().
' @param itemId - The item ID to delete
' @returns Request AA: { method, url }
function BuildDeleteItemRequest(itemId as string) as dynamic
return m.validatedReq("DELETE", buildURL(Substitute("/items/{0}", itemId)))
end function
' ═══════════════════════════════════════════════════════════════════════════
' SESSION ENDPOINTS
' ═══════════════════════════════════════════════════════════════════════════
' Build a playstate request AA for use with SubmitSideEffect().
' Applies default playstate fields and routes to the correct session endpoint.
' @param state - "start" | "update" | "stop" | "finished"
' @param params - Playback parameters (ItemId, PositionTicks, IsPaused, etc.)
' @returns Request AA: { method, url, body, headers } or invalid if state unknown
function BuildPlaystateRequest(state as string, params as object) as dynamic
merged = {
IsPaused: false,
PositionTicks: 0
}
for each item in params.items()
merged[item.key] = item.value
end for
path = ""
if state = "start"
path = "/sessions/playing"
else if state = "update"
path = "/sessions/playing/progress"
else if state = "stop" or state = "finished"
path = "/sessions/playing/stopped"
else
return invalid
end if
url = buildURL(path)
if not isValid(url) then return invalid
return {
method: "POST",
url: url,
body: FormatJson(merged),
headers: { "Content-Type": "application/json" }
}
end function
' Build a request AA to post full session capabilities, for use with SubmitSideEffect().
' @param capabilities - Device capabilities AA from getDeviceCapabilities()
' @returns Request AA: { method, url, body, headers } or invalid if server URL not set
function BuildPostSessionCapabilitiesRequest(capabilities as object) as dynamic
url = buildURL("/Sessions/Capabilities/Full")
if not isValid(url) then return invalid
return {
method: "POST",
url: url,
body: FormatJson(capabilities),
headers: { "Content-Type": "application/json" }
}
end function
' Build a request AA to get active sessions, for use with fetchJson().
' @param params - Query parameters (e.g. { deviceId: "..." })
' @returns Request AA: { method, url } or invalid if server URL not set
function BuildGetSessionsRequest(params = {} as object) as dynamic
return m.validatedReq("GET", buildURL("/sessions", params))
end function
' ═══════════════════════════════════════════════════════════════════════════
' SYSTEM ENDPOINTS
' ═══════════════════════════════════════════════════════════════════════════
' Get configuration by name
' @param name - Configuration name
' @returns API response or invalid on error
function GetConfigurationByName(name as string) as dynamic
return sdk.system.GetConfigurationByName(name)
end function
' ═══════════════════════════════════════════════════════════════════════════
' BRANDING ENDPOINTS
' ═══════════════════════════════════════════════════════════════════════════
' Build a request AA for GetBrandingConfiguration, for use with fetchRes() or fetchJson().
' @returns Request AA: { method, url } or invalid if server URL not set
function BuildGetBrandingConfigurationRequest() as dynamic
return m.validatedReq("GET", buildURL("/branding/configuration"))
end function
' ═══════════════════════════════════════════════════════════════════════════
' DISPLAY PREFERENCES ENDPOINTS
' ═══════════════════════════════════════════════════════════════════════════
' Get display preferences
' @param id - Preference ID
' @param params - Query parameters
' @returns API response or invalid on error
function GetDisplayPreferences(id as string, params = {} as object) as dynamic
return sdk.displayPreferences.Get(id, params)
end function
' ═══════════════════════════════════════════════════════════════════════════
' AUTHENTICATION ENDPOINTS
' ═══════════════════════════════════════════════════════════════════════════
' Authenticate user by username and password
' @param username - User's username
' @param password - User's password
' @returns API response with auth token or invalid on error
function AuthenticateByName(username as string, password as string) as dynamic
return sdk.users.AuthenticateByName(username, password)
end function
' Get user by ID
' @param userId - The user ID
' @returns API response or invalid on error
function GetUser(userId as string) as dynamic
return sdk.users.Get(userId)
end function
' Get public users
' @returns API response or invalid on error
function GetPublicUsers() as dynamic
return sdk.users.GetPublic()
end function
' Authenticate via Quick Connect
' @param secret - The Quick Connect secret
' @returns API response or invalid on error
function AuthenticateWithQuickConnect(secret as string) as dynamic
return sdk.users.AuthenticateWithQuickConnect(secret)
end function
' ═══════════════════════════════════════════════════════════════════════════
' QUICK CONNECT ENDPOINTS
' ═══════════════════════════════════════════════════════════════════════════
' Initiate Quick Connect
' @returns API response with secret or invalid on error
function InitiateQuickConnect() as dynamic
return sdk.quickConnect.Initiate()
end function
' Connect via Quick Connect
' @param secret - The Quick Connect secret
' @returns API response or invalid on error
function ConnectQuickConnect(secret as string) as dynamic
return sdk.quickConnect.Connect(secret)
end function
' Build a request AA for GetQuickConnectEnabled, for use with fetchRes() or fetchJson().
' Server responds with a plain boolean body (true = enabled, false = disabled).
' Lowercase path matches the existing /quickconnect/initiate convention - Jellyfin's
' routing is case-insensitive but reverse proxies in the wild sometimes are not.
' @returns Request AA: { method, url } or invalid if server URL not set
function BuildGetQuickConnectEnabledRequest() as dynamic
return m.validatedReq("GET", buildURL("/quickconnect/enabled"))
end function
' ═══════════════════════════════════════════════════════════════════════════
' VIEWS ENDPOINTS
' ═══════════════════════════════════════════════════════════════════════════
' Build a request AA for GetViews, for use with fetchRes() or fetchJson().
' Mirrors GetViews(): version-aware endpoint.
' @returns Request AA: { method, url } or invalid if no userId
function BuildGetViewsRequest() as dynamic
userId = m.getUserId()
if userId = "" then return invalid
if m.getApiVersion() >= 2
return m.validatedReq("GET", buildURL("/UserViews", { userId: userId }))
end if
return m.validatedReq("GET", buildURL(Substitute("/users/{0}/views", userId), {}))
end function
' ═══════════════════════════════════════════════════════════════════════════
' NEXT UP ENDPOINTS
' ═══════════════════════════════════════════════════════════════════════════
' Build a request AA for GetNextUp, for use with fetchRes() or fetchJson().
' Mirrors GetNextUp(): applies injectDefaults and injects UserId.
' @param params - Query parameters (SeriesId, limit, EnableRewatching, etc.)
' @returns Request AA: { method, url } or invalid if no userId
function BuildGetNextUpRequest(params = {} as object) as dynamic
userId = m.getUserId()
if userId = "" then return invalid
mergedParams = m.injectDefaults(params)
mergedParams.UserId = userId
return m.validatedReq("GET", buildURL("/shows/nextup", mergedParams))
end function
' ═══════════════════════════════════════════════════════════════════════════
' VIDEOS ENDPOINTS
' ═══════════════════════════════════════════════════════════════════════════
' Build a request AA for GetAdditionalParts, for use with fetchRes() or fetchJson().
' @param itemId - The video item ID
' @returns Request AA: { method, url } or invalid if server URL not set
function BuildGetAdditionalPartsRequest(itemId as string) as dynamic
return m.validatedReq("GET", buildURL(Substitute("/videos/{0}/additionalparts", itemId), {}))
end function
' Build a request AA to get similar items, for use with fetchRes() or fetchJson().
' @param itemId - The item ID
' @param params - Optional query parameters (userId, limit, etc.)
' @returns Request AA: { method, url } or invalid if server URL not set
function BuildGetSimilarItemsRequest(itemId as string, params = {} as object) as dynamic
return m.validatedReq("GET", buildURL(Substitute("/Items/{0}/Similar", itemId), params))
end function
' ═══════════════════════════════════════════════════════════════════════════
' FILTERS ENDPOINTS
' ═══════════════════════════════════════════════════════════════════════════
' Build a request AA for GetFilters, for use with fetchRes() or fetchJson().
' @param params - Query parameters (userid, parentid, includeitemtypes, etc.)
' @returns Request AA: { method, url } or invalid if server URL not set
function BuildGetFiltersRequest(params = {} as object) as dynamic
return m.validatedReq("GET", buildURL("/items/filters", params))
end function
' ═══════════════════════════════════════════════════════════════════════════
' IMAGE ENDPOINTS
' ═══════════════════════════════════════════════════════════════════════════
' Build a HEAD request AA to check if an item image exists, for use with fetchRes() or fetchJson().
' Caller checks res.ok — no body is returned for HEAD requests.
' @param id - Item ID
' @param imageType - Type of image (e.g. "logo", "Primary")
' @param imageIndex - Image index (default 0)
' @returns Request AA: { method, url } or invalid if server URL not set
function BuildHeadItemImageRequest(id as string, imageType as string, imageIndex = 0 as integer) as dynamic
return m.validatedReq("HEAD", buildURL(Substitute("/items/{0}/images/{1}/{2}", id, imageType, imageIndex.toStr()), {}))
end function
' Get image URL
' @param id - Item ID
' @param imageType - Type of image
' @param imageIndex - Image index
' @param params - Optional parameters
' @returns API response or invalid on error
function GetImageURL(id as string, imageType as string, imageIndex = 0 as integer, params = {} as object) as dynamic
return sdk.items.GetImageURL(id, imageType, imageIndex, params)
end function
' Get user image URL
' @param id - User ID
' @param imageType - Type of image
' @param imageIndex - Image index
' @param params - Optional parameters
' @returns API response or invalid on error
function GetUserImageURL(id as string, imageType as string, imageIndex = 0 as integer, params = {} as object) as dynamic
if m.getApiVersion() >= 2
return sdkV2.users.GetImageURL(id, imageType, imageIndex, params)
end if
return sdkV1.users.GetImageURL(id, imageType, imageIndex, params)
end function
' ═══════════════════════════════════════════════════════════════════════════
' LIVETV ENDPOINTS
' ═══════════════════════════════════════════════════════════════════════════
' Build a request AA to fetch Live TV channels, for use with fetchRes() or fetchJson().
' @param params - Query parameters
' @returns Request AA: { method, url } or invalid if server URL not set
function BuildGetLiveTVChannelsRequest(params = {} as object) as dynamic
return m.validatedReq("GET", buildURL("/livetv/channels", params))
end function
' Build a request AA for GetLiveTVPrograms (full EPG search), for use with fetchRes() or fetchJson().
' Mirrors GetLiveTVPrograms(): applies injectDefaults.
' @param params - Query parameters (SearchTerm, Limit, etc.)
' @returns Request AA: { method, url } or invalid if server URL not set
function BuildGetLiveTVProgramsRequest(params = {} as object) as dynamic
mergedParams = m.injectDefaults(params)
return m.validatedReq("GET", buildURL("/livetv/programs", mergedParams))
end function
' Build a request AA to get recommended Live TV programs (On Now), for use with fetchRes() or fetchJson().
' @param params - Query parameters (userId, isAiring, limit, etc.)
' @returns Request AA: { method, url } or invalid if server URL not set
function BuildGetLiveTvRecommendedProgramsRequest(params = {} as object) as dynamic
return m.validatedReq("GET", buildURL("/LiveTv/Programs/Recommended", params))
end function
' Build a request AA to get Live TV recordings, for use with fetchRes() or fetchJson().
' @param params - Query parameters (isInProgress, limit, status, etc.)
' @returns Request AA: { method, url } or invalid if server URL not set
function BuildGetLiveTvRecordingsRequest(params = {} as object) as dynamic
return m.validatedReq("GET", buildURL("/LiveTv/Recordings", params))
end function
' ═══════════════════════════════════════════════════════════════════════════
' STUDIOS ENDPOINTS
' ═══════════════════════════════════════════════════════════════════════════
' Build a request AA to fetch studios, for use with fetchRes() or fetchJson().
' @param params - Query parameters
' @returns Request AA: { method, url } or invalid if server URL not set
function BuildGetStudiosRequest(params = {} as object) as dynamic
return m.validatedReq("GET", buildURL("/studios", params))
end function
' ═══════════════════════════════════════════════════════════════════════════
' GENRES ENDPOINTS
' ═══════════════════════════════════════════════════════════════════════════
' Build a request AA to fetch genres, for use with fetchRes() or fetchJson().
' @param params - Query parameters
' @returns Request AA: { method, url } or invalid if server URL not set
function BuildGetGenresRequest(params = {} as object) as dynamic
return m.validatedReq("GET", buildURL("/genres", params))
end function
' ═══════════════════════════════════════════════════════════════════════════
' LIVE TV ENDPOINTS
' ═══════════════════════════════════════════════════════════════════════════
' Build a request AA to fetch a single Live TV program, for use with fetchRes() or fetchJson().
' @param programId - The program ID
' @param params - Optional query parameters (UserId, etc.)
' @returns Request AA: { method, url } or invalid if server URL not set
function BuildGetLiveTvProgramRequest(programId as string, params = {} as object) as dynamic
return m.validatedReq("GET", buildURL(Substitute("/LiveTv/Programs/{0}", programId), params))
end function
' Build a request AA to fetch the Live TV schedule, for use with fetchRes() or fetchJson().
' Uses POST because the channelIds list can exceed URL length limits.
' @param params - Query/body parameters (channelIds, startTime, endTime, UserId, etc.)
' @returns Request AA: { method, url, body, headers } or invalid if server URL not set
function BuildGetLiveTvScheduleRequest(params as object) as dynamic
url = buildURL("/LiveTv/Programs")
if not isValid(url) then return invalid
return {
method: "POST",
url: url,
body: FormatJson(params),
headers: { "Content-Type": "application/json" }
}
end function
' ═══════════════════════════════════════════════════════════════════════════
' LIVE TV RECORDING ENDPOINTS
' ═══════════════════════════════════════════════════════════════════════════
' Build a request AA to fetch Live TV timer defaults for a program, for use with fetchRes() or fetchJson().
' @param programId - The program ID to pre-fill timer defaults
' @returns Request AA: { method, url } or invalid if server URL not set
function BuildGetLiveTvTimerDefaultsRequest(programId as string) as dynamic
return m.validatedReq("GET", buildURL("/LiveTv/Timers/Defaults", { programId: programId }))
end function
' Build a request AA to create a Live TV recording timer, for use with fetchRes() or fetchJson().
' @param defaults - Timer defaults AA from BuildGetLiveTvTimerDefaultsRequest response
' @returns Request AA: { method, url, body, headers } or invalid if server URL not set
function BuildCreateTimerRequest(defaults as object) as dynamic
url = buildURL("/LiveTv/Timers")
if not isValid(url) then return invalid
return {
method: "POST",
url: url,
body: FormatJson(defaults),
headers: { "Content-Type": "application/json" }
}
end function
' Build a request AA to create a Live TV series recording timer, for use with fetchRes() or fetchJson().
' @param defaults - Timer defaults AA from BuildGetLiveTvTimerDefaultsRequest response
' @returns Request AA: { method, url, body, headers } or invalid if server URL not set
function BuildCreateSeriesTimerRequest(defaults as object) as dynamic
url = buildURL("/LiveTv/SeriesTimers")
if not isValid(url) then return invalid
return {
method: "POST",
url: url,
body: FormatJson(defaults),
headers: { "Content-Type": "application/json" }
}
end function
' Build a request AA to cancel a Live TV recording timer, for use with SubmitSideEffect().
' @param timerId - The timer ID to cancel
' @returns Request AA: { method, url } or invalid if server URL not set
function BuildCancelTimerRequest(timerId as string) as dynamic
return m.validatedReq("DELETE", buildURL(Substitute("/LiveTv/Timers/{0}", timerId)))
end function
' Build a request AA to cancel a Live TV series recording timer, for use with SubmitSideEffect().
' @param timerId - The series timer ID to cancel
' @returns Request AA: { method, url } or invalid if server URL not set
function BuildCancelSeriesTimerRequest(timerId as string) as dynamic
return m.validatedReq("DELETE", buildURL(Substitute("/LiveTv/SeriesTimers/{0}", timerId)))
end function
' ═══════════════════════════════════════════════════════════════════════════
' PRIVATE HELPERS
' ═══════════════════════════════════════════════════════════════════════════
' Inject default image parameters and version-specific fields into params
' Delegates to injectApiParams() pure function for testability
' @param params - Original parameters object
' @returns New params object with defaults merged in
private function injectDefaults(params as object) as object
return injectApiParams(params, m.getApiVersion(), m.imageDefaults)
end function
' Build a request AA with a validated URL from buildURL().
' Returns invalid if URL construction failed (e.g. server URL not set).
' @param method - HTTP method (GET, POST, DELETE, etc.)
' @param url - Result of buildURL() — may be invalid
' @returns Request AA: { method, url } or invalid
private function validatedReq(method as string, url as dynamic) as dynamic
if not isValid(url) then return invalid
return { method: method, url: url }
end function
end class
' ═══════════════════════════════════════════════════════════════════════════
' SINGLETON FACTORY
' ═══════════════════════════════════════════════════════════════════════════
' Get the singleton ApiClient instance
' Creates the instance on first call, returns cached instance thereafter
' Usage: api = GetApi() or data = GetApi().GetItem(id)
' @returns ApiClient singleton instance
function GetApi() as ApiClient
globalAA = GetGlobalAA()
if not isValid(globalAA.apiClient)
globalAA.apiClient = new ApiClient()
end if
return globalAA.apiClient
end function