components_ItemGrid_LoadItemsTask2.bs
import "pkg:/source/api/ApiClient.bs"
import "pkg:/source/api/baserequest.bs"
import "pkg:/source/api/Image.bs"
import "pkg:/source/api/Items.bs"
import "pkg:/source/data/JellyfinDataTransformer.bs"
import "pkg:/source/roku_modules/log/LogMixin.brs"
import "pkg:/source/utils/config.bs"
import "pkg:/source/utils/deviceCapabilities.bs"
import "pkg:/source/utils/misc.bs"
sub init()
m.log = log.Logger("LoadItemsTask2")
m.top.functionName = "loadItems"
m.transformer = JellyfinDataTransformer()
m.top.limit = 60
usersettingLimit = m.global.user.settings.itemGridLimit
if isValid(usersettingLimit)
m.top.limit = usersettingLimit
end if
end sub
sub loadItems()
globalUser = m.global.user
results = []
sort_field = m.top.sortField
if m.top.sortAscending = true
sort_order = "Ascending"
else
sort_order = "Descending"
end if
if m.top.ItemType = "LogoImage"
logoImageExists = GetApi().HeadImageURLByName(m.top.itemId, "logo")
if logoImageExists
m.top.content = [GetApi().GetImageURL(m.top.itemId, "logo", 0, { "maxHeight": 212, "maxWidth": 500, "quality": "90" })]
else
m.top.content = []
end if
return
end if
' Build Fields string dynamically - start with Overview, add optional fields
fields = "Overview"
if m.top.additionalFields <> ""
fields = fields + "," + m.top.additionalFields
end if
params = {
limit: m.top.limit,
StartIndex: m.top.startIndex,
SortBy: sort_field,
SortOrder: sort_order,
recursive: m.top.recursive,
Fields: fields,
StudioIds: m.top.studioIds,
genreIds: m.top.genreIds
}
' Only include parentid when non-empty — sending parentid="" causes the Jellyfin API
' to return root UserViews instead of filtering by other params (e.g. genreIds)
if m.top.itemId <> ""
params.parentid = m.top.itemId
end if
' Handle special case when getting names starting with numeral
if m.top.NameStartsWith <> ""
if m.top.NameStartsWith = "#"
if m.top.ItemType = "LiveTV" or m.top.ItemType = "TvChannel"
params.searchterm = "A"
params.append({ parentid: " " })
else
params.NameLessThan = "A"
end if
else
if m.top.ItemType = "LiveTV" or m.top.ItemType = "TvChannel"
params.searchterm = m.top.nameStartsWith
params.append({ parentid: " " })
else
params.NameStartsWith = m.top.nameStartsWith
end if
end if
end if
'reset data
if LCase(m.top.searchTerm) = LCase(tr("all"))
params.searchTerm = " "
else if m.top.searchTerm <> ""
params.searchTerm = m.top.searchTerm
end if
filter = LCase(m.top.filter)
if filter = "all"
' do nothing
else if filter = "favorites"
params.append({ Filters: "IsFavorite" })
params.append({ isFavorite: true })
else if filter = "unplayed"
params.append({ Filters: "IsUnplayed" })
else if filter = "played"
params.append({ Filters: "IsPlayed" })
else if filter = "resumable"
params.append({ Filters: "IsResumable" })
end if
if isValid(m.top.filterOptions)
if m.top.filterOptions.count() > 0
params.append(m.top.filterOptions)
end if
end if
if m.top.ItemType <> ""
params.append({ IncludeItemTypes: m.top.ItemType })
end if
' queryType drives which api namespace is called — used for both the main request
' and the optional 2nd lookup for the # (special characters) filter below.
queryType = "usersItems"
if m.top.ItemType = "LiveTV"
queryType = "liveTV"
params.append({ UserId: globalUser.id })
else if m.top.view = "Networks"
queryType = "studios"
params.append({ UserId: globalUser.id })
else if m.top.view = "Genres"
queryType = "genres"
params.append({ UserId: globalUser.id, includeItemTypes: m.top.itemType })
else if m.top.ItemType = "MusicArtist"
queryType = "artists"
' Merge Genres with existing fields instead of overriding
if fields.Instr(",Genres") = -1 and fields <> "Genres"
fields = fields + ",Genres"
end if
params.append({
UserId: globalUser.id,
Fields: fields,
IncludeItemTypes: "MusicAlbum,Audio"
})
else if m.top.ItemType = "AlbumArtists"
queryType = "albumArtists"
' Merge Genres with existing fields instead of overriding
if fields.Instr(",Genres") = -1 and fields <> "Genres"
fields = fields + ",Genres"
end if
params.append({
UserId: globalUser.id,
Fields: fields,
IncludeItemTypes: "MusicAlbum,Audio"
})
else if m.top.ItemType = "MusicAlbum"
params.append({ ImageTypeLimit: 1 })
params.append({ EnableImageTypes: "Primary,Backdrop,Banner,Thumb" })
end if
' MusicAlbum and the generic else both use queryType = "usersItems" (default)
data = executeItemQuery(queryType, params)
' If user has filtered by #, include special characters sorted after Z as well
if isValid(params.NameLessThan)
if LCase(params.NameLessThan) = "a"
' Use same params except for name filter param
params.NameLessThan = ""
params.NameStartsWithOrGreater = "z"
' Perform 2nd API lookup for items starting with Z or greater
startsWithZAndGreaterData = executeItemQuery(queryType, params)
if isValidAndNotEmpty(startsWithZAndGreaterData)
specialCharacterItems = []
' Filter out items starting with Z
for each item in startsWithZAndGreaterData.Items
itemName = LCase(item.name)
if not itemName.StartsWith("z")
specialCharacterItems.Push(item)
end if
end for
' Append data to results from before A
data.Items.Append(specialCharacterItems)
data.TotalRecordCount += specialCharacterItems.Count()
end if
end if
end if
if isValid(data)
if isValid(data.TotalRecordCount) then m.top.totalRecordCount = data.TotalRecordCount
for each item in data.Items
tmp = invalid
if item.Type = "Genre"
' Genre view: build row container with sampled items beneath it
tmp = CreateObject("roSGNode", "ContentNode")
tmp.title = item.name
genreData = GetApi().GetItemsByQuery({
SortBy: "Random",
SortOrder: "Ascending",
IncludeItemTypes: m.top.itemType,
Recursive: true,
Fields: "PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo,BackdropImageTags",
ImageTypeLimit: 1,
EnableImageTypes: "Primary,Backdrop",
Limit: 6,
GenreIds: item.id,
EnableTotalRecordCount: false,
ParentId: m.top.itemId
})
if genreData.Items.Count() > 5
' Add View All item to the start of the row — transform the genre item itself
viewAllNode = m.transformer.transformBaseItem(item)
viewAllNode.title = tr("View All") + " " + item.name
' Set library context for genre navigation (genre items have no parentId from API)
if not isValidAndNotEmpty(viewAllNode.parentId)
viewAllNode.parentId = m.top.itemId
end if
' Infer collection type from sampled items so DeterminePresenterType can route
' music genres to MusicPresenter instead of GenericPresenter
firstItemType = LCase(genreData.Items[0].Type ?? "")
if firstItemType = "audio" or firstItemType = "musicalbum" or firstItemType = "musicartist"
viewAllNode.collectionType = "music"
else if firstItemType = "movie"
viewAllNode.collectionType = "movies"
else if firstItemType = "series" or firstItemType = "episode"
viewAllNode.collectionType = "tvshows"
end if
tmp.appendChild(viewAllNode)
end if
for each genreItem in genreData.Items
row = m.transformer.transformBaseItem(genreItem)
tmp.appendChild(row)
end for
else
' All other item types: transform to JellyfinBaseItem
tmp = m.transformer.transformBaseItem(item)
' Set library context for navigation on virtual items (Genre/Studio/MusicGenre)
' that have no meaningful parentId in the API response
if not isValidAndNotEmpty(tmp.parentId)
tmp.parentId = m.top.itemId
end if
' Studio items (IsFolder=false, type="Studio") don't carry a CollectionType from the API.
' Infer it from the item type being fetched so DeterminePresenterType can route correctly.
if LCase(tmp.type) = "studio" and not isValidAndNotEmpty(tmp.collectionType)
if LCase(m.top.itemType) = "movie"
tmp.collectionType = "movies"
else if LCase(m.top.itemType) = "series"
tmp.collectionType = "tvshows"
end if
end if
end if
if isValid(tmp)
results.push(tmp)
end if
end for
end if
m.top.content = results
end sub
' Routes an item query through the ApiClient (GetApi()) based on queryType.
' Used for both the main request and the optional 2nd lookup for the # filter.
' queryType values: "liveTV", "studios", "genres", "artists", "albumArtists", "usersItems"
function executeItemQuery(queryType as string, params as object) as dynamic
if queryType = "liveTV"
return GetApi().GetLiveTVChannels(params)
else if queryType = "studios"
return GetApi().GetStudios(params)
else if queryType = "genres"
return GetApi().GetGenres(params)
else if queryType = "artists"
return GetApi().GetArtists(params)
else if queryType = "albumArtists"
return GetApi().GetAlbumArtists(params)
end if
' default: "usersItems"
return GetApi().GetItemsByQuery(params)
end function