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/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 Latest Additions to Libraries
  else if m.top.itemsToLoad = "latest"
    activeUser = globalUser.id
    if isValid(activeUser)
      params = {}
      params["Limit"] = 16
      params["ParentId"] = m.top.itemId
      params["EnableImageTypes"] = "Primary,Backdrop,Thumb"
      params["ImageTypeLimit"] = 1
      params["EnableTotalRecordCount"] = false

      data = fetchJson(GetApi().BuildGetLatestMediaRequest(params), "latest")

      ' /Items/Latest returns a bare array of BaseItemDto (not paginated).
      if isValid(data)
        for each item in data
          ' Skip Books for now as we don't support it (issue #525)
          if item.Type <> "Book"
            results.push(m.transformer.transformBaseItem(item))
          end if
        end for
      end if
    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
    params["limit"] = 69
    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)
      params = {}
      params["recursive"] = true
      params["SortBy"] = "DatePlayed"
      params["SortOrder"] = "Descending"
      params["Filters"] = "IsResumable"
      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
    params["limit"] = 16 ' 16 to be consistent with "Latest In"
    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"] = 16 ' parity with onNow
    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,
      Filters: "IsResumable",
      SortBy: "DatePlayed",
      SortOrder: "Descending",
      Limit: 1,
      recursive: true,
      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