source_utils_searchOutcome.bs
import "pkg:/source/translationKeys.bs"
import "pkg:/source/utils/misc.bs"
' What a search ended with, and what the search screen shows for it. SearchTask reports the
' status; SearchResults shows the view. Pure, so the rules are unit-tested without a network
' seam. Also which reads a search sends (canSearchLiveTv), since a read the server refuses
' would make every search look partial.
'
' A read that failed says nothing about what the server holds, so a search that got no answer
' must never read as one that found nothing: "failed" offers Try again, "empty" says there are
' no results. A search whose reads partly answered shows what arrived and says some is missing.
namespace searchOutcome
const STATUS_OK = "ok"
const STATUS_PARTIAL = "partial"
const STATUS_FAILED = "failed"
const SHOW_RESULTS = "results"
const SHOW_EMPTY = "empty"
const SHOW_FAILED = "failed"
' How a search's reads ended: every one answered, some did, or none did. A search that sent
' nothing (no read could be built) failed, since nothing answered for it.
'
' @param {integer} sentCount - the reads asked for
' @param {integer} failedCount - the reads that did not answer
' @return {string} a STATUS_ constant
function statusOf(sentCount as integer, failedCount as integer) as string
if sentCount < 1 or failedCount >= sentCount then return searchOutcome.STATUS_FAILED
if failedCount > 0 then return searchOutcome.STATUS_PARTIAL
return searchOutcome.STATUS_OK
end function
' What the screen shows for a search that ended with `status` and `itemCount` items, the
' toast to raise with it, and what the screen reader announces ("" for none). A partial search
' that found nothing is a failure: the reads that answered say nothing about the ones that did
' not. A status that is none of these reads as failed, since nobody can vouch for its empty
' list.
'
' Failed and empty are announced because focus stays on the keyboard, and the screen reader
' speaks only what has focus: without it, neither is heard. Results need no announcement (the
' viewer moves into them), and a partial search's toast is its notice.
'
' @param {dynamic} status - a STATUS_ constant
' @param {integer} itemCount - the items the search found
' @return {object} { show: a SHOW_ constant, toastKey and announceKey: a translation key or "" }
function view(status as dynamic, itemCount as integer) as object
if not isValidAndNotEmpty(status) then status = searchOutcome.STATUS_FAILED
if status = searchOutcome.STATUS_OK
if itemCount > 0 then return { show: searchOutcome.SHOW_RESULTS, toastKey: "", announceKey: "" }
return { show: searchOutcome.SHOW_EMPTY, toastKey: "", announceKey: translationKeys.MessageSearchNoResults }
end if
if status = searchOutcome.STATUS_PARTIAL and itemCount > 0
return { show: searchOutcome.SHOW_RESULTS, toastKey: translationKeys.MessageSearchPartial, announceKey: "" }
end if
return { show: searchOutcome.SHOW_FAILED, toastKey: "", announceKey: translationKeys.MessageSearchFailed }
end function
' Whether a search sends its Live TV program read. Jellyfin refuses that read to a user
' without Live TV access (its LiveTvAccess policy), so asking would count every one of that
' user's searches as partial. A policy, or a field, that is missing reads as allowed, as
' SessionDataTransformer defaults it.
'
' @param {dynamic} policy - m.global.user.policy (JellyfinUserPolicy node or AA), or invalid
' @return {boolean} true when the search asks for Live TV programs
function canSearchLiveTv(policy as dynamic) as boolean
if not isValid(policy) then return true
access = policy.enableLiveTvAccess
if not isValid(access) then return true
return access <> false
end function
end namespace