source_utils_itemImageUrl.bs
import "pkg:/source/api/Image.bs"
import "pkg:/source/constants/imageSize.bs"
import "pkg:/source/utils/misc.bs"
' Generate an image URL from a JellyfinBaseItem for a specific image type.
' This is the base function used by the typed convenience functions below.
' @param item - JellyfinBaseItem content node
' @param imageType - Jellyfin image type string: "Primary", "Thumb", "Logo", "Backdrop"
' @param size - Size constant from imageSize namespace, e.g. imageSize.POSTER_LG
' @returns Full image URL string, or "" if no tag is available for that type
function getItemImageUrl(item as object, imageType = "Primary" as string, size = imageSize.DEFAULT as object) as string
if not isValid(item) then return ""
if not isValidAndNotEmpty(item.id) then return ""
imgParams = { maxWidth: size.width, maxHeight: size.height }
if imageType = "Primary"
if isValidAndNotEmpty(item.primaryImageTag)
imgParams.tag = item.primaryImageTag
return ImageURL(item.id, "Primary", imgParams)
end if
else if imageType = "Thumb"
if isValidAndNotEmpty(item.thumbImageTag)
imgParams.tag = item.thumbImageTag
return ImageURL(item.id, "Thumb", imgParams)
end if
else if imageType = "Logo"
if isValidAndNotEmpty(item.logoImageTag)
imgParams.tag = item.logoImageTag
return ImageURL(item.id, "Logo", imgParams)
end if
else if imageType = "Backdrop"
if isValid(item.backdropImageTags) and item.backdropImageTags.Count() > 0
imgParams.tag = item.backdropImageTags[0]
return ImageURL(item.id, "Backdrop", imgParams)
end if
end if
return ""
end function
' Generate a poster (portrait) URL with fallback chain.
' Fallback order: item primary → parent primary → series primary
' @param item - JellyfinBaseItem content node
' @param size - Size constant from imageSize namespace (default: imageSize.POSTER_LG)
' @returns Full image URL string, or "" if no image is available
function getItemPosterUrl(item as object, size = imageSize.POSTER_LG as object) as string
if not isValid(item) then return ""
if not isValidAndNotEmpty(item.id) then return ""
imgParams = { maxWidth: size.width, maxHeight: size.height }
' 1. Item's own primary image
if isValidAndNotEmpty(item.primaryImageTag)
imgParams.tag = item.primaryImageTag
return ImageURL(item.id, "Primary", imgParams)
end if
' 2. Parent's primary image (episodes/seasons use season/series poster)
if isValidAndNotEmpty(item.parentPrimaryImageTag) and isValidAndNotEmpty(item.parentPrimaryImageItemId)
imgParams.tag = item.parentPrimaryImageTag
return ImageURL(item.parentPrimaryImageItemId, "Primary", imgParams)
end if
' 3. Series primary image (for episodes without a season poster)
if isValidAndNotEmpty(item.seriesPrimaryImageTag) and isValidAndNotEmpty(item.seriesId)
imgParams.tag = item.seriesPrimaryImageTag
return ImageURL(item.seriesId, "Primary", imgParams)
end if
return ""
end function
' Generate a wide/landscape URL with fallback chain.
' Fallback order: item thumb → item backdrop → parent thumb → parent backdrop
' @param item - JellyfinBaseItem content node
' @param size - Size constant from imageSize namespace (default: imageSize.WIDE_MD)
' @returns Full image URL string, or "" if no image is available
function getItemWidePosterUrl(item as object, size = imageSize.WIDE_MD as object) as string
if not isValid(item) then return ""
if not isValidAndNotEmpty(item.id) then return ""
imgParams = { maxWidth: size.width, maxHeight: size.height }
' 1. Item's own thumb image
if isValidAndNotEmpty(item.thumbImageTag)
imgParams.tag = item.thumbImageTag
return ImageURL(item.id, "Thumb", imgParams)
end if
' 2. Item's own backdrop
if isValid(item.backdropImageTags) and item.backdropImageTags.Count() > 0
imgParams.tag = item.backdropImageTags[0]
return ImageURL(item.id, "Backdrop", imgParams)
end if
' 3. Parent thumb (e.g. series thumb for episodes)
if isValidAndNotEmpty(item.parentThumbImageTag) and isValidAndNotEmpty(item.parentThumbItemId)
imgParams.tag = item.parentThumbImageTag
return ImageURL(item.parentThumbItemId, "Thumb", imgParams)
end if
' 4. Parent backdrop
if isValid(item.parentBackdropImageTags) and item.parentBackdropImageTags.Count() > 0 and isValidAndNotEmpty(item.parentBackdropItemId)
imgParams.tag = item.parentBackdropImageTags[0]
return ImageURL(item.parentBackdropItemId, "Backdrop", imgParams)
end if
return ""
end function
' Generate a thumbnail URL (wide, smaller size).
' Delegates to getItemWidePosterUrl with a smaller default size.
' @param item - JellyfinBaseItem content node
' @param size - Size constant from imageSize namespace (default: imageSize.WIDE_SM)
' @returns Full image URL string, or "" if no image is available
function getItemThumbnailUrl(item as object, size = imageSize.WIDE_SM as object) as string
return getItemWidePosterUrl(item, size)
end function
' Generate a full backdrop URL with fallback chain.
' Fallback order: item backdrop → parent backdrop
' @param item - JellyfinBaseItem content node
' @param size - { width, height } object; use device UI resolution (m.global.device.uiResolution)
' @returns Full image URL string, or "" if no image is available
function getItemBackdropUrl(item as object, size as object) as string
if not isValid(item) then return ""
if not isValidAndNotEmpty(item.id) then return ""
' 1. Item's own backdrop
if isValid(item.backdropImageTags) and item.backdropImageTags.Count() > 0
return ImageURL(item.id, "Backdrop", { tag: item.backdropImageTags[0], maxHeight: size.height, maxWidth: size.width })
end if
' 2. Parent backdrop (for episodes/seasons)
if isValid(item.parentBackdropImageTags) and item.parentBackdropImageTags.Count() > 0 and isValidAndNotEmpty(item.parentBackdropItemId)
return ImageURL(item.parentBackdropItemId, "Backdrop", { tag: item.parentBackdropImageTags[0], maxHeight: size.height, maxWidth: size.width })
end if
return ""
end function
' Generate a wide art URL preferring the parent (series/show) wide images over the item's own.
' Skips item-level Thumb and Backdrop entirely; resolves directly from parent fields.
' Use this for episodes when the user prefers show art over episode screenshots.
' Fallback order: parent thumb → parent backdrop
' @param item - JellyfinBaseItem content node (typically an Episode or Recording)
' @param size - { width, height } object; use slot dimensions
' @returns Full image URL string, or "" if no parent wide image is available
function getItemParentWidePosterUrl(item as object, size = imageSize.WIDE_MD as object) as string
if not isValid(item) then return ""
if not isValidAndNotEmpty(item.id) then return ""
imgParams = { maxWidth: size.width, maxHeight: size.height }
' 1. Parent (series) thumb
if isValidAndNotEmpty(item.parentThumbImageTag) and isValidAndNotEmpty(item.parentThumbItemId)
imgParams.tag = item.parentThumbImageTag
return ImageURL(item.parentThumbItemId, "Thumb", imgParams)
end if
' 2. Parent (series) backdrop
if isValid(item.parentBackdropImageTags) and item.parentBackdropImageTags.Count() > 0 and isValidAndNotEmpty(item.parentBackdropItemId)
imgParams.tag = item.parentBackdropImageTags[0]
return ImageURL(item.parentBackdropItemId, "Backdrop", imgParams)
end if
return ""
end function
' Generate a backdrop URL that prefers the parent (series/show) backdrop over the item's own.
' Use this for sub-item views (season episode lists, etc.) to maintain visual continuity
' with the parent detail screen. Both screens will resolve to the same series backdrop URL,
' which lets BackdropFader deduplicate the transition and avoid flickering.
' Fallback order: parent backdrop → item's own backdrop
' @param item - JellyfinBaseItem content node (typically a Season)
' @param size - { width, height } object; use device UI resolution (m.global.device.uiResolution)
' @returns Full image URL string, or "" if no image is available
function getItemParentBackdropUrl(item as object, size as object) as string
if not isValid(item) then return ""
if not isValidAndNotEmpty(item.id) then return ""
' 1. Parent (series) backdrop first - matches what the parent detail screen shows
if isValid(item.parentBackdropImageTags) and item.parentBackdropImageTags.Count() > 0 and isValidAndNotEmpty(item.parentBackdropItemId)
return ImageURL(item.parentBackdropItemId, "Backdrop", { tag: item.parentBackdropImageTags[0], maxHeight: size.height, maxWidth: size.width })
end if
' 2. Fall back to item's own backdrop if no parent backdrop exists
if isValid(item.backdropImageTags) and item.backdropImageTags.Count() > 0
return ImageURL(item.id, "Backdrop", { tag: item.backdropImageTags[0], maxHeight: size.height, maxWidth: size.width })
end if
return ""
end function
' Generate an image URL for a Live TV program, falling back to the channel image.
' Accommodates the two-phase load constraint: program and channel are separate API calls.
' @param programItem - JellyfinBaseItem for the program
' @param channelItem - JellyfinBaseItem for the channel (may be invalid if not yet loaded)
' @param size - Size constant from imageSize namespace (default: imageSize.WIDE_MD)
' @returns Full image URL string, or "" if no image is available
function getProgramImageUrl(programItem as object, channelItem as object, size = imageSize.WIDE_MD as object) as string
' 1. Program's own primary image
if isValid(programItem) and isValidAndNotEmpty(programItem.id)
url = getItemPosterUrl(programItem, size)
if isValidAndNotEmpty(url)
return url
end if
end if
' 2. Channel's primary image as fallback
if isValid(channelItem) and isValidAndNotEmpty(channelItem.id)
imgParams = { maxWidth: size.width, maxHeight: size.height }
if isValidAndNotEmpty(channelItem.primaryImageTag)
imgParams.tag = channelItem.primaryImageTag
return ImageURL(channelItem.id, "Primary", imgParams)
end if
end if
return ""
end function