components_search_SearchTask.bs

import "pkg:/source/api/ApiClient.bs"
import "pkg:/source/api/apiPool.bs"
import "pkg:/source/data/JellyfinDataTransformer.bs"
import "pkg:/source/translationKeys.bs"
import "pkg:/source/utils/misc.bs"

sub init()
  m.top.functionName = "search"
end sub

sub search()
  if isValid(m.top.query) and m.top.query <> ""
    m.top.results = searchMedia(m.top.query)
  end if
end sub

' Searches multiple Jellyfin endpoints for media matching the query.
' Runs on SearchTask thread — fetchJson blocks waiting for ApiPool responses.
function searchMedia(query as string)
  if query = "" then return { Items: [], TotalRecordCount: 0 }

  transformer = JellyfinDataTransformer()
  allItems = []

  ' Regular library items — Person and MusicArtist are fetched via dedicated endpoints below.
  ' LiveTvProgram is excluded here because /Items EPG search is incomplete; /LiveTv/Programs is used instead.
  data = fetchJson(GetApi().BuildGetItemsByQueryRequest({
    "searchTerm": query,
    "IncludeItemTypes": "Movie,Series,Episode,Video,MusicVideo,Audio,MusicAlbum,Playlist,LiveTvChannel,PhotoAlbum,Photo,BoxSet",
    "EnableTotalRecordCount": false,
    "Recursive": true,
    "limit": 100
  }), "searchItems")
  if isValid(data) and isValid(data.Items)
    for each item in data.Items
      ' 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
      allItems.push(transformer.transformBaseItem(item))
    end for
  end if

  ' People — /Items does not return Person type; /Persons endpoint required.
  personData = fetchJson(GetApi().BuildGetPersonsRequest({
    "searchTerm": query,
    "Limit": 20,
    "EnableTotalRecordCount": false
  }), "searchPersons")
  if isValid(personData) and isValid(personData.Items)
    for each item in personData.Items
      allItems.push(transformer.transformBaseItem(item))
    end for
  end if

  ' Artists — /Items does not return MusicArtist type; /Artists endpoint required.
  artistData = fetchJson(GetApi().BuildGetArtistsRequest({
    "searchTerm": query,
    "Limit": 20,
    "EnableTotalRecordCount": false
  }), "searchArtists")
  if isValid(artistData) and isValid(artistData.Items)
    for each item in artistData.Items
      allItems.push(transformer.transformBaseItem(item))
    end for
  end if

  ' Live TV Programs — /LiveTv/Programs searches the full EPG; /Items only covers recordings.
  programData = fetchJson(GetApi().BuildGetLiveTVProgramsRequest({
    "SearchTerm": query,
    "Limit": 20,
    "EnableTotalRecordCount": false
  }), "searchPrograms")
  if isValid(programData) and isValid(programData.Items)
    for each item in programData.Items
      allItems.push(transformer.transformBaseItem(item))
    end for
  end if

  result = {}
  result.Items = allItems
  result.TotalRecordCount = allItems.count()
  return result
end function