source_utils_versionDisplay.bs
import "pkg:/source/api/ApiClient.bs"
import "pkg:/source/api/apiPool.bs"
import "pkg:/source/utils/mediaSources.bs"
import "pkg:/source/utils/misc.bs"
import "pkg:/source/utils/versionResume.bs"
' The imperative shell for the display-progress correction — the half that fetches.
'
' versionResume's DISPLAY section holds the rules and the measurements; this holds the two
' requests they need, in one place, because three loaders make the same correction and two
' copies of it had already drifted apart (scoping, caching, and where the server version was
' read). Every loader now gets one behaviour.
'
' Runs on a Task thread: fetchJson blocks, which is what a Task thread is for.
namespace versionDisplay
' correctDisplayProgress: Rewrite a list response's UserData so each tile's progress bar
' shows the version that would actually play.
'
' Costs nothing at all on a pre-12 server, or for a page with nothing grouped. When a page
' DOES hold grouped items it makes one cheap request, and a second only if something is
' actually in progress — see the DISPLAY section of versionResume for why that order, and for
' the measurements behind asking for MediaSources by id rather than on the resume list.
'
' `cache` is owned by the CALLER, which is what lets one function serve both shapes: a grid
' passes a fresh AA and corrects its whole page at once, while the extras orchestrator passes
' one AA for the run so the ranking is fetched once and sibling ids accumulate across rows.
'
' @param {dynamic} items - raw BaseItemDto array from a list response (mutated in place)
' @param {string} serverVersion - m.global.server.version, read ONCE by the caller
' @param {string} parentId - scope the resume query to this container, or "" for none
' @param {string} requestId - for the pool
' @param {object} cache - caller-owned AA, reused across calls that share a run
' @return {integer} how many items were changed
function correctDisplayProgress(items as dynamic, serverVersion as string, parentId as string, requestId as string, cache as object) as integer
if type(items) <> "roArray" or items.Count() = 0 then return 0
if not versionResume.isPerVersionServer(serverVersion) then return 0
if not isValid(cache) then return 0
ids = versionResume.multiVersionIds(items)
if ids.Count() = 0 then return 0
' The ranking first, because an empty one ends it: nothing is in progress anywhere, so no
' tile can need correcting and the second request is not worth making. Memoized either way
' — a failed lookup must not be retried once per row.
if not isValid(cache.ranking)
cache.ranking = versionResume.resumeRanking(versionDisplay.resumeRows(parentId, requestId))
end if
if cache.ranking.Count() = 0 then return 0
versionDisplay.cacheSourcesFor(ids, requestId, cache)
' Only the tiles on THIS page, so a shared cache never corrects an item the caller did not
' ask about.
pageSources = {}
for each id in ids
sources = cache.sourcesById[id]
if isValid(sources) then pageSources[id] = sources
end for
return versionResume.applyDisplayProgress(items, versionResume.displayProgressMap(pageSources, cache.ranking))
end function
' resumeRows: Every version the viewer is part-way through, in the server's DatePlayed order.
'
' No Fields: the sibling ids come from mediaSources.byIds() instead, which is what keeps this
' request's size tied to the viewer's backlog rather than multiplying it — 1.1 KB a row here
' against 7.3 KB with Fields=MediaSources, measured 2026-09-20 on a local 12.0 server.
'
' @param {string} parentId - container to scope to, or ""
' @param {string} requestId - for the pool
' @return {object} array of rows (empty when the lookup failed)
function resumeRows(parentId as string, requestId as string) as object
params = {
MediaTypes: "Video",
EnableImages: false,
EnableTotalRecordCount: false
}
if isValidAndNotEmpty(parentId) then params.parentId = parentId
res = fetchJson(GetApi().BuildGetResumeItemsRequest(params), requestId)
if not isValid(res) or type(res.Items) <> "roArray" then return []
return res.Items
end function
' cacheSourcesFor: Make sure the cache holds a sibling list for each of these tile ids.
'
' `attempted` is tracked separately from the answers so an id the server returned nothing for
' is not re-requested by the next row that mentions it.
'
' @param {object} ids - tile ids
' @param {string} requestId - for the pool
' @param {object} cache - caller-owned AA
sub cacheSourcesFor(ids as object, requestId as string, cache as object)
if not isValid(cache.sourcesById) then cache.sourcesById = {}
if not isValid(cache.attempted) then cache.attempted = {}
missing = []
for each id in ids
if not cache.attempted.DoesExist(id)
missing.push(id)
cache.attempted[id] = true
end if
end for
if missing.Count() = 0 then return
fetched = mediaSources.byIds(missing, requestId)
for each id in fetched
cache.sourcesById[id] = fetched[id]
end for
end sub
end namespace