source_GridView_gridPage.bs
import "pkg:/source/utils/misc.bs"
' A library grid page as LoadItemsTask2 assembles it from the server's replies.
'
' In source/ rather than in the Task so a Rooibos suite can reach it: a Task's functions are
' scoped to its component. Same move as gridQuery.bs.
namespace gridPage
' The "#" letter filter: every name that starts with no letter A–Z. The server offers no "not
' a letter" filter, so "#" is two ranges of the sort name, asked for one after the other:
' before "a", and from "{" on ("{" is the character after "z"). Between them they hold every
' item no letter holds, each once, so the server counts both exactly and nothing is filtered
' here.
'
' Verified 2026-09-25 against 10.7.7, 10.8.13, 10.9.11, 10.10.7, 10.11.11 and 12.0.0: A–Z plus
' these two ranges put every item of a library in exactly one place, for /Items and /Genres
' (/Artists and /Studios share the /Genres filter code). 10.11.0–10.11.2 compared only first
' characters and matched on Name as well, so there some items fall in no range or in two
' whatever this sends; 10.11.3 fixed it (jellyfin#15381). See jellyfin-server-versioning.md.
function hashRanges() as object
return [{ NameLessThan: "A" }, { NameStartsWithOrGreater: "{" }]
end function
' The first request for one page of "#": which range, from where, for how many, and whether
' the server counts it.
'
' The first page counts, and starts at the top of the first range. A later page knows the
' first range's size from that count, which is how it finds its place in the two ranges.
'
' @param startIndex - the page's first item, counted across both ranges
' @param limit - the page size
' @param firstRangeTotal - the first range's count from the first page; -1 on the first page
' @return an AA of range (an index into hashRanges()), startIndex, limit and counts
function hashFirstRequest(startIndex as integer, limit as integer, firstRangeTotal as integer) as object
if firstRangeTotal < 0
return { range: 0, startIndex: 0, limit: limit, counts: true }
end if
if startIndex < firstRangeTotal
return { range: 0, startIndex: startIndex, limit: limit, counts: false }
end if
return { range: 1, startIndex: startIndex - firstRangeTotal, limit: limit, counts: false }
end function
' The request that completes the page after the first delivered `delivered` items, or invalid
' when the page needs none.
'
' Once the first range has run out (it delivered fewer than asked for), the rest of the page
' comes from the top of the second. The first page always asks the second range, even when the
' first range filled the page (limit 0: the count alone), because the grid's total is both
' counts.
'
' @param first - from hashFirstRequest()
' @param delivered - how many items the first request delivered
' @param limit - the page size
function hashSecondRequest(first as object, delivered as integer, limit as integer) as dynamic
if first.range <> 0 then return invalid
rest = limit - delivered
if first.counts
if rest < 0 then rest = 0
return { range: 1, startIndex: 0, limit: rest, counts: true }
end if
if delivered >= first.limit then return invalid
return { range: 1, startIndex: 0, limit: rest, counts: false }
end function
' The query params for one "#" request: the page's params with this request's range, start,
' size and whether to count. A copy: `params` stays the page's.
'
' @param params - the page's query params, without a name filter
' @param request - from hashFirstRequest() or hashSecondRequest()
function hashRequestParams(params as object, request as object) as object
requestParams = {}
requestParams.append(params)
requestParams.append(gridPage.hashRanges()[request.range])
requestParams.StartIndex = request.startIndex
requestParams.limit = request.limit
if not request.counts then requestParams.EnableTotalRecordCount = false
return requestParams
end function
' Adds the second request's reply to the first's. On the first page the total is the two
' ranges' counts together; on a later page neither counted, and the total is left as it is.
'
' The caller makes both requests and decides what a failure means: this only combines two
' replies that both arrived.
'
' @param data - the first request's reply ({ Items, TotalRecordCount }); modified in place
' @param second - the second request's reply
' @param isCounted - whether both requests counted (the first page)
sub appendSecondRange(data as object, second as object, isCounted as boolean)
if type(data.Items) <> "roArray" or type(second.Items) <> "roArray" then return
data.Items.append(second.Items)
if isCounted and isValid(data.TotalRecordCount) and isValid(second.TotalRecordCount)
data.TotalRecordCount += second.TotalRecordCount
end if
end sub
' The Fields a grid query asks for: Overview, MediaSourceCount for every type except music, then
' the caller's extras.
'
' MediaSourceCount is what tells a video tile it has alternate versions. Jellyfin fills it only
' for videos (DtoService, 10.7.7 through 12.x), so the music grid queries leave it out.
'
' @param itemType - the grid's item type, such as "Movie" or "MusicAlbum"
' @param additionalFields - extra comma-separated fields to append, or ""
' @return the comma-separated Fields string
function itemFields(itemType as string, additionalFields as string) as string
fields = "Overview,MediaSourceCount"
if itemType = "MusicArtist" or itemType = "AlbumArtists" or itemType = "MusicAlbum"
fields = "Overview"
end if
if additionalFields <> ""
fields = fields + "," + additionalFields
end if
return fields
end function
' The Fields a Genres view asks for on each genre's sample items: the poster and backdrop data
' and the sync flag, plus MediaSourceCount unless the samples are music.
'
' See itemFields for why music leaves MediaSourceCount out.
'
' @param isMusic - true when the samples are music items (the music library's Genres view)
' @return the comma-separated Fields string
function genreSampleFields(isMusic as boolean) as string
if isMusic
return "PrimaryImageAspectRatio,BasicSyncInfo,BackdropImageTags"
end if
return "PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo,BackdropImageTags"
end function
end namespace