source_utils_mediaDisplayTitle.bs

import "pkg:/source/utils/languages.bs"

' getVideoResolutionLabel: Determine resolution label from video dimensions
' Resolution thresholds check width OR height (either can qualify)
' 4K never gets p/i suffix. All other resolutions get "p" or "i" based on interlacing.
'
' @param {integer} width - Video width in pixels
' @param {integer} height - Video height in pixels
' @param {boolean} isInterlaced - Whether the video is interlaced
' @returns {string} - Resolution label (e.g., "4K", "1080p", "720i") or "" if below all thresholds
function getVideoResolutionLabel(width as integer, height as integer, isInterlaced as boolean) as string
  ' 4K: Width >= 3800 OR Height >= 2000 (no p/i suffix)
  if width >= 3800 or height >= 2000
    return "4K"
  end if

  ' Determine scan type suffix for non-4K resolutions
  suffix = "p"
  if isInterlaced then suffix = "i"

  ' 1440: Width >= 2500 OR Height >= 1400
  if width >= 2500 or height >= 1400
    return "1440" + suffix
  end if

  ' 1080: Width >= 1800 OR Height >= 1000
  if width >= 1800 or height >= 1000
    return "1080" + suffix
  end if

  ' 720: Width >= 1200 OR Height >= 700
  if width >= 1200 or height >= 700
    return "720" + suffix
  end if

  ' 480: Width >= 700 OR Height >= 400
  if width >= 700 or height >= 400
    return "480" + suffix
  end if

  ' Below all thresholds
  return ""
end function

' resolveLanguageName: Convert a 3-letter ISO 639-2 language code to its full display name
' Uses getSubtitleLanguages() from languages.bs as the lookup table.
' Falls back to the raw code if not found in the lookup table.
'
' @param {string} langCode - 3-letter ISO 639-2 language code (e.g., "eng", "spa")
' @returns {string} - Full language name (e.g., "English", "Spanish") or raw code if unknown
function resolveLanguageName(langCode as string) as string
  if langCode = "" then return ""

  lowerCode = LCase(langCode)

  ' "und" (Undefined) is not useful to display — omit it entirely
  if lowerCode = "und" then return ""

  languages = getSubtitleLanguages()

  if languages.doesExist(lowerCode)
    return languages[lowerCode]
  end if

  return langCode
end function

' formatAudioChannels: Convert channel count integer to display format
' Ignores ChannelLayout field since it can return inconsistent strings (e.g., "stereo")
'
' @param {integer} channels - Number of audio channels
' @returns {string} - Channel layout string (e.g., "2.0", "5.1", "7.1") or "" if invalid/zero
function formatAudioChannels(channels as integer) as string
  if channels <= 0 then return ""

  if channels <= 2
    return channels.toStr().trim() + ".0"
  end if

  ' For > 2 channels: (channels - 1).1 (e.g., 6 -> "5.1", 8 -> "7.1")
  return (channels - 1).toStr().trim() + ".1"
end function

' formatVideoDisplayTitle: Build a display title for a video stream
' Format: "RESOLUTION CODEC" or "RESOLUTION CODEC RANGETYPE" (when HDR)
'
' @param {object} stream - Video MediaStream object from Jellyfin API
' @returns {string} - Formatted display title (e.g., "4K HEVC HDR10") or "N/A" if insufficient data
function formatVideoDisplayTitle(stream as object) as string
  if not isValid(stream) then return "N/A"

  parts = []

  ' 1. Resolution label
  width = 0
  height = 0
  isInterlaced = false
  if isValid(stream.Width) then width = stream.Width
  if isValid(stream.Height) then height = stream.Height
  if isValid(stream.IsInterlaced) then isInterlaced = stream.IsInterlaced

  resolutionLabel = getVideoResolutionLabel(width, height, isInterlaced)
  if resolutionLabel <> ""
    parts.push(resolutionLabel)
  end if

  ' 2. Codec (uppercase)
  if isValid(stream.Codec) and stream.Codec <> ""
    parts.push(UCase(stream.Codec))
  end if

  ' 3. HDR info - only when VideoRange = "HDR" (case-insensitive)
  if isValid(stream.VideoRange) and LCase(stream.VideoRange) = "hdr"
    hdrLabel = "HDR"

    if isValid(stream.VideoRangeType) and stream.VideoRangeType <> "" and LCase(stream.VideoRangeType) <> "unknown"
      rangeTypeLower = LCase(stream.VideoRangeType)

      if rangeTypeLower = "hdr10"
        hdrLabel = "HDR10"
      else if rangeTypeLower = "hdr10plus"
        hdrLabel = "HDR10+"
      else if rangeTypeLower = "hlg"
        hdrLabel = "HLG"
      else if rangeTypeLower.left(4) = "dovi"
        ' All Dolby Vision variants: DOVI, DOVIWithSDR, DOVIWithHDR10, DOVIWithHLG, etc.
        hdrLabel = "DV"

        if isValid(stream.DvProfile)
          doviProfile = stream.DvProfile.toStr().trim()
          if doviProfile <> "" and doviProfile <> "0"
            hdrLabel = hdrLabel + " " + doviProfile

            if isValid(stream.DvBlSignalCompatibilityId)
              doviCompatId = stream.DvBlSignalCompatibilityId.toStr().trim()
              if doviCompatId <> "" and doviCompatId <> "0"
                hdrLabel = hdrLabel + "." + doviCompatId
              end if
            end if
          end if
        end if
      else
        ' Unrecognized HDR type - show raw value as-is
        hdrLabel = stream.VideoRangeType
      end if
    end if

    parts.push(hdrLabel)
  end if

  ' If no parts could be assembled, return "N/A"
  if parts.count() = 0 then return "N/A"

  return parts.join(" ")
end function

' formatVideoSourceTitle: Build a display title for a video source (MediaSource)
' Finds the first video stream and formats it via formatVideoDisplayTitle().
' When multiple sources exist (i.e., multiple versions of the same video),
' prepends the source Name (e.g., "[B&W] 1080p H264").
'
' @param {object} mediaSource - A MediaSource object from Jellyfin API (must have mediaStreams array)
' @param {boolean} hasMultipleSources - True when item has more than one MediaSource version
' @returns {string} - Formatted display title (e.g., "[B&W] 1080p H264") or "N/A" if no video stream found
function formatVideoSourceTitle(mediaSource as object, hasMultipleSources as boolean) as string
  if not isValid(mediaSource) or not isValid(mediaSource.mediaStreams) then return "N/A"

  videoStream = getFirstVideoStream(mediaSource.mediaStreams)
  if not isValid(videoStream) then return "N/A"

  title = formatVideoDisplayTitle(videoStream)

  ' Prepend the source name when multiple versions exist (e.g., "[B&W]")
  if hasMultipleSources and isValidAndNotEmpty(mediaSource.Name)
    title = mediaSource.Name + " " + title
  end if

  return title
end function

' formatAudioDisplayTitle: Build a display title for an audio stream
' Format: "Language CODEC CHANNELS" or "Language CODEC CHANNELS (default)"
'
' @param {object} stream - Audio MediaStream object from Jellyfin API
' @returns {string} - Formatted display title (e.g., "English AAC 2.0 (default)") or "N/A" if insufficient data
function formatAudioDisplayTitle(stream as object) as string
  if not isValid(stream) then return "N/A"

  parts = []

  ' 1. Language (full name from ISO 639-2 code; omitted for "und"/Undefined)
  if isValid(stream.Language) and stream.Language <> ""
    langName = resolveLanguageName(stream.Language)
    if langName <> "" then parts.push(langName)
  end if

  ' 2. Codec (uppercase)
  if isValid(stream.Codec) and stream.Codec <> ""
    parts.push(UCase(stream.Codec))
  end if

  ' 3. Channel layout (prefer ChannelLayout from server, fall back to computing from Channels)
  channelLabel = ""
  if isValid(stream.ChannelLayout) and stream.ChannelLayout <> ""
    channelLabel = stream.ChannelLayout
  else if isValid(stream.Channels)
    channelLabel = formatAudioChannels(stream.Channels)
  end if
  if channelLabel <> ""
    parts.push(channelLabel)
  end if

  ' If no parts could be assembled, return "N/A"
  if parts.count() = 0 then return "N/A"

  result = parts.join(" ")

  ' 4. Default suffix
  if isValid(stream.IsDefault) and stream.IsDefault = true
    result = result + " (default)"
  end if

  return result
end function

' formatSubtitleDisplayTitle: Build a display title for a subtitle stream
' Format: "Language CODEC", "Language CODEC (forced)", or "Language CODEC (default)"
' Forced takes precedence over default if both are set.
'
' @param {object} stream - Subtitle MediaStream object from Jellyfin API
' @returns {string} - Formatted display title (e.g., "English SUBRIP (forced)") or "N/A" if insufficient data
function formatSubtitleDisplayTitle(stream as object) as string
  if not isValid(stream) then return "N/A"

  parts = []

  ' 1. Language (full name from ISO 639-2 code; omitted for "und"/Undefined)
  if isValid(stream.Language) and stream.Language <> ""
    langName = resolveLanguageName(stream.Language)
    if langName <> "" then parts.push(langName)
  end if

  ' 2. Codec (uppercase)
  if isValid(stream.Codec) and stream.Codec <> ""
    parts.push(UCase(stream.Codec))
  end if

  ' If no parts could be assembled, return "N/A"
  if parts.count() = 0 then return "N/A"

  result = parts.join(" ")

  ' 3. Forced/Default suffix
  if isValid(stream.IsForced) and stream.IsForced = true
    result = result + " (forced)"
  else if isValid(stream.IsDefault) and stream.IsDefault = true
    result = result + " (default)"
  end if

  return result
end function