source_api_ApiClient.bs

import "pkg:/source/api/sdk.bs"
import "pkg:/source/api/sdk.v1.bs"
import "pkg:/source/api/sdk.v2.bs"
import "pkg:/source/utils/misc.bs"

' Note: getApiVersionFromGlobal() is imported from misc.bs and serves as the
' single source of truth for API version detection across the entire app.

' ApiClient provides a centralized, stateful wrapper around the Jellyfin API.
' It automatically injects image parameters and version-specific fields for consistent,
' bulletproof item fetching across all Jellyfin server versions (10.7.0+).
'
' Usage:
'   api = GetApi()
'   data = api.GetItem(itemId, { fields: "Overview" })
'
' Or inline:
'   data = GetApi().GetItem(itemId, { fields: "Overview" })
'
' The singleton pattern ensures one instance is shared across the entire app.
class ApiClient
  ' Cache global reference so m.global works inside class methods
  private global = invalid

  sub new()
    m.global = GetGlobalAA().global
  end sub

  ' Default image parameters for all item endpoints
  ' These ensure backdrop, logo, and thumb images are always requested
  private imageDefaults = {
    EnableImageTypes: "Primary,Backdrop,Logo,Thumb",
    ImageTypeLimit: 1
  }

  ' Get current user ID from global state
  private function getUserId() as string
    return m.global.user.id
  end function

  ' Get API version from global state via helper
  ' Uses getApiVersionFromGlobal() as single source of truth for safe API version reading
  private function getApiVersion() as integer
    return getApiVersionFromGlobal()
  end function

  ' ═══════════════════════════════════════════════════════════════════════════
  ' USER ENDPOINTS - Version-aware (V1/V2 routing)
  ' When adding V3 support, add else-if branch in each method below
  ' ═══════════════════════════════════════════════════════════════════════════

  ' Get a single item by ID with automatic image and version field injection
  ' @param itemId - The Jellyfin item ID
  ' @param params - Optional query parameters (fields, etc.)
  ' @returns API response or invalid on error
  function GetItem(itemId as string, params = {} as object) as dynamic
    userId = m.getUserId()
    if userId = "" then return invalid

    if m.getApiVersion() >= 2
      return sdkV2.users.GetItem(userId, itemId, m.injectDefaults(params))
    end if
    return sdkV1.users.GetItem(userId, itemId, m.injectDefaults(params))
  end function

  ' Get items by query with automatic image and version field injection
  ' @param params - Query parameters (limit, sortBy, filters, etc.)
  ' @returns API response or invalid on error
  function GetItemsByQuery(params = {} as object) as dynamic
    userId = m.getUserId()
    if userId = "" then return invalid

    if m.getApiVersion() >= 2
      return sdkV2.users.GetItemsByQuery(userId, m.injectDefaults(params))
    end if
    return sdkV1.users.GetItemsByQuery(userId, m.injectDefaults(params))
  end function

  ' Get resume items (Continue Watching) with automatic image injection
  ' @param params - Optional query parameters
  ' @returns API response or invalid on error
  function GetResumeItems(params = {} as object) as dynamic
    userId = m.getUserId()
    if userId = "" then return invalid

    if m.getApiVersion() >= 2
      return sdkV2.users.GetResumeItemsByQuery(userId, m.injectDefaults(params))
    end if
    return sdkV1.users.GetResumeItemsByQuery(userId, m.injectDefaults(params))
  end function

  ' Get suggestions with automatic image injection
  ' @param params - Optional query parameters
  ' @returns API response or invalid on error
  function GetSuggestions(params = {} as object) as dynamic
    userId = m.getUserId()
    if userId = "" then return invalid

    if m.getApiVersion() >= 2
      return sdkV2.users.GetSuggestions(userId, m.injectDefaults(params))
    end if
    return sdkV1.users.GetSuggestions(userId, m.injectDefaults(params))
  end function

  ' Get latest media with automatic image injection
  ' @param params - Optional query parameters
  ' @returns API response or invalid on error
  function GetLatestMedia(params = {} as object) as dynamic
    userId = m.getUserId()
    if userId = "" then return invalid

    if m.getApiVersion() >= 2
      return sdkV2.users.GetLatestMedia(userId, m.injectDefaults(params))
    end if
    return sdkV1.users.GetLatestMedia(userId, m.injectDefaults(params))
  end function

  ' ═══════════════════════════════════════════════════════════════════════════
  ' RAW ACCESS - No image injection (for playback/internal use)
  ' ═══════════════════════════════════════════════════════════════════════════

  ' Get item without image parameters - use for playback info only
  ' @param itemId - The Jellyfin item ID
  ' @param params - Query parameters (passed through as-is)
  ' @returns API response or invalid on error
  function GetItemRaw(itemId as string, params = {} as object) as dynamic
    userId = m.getUserId()
    if userId = "" then return invalid

    if m.getApiVersion() >= 2
      return sdkV2.users.GetItem(userId, itemId, params)
    end if
    return sdkV1.users.GetItem(userId, itemId, params)
  end function

  ' Get items by query without image parameters
  ' @param params - Query parameters (passed through as-is)
  ' @returns API response or invalid on error
  function GetItemsByQueryRaw(params = {} as object) as dynamic
    userId = m.getUserId()
    if userId = "" then return invalid

    if m.getApiVersion() >= 2
      return sdkV2.users.GetItemsByQuery(userId, params)
    end if
    return sdkV1.users.GetItemsByQuery(userId, params)
  end function

  ' ═══════════════════════════════════════════════════════════════════════════
  ' SHOWS ENDPOINTS
  ' ═══════════════════════════════════════════════════════════════════════════

  ' Get seasons for a TV series with automatic image injection
  ' @param seriesId - The series ID
  ' @param params - Optional query parameters
  ' @returns API response or invalid on error
  function GetSeasons(seriesId as string, params = {} as object) as dynamic
    userId = m.getUserId()
    if userId = "" then return invalid

    mergedParams = m.injectDefaults(params)
    mergedParams.UserId = userId

    return sdk.shows.GetSeasons(seriesId, mergedParams)
  end function

  ' Get episodes for a TV season with automatic image injection
  ' @param seriesId - The series ID
  ' @param params - Optional query parameters (season number, etc.)
  ' @returns API response or invalid on error
  function GetEpisodes(seriesId as string, params = {} as object) as dynamic
    userId = m.getUserId()
    if userId = "" then return invalid

    mergedParams = m.injectDefaults(params)
    mergedParams.UserId = userId

    return sdk.shows.GetEpisodes(seriesId, mergedParams)
  end function

  ' ═══════════════════════════════════════════════════════════════════════════
  ' ARTIST ENDPOINTS
  ' ═══════════════════════════════════════════════════════════════════════════

  ' Get artist by name
  ' @param name - Artist name
  ' @param params - Optional query parameters
  ' @returns API response or invalid on error
  function GetArtistByName(name as string, params = {} as object) as dynamic
    return sdk.artists.GetByName(name, params)
  end function

  ' Get all artists with automatic image injection
  ' @param params - Query parameters
  ' @returns API response or invalid on error
  function GetArtists(params = {} as object) as dynamic
    userId = m.getUserId()
    if userId = "" then return invalid

    mergedParams = m.injectDefaults(params)
    mergedParams.UserId = userId
    return sdk.artists.Get(mergedParams)
  end function

  ' Get all album artists with automatic image injection
  ' @param params - Query parameters
  ' @returns API response or invalid on error
  function GetAlbumArtists(params = {} as object) as dynamic
    userId = m.getUserId()
    if userId = "" then return invalid

    mergedParams = m.injectDefaults(params)
    mergedParams.UserId = userId
    return sdk.artists.GetAlbumArtists(mergedParams)
  end function

  ' ═══════════════════════════════════════════════════════════════════════════
  ' PLAYLIST ENDPOINTS
  ' ═══════════════════════════════════════════════════════════════════════════

  ' Get playlist items
  ' @param playlistId - The playlist ID
  ' @param params - Optional query parameters
  ' @returns API response or invalid on error
  function GetPlaylistItems(playlistId as string, params = {} as object) as dynamic
    userId = m.getUserId()
    if userId = "" then return invalid

    mergedParams = {}
    mergedParams.append(params)
    mergedParams.UserId = userId

    return sdk.playlists.GetItems(playlistId, mergedParams)
  end function

  ' ═══════════════════════════════════════════════════════════════════════════
  ' ITEMS ENDPOINTS
  ' ═══════════════════════════════════════════════════════════════════════════

  ' Get instant mix based on item
  ' @param itemId - The item ID
  ' @param params - Optional query parameters
  ' @returns API response or invalid on error
  function GetInstantMix(itemId as string, params = {} as object) as dynamic
    userId = m.getUserId()
    if userId = "" then return invalid

    mergedParams = {}
    mergedParams.append(params)
    mergedParams.UserId = userId

    return sdk.items.GetInstantMix(itemId, mergedParams)
  end function

  ' Get intros for an item
  ' @param itemId - The item ID
  ' @returns API response or invalid on error
  function GetIntros(itemId as string) as dynamic
    userId = m.getUserId()
    if userId = "" then return invalid

    if m.getApiVersion() >= 2
      return sdkV2.users.GetIntros(userId, itemId)
    end if
    return sdkV1.users.GetIntros(userId, itemId)
  end function

  ' ═══════════════════════════════════════════════════════════════════════════
  ' THIN WRAPPERS - Pass-through to SDK for consistent UX
  ' These don't modify params but provide consistent api.* interface
  ' ═══════════════════════════════════════════════════════════════════════════

  ' Post playback info (used for starting playback)
  ' @param itemId - The item ID
  ' @param postData - Request body data
  ' @returns API response or invalid on error
  function PostPlaybackInfo(itemId as string, postData as object) as dynamic
    return sdk.items.PostPlayBackInfo(itemId, postData)
  end function

  ' Get similar artists
  ' @param id - Artist ID
  ' @param params - Optional query parameters
  ' @returns API response or invalid on error
  function GetSimilarArtists(id as string, params = {} as object) as dynamic
    return sdk.artists.GetSimilar(id, params)
  end function

  ' Get similar albums
  ' @param id - Album ID
  ' @param params - Optional query parameters
  ' @returns API response or invalid on error
  function GetSimilarAlbums(id as string, params = {} as object) as dynamic
    return sdk.albums.GetSimilar(id, params)
  end function

  ' Get album instant mix
  ' @param id - Album ID
  ' @param params - Optional query parameters
  ' @returns API response or invalid on error
  function GetAlbumInstantMix(id as string, params = {} as object) as dynamic
    return sdk.albums.GetInstantMix(id, params)
  end function

  ' Get local trailers for an item
  ' @param itemId - The item ID
  ' @returns API response or invalid on error
  function GetLocalTrailers(itemId as string) as dynamic
    userId = m.getUserId()
    if userId = "" then return invalid

    if m.getApiVersion() >= 2
      return sdkV2.users.GetLocalTrailers(userId, itemId)
    end if
    return sdkV1.users.GetLocalTrailers(userId, itemId)
  end function

  ' Get special features for an item
  ' @param itemId - The item ID
  ' @returns API response or invalid on error
  function GetSpecialFeatures(itemId as string) as dynamic
    userId = m.getUserId()
    if userId = "" then return invalid

    if m.getApiVersion() >= 2
      return sdkV2.users.GetSpecialFeatures(userId, itemId)
    end if
    return sdkV1.users.GetSpecialFeatures(userId, itemId)
  end function

  ' Mark item as favorite
  ' @param itemId - The item ID
  ' @returns API response or invalid on error
  function MarkFavorite(itemId as string) as dynamic
    userId = m.getUserId()
    if userId = "" then return invalid

    if m.getApiVersion() >= 2
      return sdkV2.users.MarkFavorite(userId, itemId)
    end if
    return sdkV1.users.MarkFavorite(userId, itemId)
  end function

  ' Unmark item as favorite
  ' @param itemId - The item ID
  ' @returns API response or invalid on error
  function UnmarkFavorite(itemId as string) as dynamic
    userId = m.getUserId()
    if userId = "" then return invalid

    if m.getApiVersion() >= 2
      return sdkV2.users.UnmarkFavorite(userId, itemId)
    end if
    return sdkV1.users.UnmarkFavorite(userId, itemId)
  end function

  ' Mark item as played
  ' @param itemId - The item ID
  ' @param params - Optional parameters (DatePlayed, etc.)
  ' @returns API response or invalid on error
  function MarkPlayed(itemId as string, params = {} as object) as dynamic
    userId = m.getUserId()
    if userId = "" then return invalid

    if m.getApiVersion() >= 2
      return sdkV2.users.MarkPlayed(userId, itemId, params)
    end if
    return sdkV1.users.MarkPlayed(userId, itemId, params)
  end function

  ' Mark item as unplayed
  ' @param itemId - The item ID
  ' @returns API response or invalid on error
  function UnmarkPlayed(itemId as string) as dynamic
    userId = m.getUserId()
    if userId = "" then return invalid

    if m.getApiVersion() >= 2
      return sdkV2.users.UnmarkPlayed(userId, itemId)
    end if
    return sdkV1.users.UnmarkPlayed(userId, itemId)
  end function

  ' Update user rating
  ' @param itemId - The item ID
  ' @param params - Rating parameters
  ' @returns API response or invalid on error
  function UpdateRating(itemId as string, params = {} as object) as dynamic
    userId = m.getUserId()
    if userId = "" then return invalid

    if m.getApiVersion() >= 2
      return sdkV2.users.UpdateRating(userId, itemId, params)
    end if
    return sdkV1.users.UpdateRating(userId, itemId, params)
  end function

  ' Delete user rating
  ' @param itemId - The item ID
  ' @returns API response or invalid on error
  function DeleteRating(itemId as string) as dynamic
    userId = m.getUserId()
    if userId = "" then return invalid

    if m.getApiVersion() >= 2
      return sdkV2.users.DeleteRating(userId, itemId)
    end if
    return sdkV1.users.DeleteRating(userId, itemId)
  end function

  ' Delete an item
  ' @param itemId - The item ID to delete
  ' @returns API response or invalid on error
  function DeleteItem(itemId as string) as dynamic
    return sdk.items.DeleteByID(itemId)
  end function

  ' ═══════════════════════════════════════════════════════════════════════════
  ' SESSION ENDPOINTS
  ' ═══════════════════════════════════════════════════════════════════════════

  ' Report playback started
  ' @param params - Playback parameters
  ' @returns API response or invalid on error
  function Playing(params as object) as dynamic
    return sdk.sessions.Playing(params)
  end function

  ' Report playback stopped
  ' @param params - Playback parameters
  ' @returns API response or invalid on error
  function PostStopped(params as object) as dynamic
    return sdk.sessions.PostStopped(params)
  end function

  ' Report playback progress
  ' @param params - Playback parameters
  ' @returns API response or invalid on error
  function PostProgress(params as object) as dynamic
    return sdk.sessions.PostProgress(params)
  end function

  ' Get active sessions
  ' @param params - Query parameters
  ' @returns API response or invalid on error
  function GetSessions(params = {} as object) as dynamic
    return sdk.sessions.Get(params)
  end function

  ' ═══════════════════════════════════════════════════════════════════════════
  ' SYSTEM ENDPOINTS
  ' ═══════════════════════════════════════════════════════════════════════════

  ' Get configuration by name
  ' @param name - Configuration name
  ' @returns API response or invalid on error
  function GetConfigurationByName(name as string) as dynamic
    return sdk.system.GetConfigurationByName(name)
  end function

  ' ═══════════════════════════════════════════════════════════════════════════
  ' BRANDING ENDPOINTS
  ' ═══════════════════════════════════════════════════════════════════════════

  ' Get branding configuration
  ' @returns API response or invalid on error
  function GetBrandingConfiguration() as dynamic
    return sdk.branding.GetConfiguration()
  end function

  ' ═══════════════════════════════════════════════════════════════════════════
  ' DISPLAY PREFERENCES ENDPOINTS
  ' ═══════════════════════════════════════════════════════════════════════════

  ' Get display preferences
  ' @param id - Preference ID
  ' @param params - Query parameters
  ' @returns API response or invalid on error
  function GetDisplayPreferences(id as string, params = {} as object) as dynamic
    return sdk.displayPreferences.Get(id, params)
  end function

  ' ═══════════════════════════════════════════════════════════════════════════
  ' AUTHENTICATION ENDPOINTS
  ' ═══════════════════════════════════════════════════════════════════════════

  ' Authenticate user by username and password
  ' @param username - User's username
  ' @param password - User's password
  ' @returns API response with auth token or invalid on error
  function AuthenticateByName(username as string, password as string) as dynamic
    return sdk.users.AuthenticateByName(username, password)
  end function

  ' Get user by ID
  ' @param userId - The user ID
  ' @returns API response or invalid on error
  function GetUser(userId as string) as dynamic
    return sdk.users.Get(userId)
  end function

  ' Get public users
  ' @returns API response or invalid on error
  function GetPublicUsers() as dynamic
    return sdk.users.GetPublic()
  end function

  ' Authenticate via Quick Connect
  ' @param secret - The Quick Connect secret
  ' @returns API response or invalid on error
  function AuthenticateWithQuickConnect(secret as string) as dynamic
    return sdk.users.AuthenticateWithQuickConnect(secret)
  end function

  ' ═══════════════════════════════════════════════════════════════════════════
  ' QUICK CONNECT ENDPOINTS
  ' ═══════════════════════════════════════════════════════════════════════════

  ' Initiate Quick Connect
  ' @returns API response with secret or invalid on error
  function InitiateQuickConnect() as dynamic
    return sdk.quickConnect.Initiate()
  end function

  ' Connect via Quick Connect
  ' @param secret - The Quick Connect secret
  ' @returns API response or invalid on error
  function ConnectQuickConnect(secret as string) as dynamic
    return sdk.quickConnect.Connect(secret)
  end function

  ' ═══════════════════════════════════════════════════════════════════════════
  ' VIEWS ENDPOINTS
  ' ═══════════════════════════════════════════════════════════════════════════

  ' Get user views
  ' @returns API response or invalid on error
  function GetViews() as dynamic
    userId = m.getUserId()
    if userId = "" then return invalid

    if m.getApiVersion() >= 2
      return sdkV2.users.GetViews(userId)
    end if
    return sdkV1.users.GetViews(userId)
  end function

  ' ═══════════════════════════════════════════════════════════════════════════
  ' NEXT UP ENDPOINTS
  ' ═══════════════════════════════════════════════════════════════════════════

  ' Get next up episodes with automatic image injection
  ' @param params - Query parameters
  ' @returns API response or invalid on error
  function GetNextUp(params = {} as object) as dynamic
    userId = m.getUserId()
    if userId = "" then return invalid

    mergedParams = m.injectDefaults(params)
    mergedParams.UserId = userId

    return sdk.shows.GetNextUp(mergedParams)
  end function

  ' Get upcoming episodes with automatic image injection
  ' @param params - Query parameters
  ' @returns API response or invalid on error
  function GetUpcoming(params = {} as object) as dynamic
    userId = m.getUserId()
    if userId = "" then return invalid

    mergedParams = m.injectDefaults(params)
    mergedParams.UserId = userId

    return sdk.shows.GetUpcoming(mergedParams)
  end function

  ' Get similar shows with automatic image injection
  ' @param id - Show ID
  ' @param params - Query parameters
  ' @returns API response or invalid on error
  function GetSimilarShows(id as string, params = {} as object) as dynamic
    userId = m.getUserId()
    if userId = "" then return invalid

    mergedParams = m.injectDefaults(params)
    mergedParams.UserId = userId

    return sdk.shows.GetSimilar(id, mergedParams)
  end function

  ' ═══════════════════════════════════════════════════════════════════════════
  ' VIDEOS ENDPOINTS
  ' ═══════════════════════════════════════════════════════════════════════════

  ' Get additional parts for a video
  ' @param itemId - The video item ID
  ' @returns API response or invalid on error
  function GetAdditionalParts(itemId as string) as dynamic
    return sdk.videos.GetAdditionalParts(itemId)
  end function

  ' ═══════════════════════════════════════════════════════════════════════════
  ' FILTERS ENDPOINTS
  ' ═══════════════════════════════════════════════════════════════════════════

  ' Get filters
  ' @param params - Query parameters
  ' @returns API response or invalid on error
  function GetFilters(params = {} as object) as dynamic
    return sdk.items.GetFilters(params)
  end function

  ' Get items by query (alternative endpoint)
  ' @param params - Query parameters
  ' @returns API response or invalid on error
  function GetByQuery(params = {} as object) as dynamic
    return sdk.items.GetByQuery(params)
  end function

  ' ═══════════════════════════════════════════════════════════════════════════
  ' IMAGE ENDPOINTS
  ' ═══════════════════════════════════════════════════════════════════════════

  ' Get image URL for an item by ID (HEAD request helper)
  ' @param id - Item ID
  ' @param imageType - Type of image
  ' @param imageIndex - Image index (default 0)
  ' @param params - Optional parameters
  ' @returns API response or invalid on error
  function HeadImageURLByName(id as string, imageType as string, imageIndex = 0 as integer, params = {} as object) as dynamic
    return sdk.items.HeadImageURLByName(id, imageType, imageIndex, params)
  end function

  ' Get image URL
  ' @param id - Item ID
  ' @param imageType - Type of image
  ' @param imageIndex - Image index
  ' @param params - Optional parameters
  ' @returns API response or invalid on error
  function GetImageURL(id as string, imageType as string, imageIndex = 0 as integer, params = {} as object) as dynamic
    return sdk.items.GetImageURL(id, imageType, imageIndex, params)
  end function

  ' Get user image URL
  ' @param id - User ID
  ' @param imageType - Type of image
  ' @param imageIndex - Image index
  ' @param params - Optional parameters
  ' @returns API response or invalid on error
  function GetUserImageURL(id as string, imageType as string, imageIndex = 0 as integer, params = {} as object) as dynamic
    if m.getApiVersion() >= 2
      return sdkV2.users.GetImageURL(id, imageType, imageIndex, params)
    end if
    return sdkV1.users.GetImageURL(id, imageType, imageIndex, params)
  end function

  ' ═══════════════════════════════════════════════════════════════════════════
  ' LIVETV ENDPOINTS
  ' ═══════════════════════════════════════════════════════════════════════════

  ' Get Live TV channels
  ' @param params - Query parameters
  ' @returns API response or invalid on error
  function GetLiveTVChannels(params = {} as object) as dynamic
    return sdk.liveTV.GetChannels(params)
  end function

  ' ═══════════════════════════════════════════════════════════════════════════
  ' STUDIOS ENDPOINTS
  ' ═══════════════════════════════════════════════════════════════════════════

  ' Get studios
  ' @param params - Query parameters
  ' @returns API response or invalid on error
  function GetStudios(params = {} as object) as dynamic
    return sdk.studios.Get(params)
  end function

  ' ═══════════════════════════════════════════════════════════════════════════
  ' GENRES ENDPOINTS
  ' ═══════════════════════════════════════════════════════════════════════════

  ' Get genres
  ' @param params - Query parameters
  ' @returns API response or invalid on error
  function GetGenres(params = {} as object) as dynamic
    return sdk.genres.Get(params)
  end function

  ' ═══════════════════════════════════════════════════════════════════════════
  ' PRIVATE HELPERS
  ' ═══════════════════════════════════════════════════════════════════════════

  ' Inject default image parameters and version-specific fields into params
  ' Delegates to injectApiParams() pure function for testability
  ' @param params - Original parameters object
  ' @returns New params object with defaults merged in
  private function injectDefaults(params as object) as object
    return injectApiParams(params, m.getApiVersion(), m.imageDefaults)
  end function

end class

' ═══════════════════════════════════════════════════════════════════════════
' SINGLETON FACTORY
' ═══════════════════════════════════════════════════════════════════════════

' Get the singleton ApiClient instance
' Creates the instance on first call, returns cached instance thereafter
' Usage: api = GetApi() or data = GetApi().GetItem(id)
' @returns ApiClient singleton instance
function GetApi() as ApiClient
  globalAA = GetGlobalAA()

  if not isValid(globalAA.apiClient)
    globalAA.apiClient = new ApiClient()
  end if

  return globalAA.apiClient
end function