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.
' Every result echoes the query it ANSWERS, so the caller can check that it got the one it
' asked for. This node is reused across keystrokes — each stops the run in flight and
' relaunches it — so `m.top.query` already holds the NEXT query by the time a delivery is
' handled, and without the echo a late one is indistinguishable from the live one. Taken
' from the local the search actually ran with, so the echo and the items cannot disagree.
'
' ⚠️ `m.top.results` is assigned ONCE, below, as this function's last act — and that is
' load-bearing, not incidental. It is what lets the caller's `control = "stop"` prevent a
' superseded query from delivering at all (measured: 12 keystrokes, 6 deliveries, every one
' current). Publishing partial results as each fetch lands would open exactly the race
' `SearchResults.loadResults` watches for.
function searchMedia(query as string)
if query = "" then return { Items: [], TotalRecordCount: 0, query: query }
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()
result.query = query
return result
end function