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