components_home_LoadItemsTask.bs

import "pkg:/source/api/ApiClient.bs"
import "pkg:/source/api/apiPool.bs"
import "pkg:/source/api/items.bs"
import "pkg:/source/data/JellyfinDataTransformer.bs"
import "pkg:/source/roku_modules/log/LogMixin.brs"
import "pkg:/source/translationKeys.bs"
import "pkg:/source/utils/config.bs"
import "pkg:/source/utils/misc.bs"

sub init()
  m.log = new log.Logger("LoadItemsTask")
  m.top.functionName = "loadItems"
end sub

sub loadItems()
  globalUser = m.global.user
  m.transformer = JellyfinDataTransformer()

  results = []

  ' Load Libraries
  if m.top.itemsToLoad = "libraries"

    data = fetchJson(GetApi().BuildGetViewsRequest(), "libraries")
    if isValid(data) and isValid(data.Items)
      for each item in data.Items
        ' Skip Books for now as we don't support it (issue #525)
        if item.CollectionType <> "books"
          results.push(m.transformer.transformBaseItem(item))
        end if
      end for
    end if

    ' Load Next Up
  else if m.top.itemsToLoad = "nextUp"
    userSettings = globalUser.settings

    params = {}
    params["recursive"] = true
    params["SortBy"] = "DatePlayed"
    params["SortOrder"] = "Descending"
    params["EnableRewatching"] = userSettings.uiDetailsEnableRewatchingNextUp
    params["DisableFirstEpisode"] = false
    ' DELIBERATELY NO `limit`. Next Up is a WORKLIST — the user is trying to get through it,
    ' so any cap hides episodes they are part-way into a series on, and no number is the right
    ' one to hide at. It sent 69 for a long time, which was arbitrary rather than derived.
    ' `uiHomeRowLimit` deliberately does NOT reach here: that setting sizes the browse FEEDS
    ' (Recently Added / On Now / Active Recordings), where a limit chooses how much of a
    ' larger set to sample. Continue Watching, the other worklist, sends no limit either and
    ' has shipped that way — the two now match on purpose rather than by omission.
    params["EnableTotalRecordCount"] = false
    params["EnableResumable"] = false

    maxDaysInNextUp = userSettings.uiDetailsMaxDaysNextUp
    if isValid(maxDaysInNextUp)
      if maxDaysInNextUp > 0
        dateToday = CreateObject("roDateTime")
        dateCutoff = CreateObject("roDateTime")

        dateCutoff.FromSeconds(dateToday.AsSeconds() - (maxDaysInNextUp * 86400))

        params["NextUpDateCutoff"] = dateCutoff.ToISOString()
      end if
    end if

    data = fetchJson(GetApi().BuildGetNextUpRequest(params), "nextUp")
    if isValid(data) and isValid(data.Items)
      for each item in data.Items
        results.push(m.transformer.transformBaseItem(item))
      end for
    end if

    ' Load Continue Watching
  else if m.top.itemsToLoad = "continue"
    activeUser = globalUser.id
    if isValid(activeUser)
      ' DELIBERATELY NO `limit`, and now stated rather than merely absent — the silence here
      ' was previously indistinguishable from nobody having got round to it. Continue Watching
      ' is a WORKLIST: capping it can only hide something the user is mid-way through. Next Up
      ' is uncapped for the same reason; `uiHomeRowLimit` sizes the browse feeds only.
      params = {}
      params["EnableTotalRecordCount"] = false

      data = fetchJson(GetApi().BuildGetResumeItemsRequest(params), "continue")
      if isValid(data) and isValid(data.Items)
        for each item in data.Items
          ' Skip Books for now as we don't support it (issue #558)
          if item.Type <> "Book"
            results.push(m.transformer.transformBaseItem(item))
          end if
        end for
      end if
    end if

  else if m.top.itemsToLoad = "favorites"

    params = {}
    params["Filters"] = "IsFavorite"
    params["recursive"] = true
    params["SortBy"] = "SortName"
    params["SortOrder"] = "Ascending"
    params["EnableTotalRecordCount"] = false

    data = fetchJson(GetApi().BuildGetItemsByQueryRequest(params), "favorites")
    if isValid(data) and isValid(data.Items)
      for each item in data.Items
        ' Skip Books for now as we don't support it (issue #558)
        if item.Type <> "Book"
          ' Live TV recordings come back from /Items as the content type (Movie/Episode).
          ' The only reliable discriminator is the container format: recordings are always
          ' stored as MPEG transport streams ("ts"), while library items never are.
          if LCase(item.Container ?? "") = "ts"
            item.Type = "Recording"
          end if
          results.push(m.transformer.transformBaseItem(item))
        end if
      end for
    end if

    ' People — favorited persons are not returned by /Items; requires /Persons endpoint.
    personData = fetchJson(GetApi().BuildGetPersonsRequest({
      "IsFavorite": true,
      "SortBy": "SortName",
      "SortOrder": "Ascending",
      "EnableTotalRecordCount": false
    }), "favoritePeople")
    if isValid(personData) and isValid(personData.Items)
      for each item in personData.Items
        results.push(m.transformer.transformBaseItem(item))
      end for
    end if

    ' Note: MusicArtist items ARE returned by /Items above (they are regular library items),
    ' so no separate /Artists call is needed here (unlike search, where /Items misses them).

  else if m.top.itemsToLoad = "onNow"
    params = {}
    params["userId"] = globalUser.id
    params["isAiring"] = true
    ' The comment this replaces read "16 to be consistent with Latest In" — a parity three
    ' files asserted by hand and nothing enforced. Reading the same setting makes it hold.
    params["limit"] = resolveHomeRowLimit(globalUser.settings.uiHomeRowLimit)
    params["imageTypeLimit"] = 1
    params["enableImageTypes"] = "Primary,Thumb,Backdrop"
    params["enableTotalRecordCount"] = false
    params["fields"] = "ChannelInfo,PrimaryImageAspectRatio"

    data = fetchJson(GetApi().BuildGetLiveTvRecommendedProgramsRequest(params), "onNow")
    if isValid(data) and isValid(data.Items)
      for each item in data.Items
        results.push(m.transformer.transformBaseItem(item))
      end for
    end if

  else if m.top.itemsToLoad = "activeRecordings"
    params = {}
    params["userId"] = globalUser.id
    params["isInProgress"] = true
    params["limit"] = resolveHomeRowLimit(globalUser.settings.uiHomeRowLimit)
    params["imageTypeLimit"] = 1
    params["enableImageTypes"] = "Primary,Thumb,Backdrop"
    params["enableTotalRecordCount"] = false
    params["fields"] = "ChannelInfo,PrimaryImageAspectRatio"

    data = fetchJson(GetApi().BuildGetLiveTvRecordingsRequest(params), "activeRecordings")
    if isValid(data) and isValid(data.Items)
      for each item in data.Items
        results.push(m.transformer.transformBaseItem(item))
      end for
    end if

    ' Extract array of persons from Views and download full metadata for each
  else if m.top.itemsToLoad = "people"
    if not isValid(m.top.peopleList)
      m.top.content = results
      return
    end if
    for each person in m.top.peopleList
      node = m.transformer.transformPerson(person)
      if isValid(node) then results.push(node)
    end for
  else if m.top.itemsToLoad = "specialfeatures"
    data = fetchJson(GetApi().BuildGetSpecialFeaturesRequest(m.top.itemId), "specialfeatures")
    if isValid(data) and data.count() > 0
      for each specfeat in data
        node = m.transformer.transformBaseItem(specfeat)
        if isValid(node) then results.push(node)
      end for
    end if
  else if m.top.itemsToLoad = "additionalparts"
    data = fetchJson(GetApi().BuildGetAdditionalPartsRequest(m.top.itemId), "additionalparts")
    if isValid(data)
      for each part in data.items
        node = m.transformer.transformBaseItem(part)
        if isValid(node) then results.push(node)
      end for
    end if
  else if m.top.itemsToLoad = "likethis"
    params = { "userId": globalUser.id, "limit": 16 }
    data = fetchJson(GetApi().BuildGetSimilarItemsRequest(m.top.itemId, params), "likethis")
    if isValid(data) and isValid(data.Items)
      for each item in data.items
        node = m.transformer.transformBaseItem(item)
        if isValid(node) then results.push(node)
      end for
    end if
  else if m.top.itemsToLoad = "personMovies"
    getPersonVideos("Movie", results)
  else if m.top.itemsToLoad = "personTVShows"
    getPersonVideos("Episode", results)
  else if m.top.itemsToLoad = "personSeries"
    getPersonVideos("Series", results)
  else if m.top.itemsToLoad = "seasons"
    data = fetchJson(GetApi().BuildGetSeasonsRequest(m.top.itemId, { Fields: "PrimaryImageAspectRatio,Overview", EnableImages: true }), "seasons")
    if isValid(data) and isValid(data.Items)
      for each item in data.Items
        node = m.transformer.transformBaseItem(item)
        if isValid(node) then results.push(node)
      end for
    end if
  else if m.top.itemsToLoad = "seasonEpisodes"
    ' itemId = seriesId, metadata.seasonId = season to load
    if not isValid(m.top.metadata) or not isValidAndNotEmpty(m.top.metadata.seasonId)
      m.log.warn("seasonEpisodes requested but metadata.seasonId is missing or empty; skipping GetEpisodes call")
      m.top.content = results
      return
    end if
    data = fetchJson(GetApi().BuildGetEpisodesRequest(m.top.itemId, {
      SeasonId: m.top.metadata.seasonId,
      Fields: "PrimaryImageAspectRatio,Overview",
      EnableImages: true
    }), "seasonEpisodes")
    if isValid(data) and isValid(data.Items)
      for each item in data.Items
        node = m.transformer.transformBaseItem(item)
        if isValid(node) then results.push(node)
      end for
    end if
  else if m.top.itemsToLoad = "seriesFirstEpisode"
    data = fetchJson(GetApi().BuildGetEpisodesRequest(m.top.itemId, { Limit: 1, SortBy: "IndexNumber,SortName", SortOrder: "Ascending" }), "seriesFirstEpisode")
    if isValid(data) and isValid(data.Items) and data.Items.count() > 0
      node = m.transformer.transformBaseItem(data.Items[0])
      if isValid(node) then results.push(node)
    end if
  else if m.top.itemsToLoad = "seriesResume"
    ' For the series Resume button, prefer an in-progress (resumable) episode and fall back to the next unstarted episode.
    ' Uses the same endpoint as the Continue Watching home row so the Resume button is consistent with it.
    resumeData = fetchJson(GetApi().BuildGetResumeItemsRequest({
      ParentId: m.top.itemId,
      Limit: 1,
      Fields: "PrimaryImageAspectRatio",
      EnableTotalRecordCount: false
    }), "seriesResume")
    if isValid(resumeData) and isValid(resumeData.Items) and resumeData.Items.Count() > 0
      node = m.transformer.transformBaseItem(resumeData.Items[0])
      if isValid(node) then results.push(node)
    else
      ' No resumable episode — fall back to next unstarted episode.
      ' DisableFirstEpisode: true so a Resume button never appears for a completely unwatched series.
      nextUpData = fetchJson(GetApi().BuildGetNextUpRequest({ SeriesId: m.top.itemId, Limit: 1, DisableFirstEpisode: true, Fields: "PrimaryImageAspectRatio" }), "seriesNextUp")
      if isValid(nextUpData) and isValid(nextUpData.Items) and nextUpData.Items.Count() > 0
        node = m.transformer.transformBaseItem(nextUpData.Items[0])
        if isValid(node) then results.push(node)
      end if
    end if
  else if m.top.itemsToLoad = "metaData"
    results.push(ItemMetaData(m.top.itemId))
  else if m.top.itemsToLoad = "metaDataDetails"
    results.push(ItemDetailsMetaData(m.top.itemId, m.top.shouldForceRefresh))
  else if m.top.itemsToLoad = "audioStream"
    results.push(AudioStream(m.top.itemId))
  else if m.top.itemsToLoad = "backdropImage"
    results.push(BackdropImage(m.top.itemId))
  else if m.top.itemsToLoad = "boxsetitems"
    data = fetchJson(GetApi().BuildGetItemsByQueryRequest({
      "ParentId": m.top.itemId,
      "SortBy": "PremiereDate,ProductionYear,SortName",
      "SortOrder": "Ascending",
      "Fields": "PrimaryImageAspectRatio,Overview",
      "EnableTotalRecordCount": false
    }), "boxsetitems")
    if isValid(data) and isValid(data.Items)
      for each item in data.Items
        node = m.transformer.transformBaseItem(item)
        if isValid(node) then results.push(node)
      end for
    end if
  else if m.top.itemsToLoad = "artistAlbums"
    ' Albums where this artist is the album artist (AlbumArtistIds), sorted by year descending
    data = MusicAlbumList(m.top.itemId)
    if isValid(data) and isValid(data.Items)
      for each item in data.Items
        results.push(item)
      end for
    end if
  else if m.top.itemsToLoad = "artistAppearsOn"
    ' Albums where this artist contributes (ContributingArtistIds) but is not the album artist
    data = AppearsOnList(m.top.itemId)
    if isValid(data) and isValid(data.Items)
      for each item in data.Items
        results.push(item)
      end for
    end if
  else if m.top.itemsToLoad = "albumSongs"
    ' All Audio tracks under a MusicAlbum, sorted by track number
    data = MusicSongList(m.top.itemId)
    if isValid(data) and isValid(data.Items)
      for each item in data.Items
        results.push(item)
      end for
    end if
  else if m.top.itemsToLoad = "lyrics"
    ' Fetch lyrics for an Audio item and join lines into a single string.
    ' Returns a one-element array containing the lyrics string, or empty on failure.
    lyricsData = GetItemLyrics(m.top.itemId)
    if isValid(lyricsData) and isValid(lyricsData.Lyrics)
      lines = []
      for each lyric in lyricsData.Lyrics
        if isValid(lyric.Text) and lyric.Text <> ""
          lines.push(lyric.Text)
        end if
      end for
      if lines.count() > 0
        results.push(lines.join(Chr(10)))
      end if
    end if
  else if m.top.itemsToLoad = "artistSimilar"
    ' Similar artists via the artist-specific endpoint (more reliable than Items/{id}/Similar for MusicArtist)
    params = { "userId": globalUser.id, "limit": 16 }
    data = fetchJson(GetApi().BuildGetArtistSimilarRequest(m.top.itemId, params), "artistSimilar")
    if isValid(data) and isValid(data.Items)
      for each item in data.Items
        node = m.transformer.transformBaseItem(item)
        if isValid(node) then results.push(node)
      end for
    end if
  else if m.top.itemsToLoad = "artistSongs"
    ' All Audio tracks by this artist (ArtistIds = any capacity: album artist OR contributing).
    ' Sorted by album then track number so songs group naturally even without album rows.
    ' Limit 100 to avoid overwhelming the row for prolific artists.
    data = GetSongsByArtistBroad(m.top.itemId)
    if isValid(data) and isValid(data.Items)
      for each item in data.Items
        results.push(item)
      end for
    end if
  else if m.top.itemsToLoad = "playlistItems"
    ' All items in a Playlist, sorted newest-added first.
    data = fetchJson(GetApi().BuildGetPlaylistItemsRequest(m.top.itemId, {
      "SortBy": "DateCreated",
      "SortOrder": "Descending",
      "Fields": "PrimaryImageAspectRatio,Overview",
      "EnableTotalRecordCount": false
    }), "playlistItems")
    if isValid(data) and isValid(data.Items)
      for each item in data.Items
        node = m.transformer.transformBaseItem(item)
        if isValid(node) then results.push(node)
      end for
    end if
  else if m.top.itemsToLoad = "photoAlbumItems"
    ' All photos in a PhotoAlbum, sorted by name.
    data = fetchJson(GetApi().BuildGetItemsByQueryRequest({
      "ParentId": m.top.itemId,
      "IncludeItemTypes": "Photo",
      "SortBy": "SortName",
      "SortOrder": "Ascending",
      "Fields": "PrimaryImageAspectRatio",
      "EnableTotalRecordCount": false
    }), "photoAlbumItems")
    if isValid(data) and isValid(data.Items)
      for each item in data.Items
        node = m.transformer.transformBaseItem(item)
        if isValid(node) then results.push(node)
      end for
    end if
  else if m.top.itemsToLoad = "channelPrograms"
    ' Upcoming (not yet aired) programs on a Live TV channel, sorted by start time.
    ' ChannelInfo required for subtitle display (channel name) in JRRowItem.
    data = fetchJson(GetApi().BuildGetLiveTVProgramsRequest({
      "ChannelIds": m.top.itemId,
      "HasAired": false,
      "SortBy": "StartDate",
      "SortOrder": "Ascending",
      "Limit": 20,
      "Fields": "ChannelInfo,PrimaryImageAspectRatio,Overview",
      "EnableTotalRecordCount": false
    }), "channelPrograms")
    if isValid(data) and isValid(data.Items)
      for each item in data.Items
        node = m.transformer.transformBaseItem(item)
        if isValid(node) then results.push(node)
      end for
    end if
  end if

  m.top.content = results

end sub

sub getPersonVideos(videoType as string, dest as object)
  params = { personIds: m.top.itemId, recursive: true, includeItemTypes: videoType, Limit: 50, SortBy: "Random" }
  data = fetchJson(GetApi().BuildGetItemsByQueryRequest(params), "personVideos_" + videoType)
  if isValid(data) and data.count() > 0
    for each item in data.items
      node = m.transformer.transformBaseItem(item)
      if isValid(node) then dest.push(node)
    end for
  end if
end sub