R/st_app_details.R

Defines functions process_app_details_response st_app_details

Documented in process_app_details_response st_app_details

#' Fetch Detailed App Metadata
#'
#' Retrieves comprehensive metadata for one or more apps including descriptions,
#' screenshots, ratings, publisher information, and more. This function provides
#' rich app store listing data for apps when you already know their IDs.
#'
#' @param app_ids Character vector. App IDs to fetch details for. 
#'   - For iOS: numeric app IDs (e.g., "553834731")
#'   - For Android: bundle IDs (e.g., "com.king.candycrushsaga")
#'   - For unified: unified app IDs
#'   Maximum 100 apps per request.
#' @param os Character string. Required. Operating system: "ios", "android", or "unified".
#' @param include_developer_contacts Logical. Include developer contact information
#'   (email, address). Defaults to TRUE.
#' @param auth_token Character string. Sensor Tower API authentication token.
#'   Defaults to environment variable SENSORTOWER_AUTH_TOKEN.
#'
#' @return A [tibble][tibble::tibble] containing detailed app metadata with columns:
#'   - `app_id`: The app's store ID
#'   - `app_name`: The app's display name
#'   - `publisher_name`: Publisher/developer name
#'   - `publisher_id`: Publisher ID
#'   - `categories`: App store categories
#'   - `description`: Full app description
#'   - `subtitle`: App subtitle (iOS) or short description (Android)
#'   - `rating`: Current average rating
#'   - `rating_count`: Total number of ratings
#'   - `rating_current_version`: Rating for current version
#'   - `rating_count_current_version`: Rating count for current version
#'   - `content_rating`: Age rating/content rating
#'   - `price`: App price
#'   - `currency`: Price currency
#'   - `release_date`: Initial release date
#'   - `last_update`: Last update date
#'   - `version`: Current version
#'   - `size_bytes`: App size in bytes
#'   - `screenshots`: List of screenshot URLs
#'   - `icon_url`: App icon URL
#'   - `publisher_email`: Developer email (if include_developer_contacts = TRUE)
#'   - `publisher_address`: Developer address (if include_developer_contacts = TRUE)
#'   - `publisher_country`: Developer country
#'   - `ios_app_id`: Primary iOS app ID (for `os = "unified"`, when available)
#'   - `android_app_id`: Primary Android app ID (for `os = "unified"`, when available)
#'   - Additional platform-specific fields
#'
#' @section API Endpoint Used:
#'   - `GET /v1/\{os\}/apps`
#'
#' @examples
#' \dontrun{
#' # Get details for a single iOS app
#' candy_crush <- st_app_details(
#'   app_ids = "553834731",
#'   os = "ios"
#' )
#'
#' # Get details for multiple Android apps
#' android_games <- st_app_details(
#'   app_ids = c("com.king.candycrushsaga", "com.supercell.clashofclans"),
#'   os = "android"
#' )
#'
#' # Get details without developer contacts
#' apps <- st_app_details(
#'   app_ids = c("553834731", "1053012308"),
#'   include_developer_contacts = FALSE
#' )
#' }
#'
#' @importFrom rlang %||% abort
#' @importFrom httr2 resp_body_raw
#' @importFrom jsonlite fromJSON
#' @importFrom tibble tibble as_tibble as_tibble_row
#' @importFrom dplyr rename bind_rows
#' @export
st_app_details <- function(app_ids,
                          os,
                          include_developer_contacts = TRUE,
                          auth_token = NULL) {
  
  # Validate OS parameter
  if (missing(os) || is.null(os) || !os %in% c("ios", "android", "unified")) {
    rlang::abort("'os' parameter is required and must be one of: 'ios', 'android', or 'unified'")
  }
  
  # Input validation
  if (missing(app_ids) || length(app_ids) == 0) {
    rlang::abort("At least one app_id is required.")
  }
  
  if (length(app_ids) > 100) {
    rlang::abort("Maximum 100 app IDs allowed per request.")
  }
  
  # Authentication
  auth_token_val <- resolve_auth_token(
    auth_token,
    error_message = "Authentication token not found. Set SENSORTOWER_AUTH_TOKEN environment variable or pass via auth_token argument."
  )
  
  # Build query parameters
  query_params <- list(
    auth_token = auth_token_val,
    app_ids = paste(app_ids, collapse = ",")
  )

  # Unified app details endpoint requires app_id_type.
  if (os == "unified") {
    query_params$app_id_type <- "unified"
  }
  
  if (include_developer_contacts) {
    query_params$attributes = "contact_info"
  }
  
  # Build and perform request
  path <- st_endpoint_segments("apps", os = os)
  req <- build_request(st_api_base_url(), path, query_params)
  resp <- perform_request(req)
  
  # Process response
  result <- process_app_details_response(resp, os)
  
  return(result)
}

#' Process App Details API Response
#'
#' Internal function to process and enrich app details API responses.
#'
#' @param resp Response object from httr2
#' @param os Operating system
#'
#' @return A processed tibble with app details
#' @keywords internal
process_app_details_response <- function(resp, os) {
  
  # Get raw response
  body_raw <- httr2::resp_body_raw(resp)
  if (length(body_raw) == 0) {
    return(tibble::tibble())
  }
  
  body_text <- rawToChar(body_raw)
  result <- jsonlite::fromJSON(body_text, flatten = TRUE)
  
  if (length(result) == 0) {
    return(tibble::tibble())
  }
  
  # The API returns a list with an "apps" key containing a data frame
  if (is.list(result) && "apps" %in% names(result)) {
    result_tbl <- tibble::as_tibble(result$apps)
  } else if (is.data.frame(result)) {
    result_tbl <- tibble::as_tibble(result)
  } else {
    # Fallback - shouldn't reach here normally
    return(tibble::tibble())
  }
  
  if (nrow(result_tbl) == 0) {
    return(result_tbl)
  }
  
  # Platform-specific field mappings
  if (os == "ios") {
    # iOS field mappings
    if ("name" %in% names(result_tbl) && !"app_name" %in% names(result_tbl)) {
      result_tbl <- dplyr::rename(result_tbl, app_name = name)
    }
    
    # Handle categories - iOS uses primary_category and category_ids
    if ("primary_category" %in% names(result_tbl) || "category_ids" %in% names(result_tbl)) {
      # Categories might be a complex structure, keep as is for now
    }
    
    # Handle version-specific ratings
    if ("user_rating" %in% names(result_tbl)) {
      result_tbl <- dplyr::rename(result_tbl, rating = user_rating)
    }
    if ("user_rating_count" %in% names(result_tbl)) {
      result_tbl <- dplyr::rename(result_tbl, rating_count = user_rating_count)
    }
    
  } else if (os == "android") {
    # Android field mappings
    if ("title" %in% names(result_tbl) && !"app_name" %in% names(result_tbl)) {
      result_tbl <- dplyr::rename(result_tbl, app_name = title)
    }
    # Also check for "name" field
    if ("name" %in% names(result_tbl) && !"app_name" %in% names(result_tbl)) {
      result_tbl <- dplyr::rename(result_tbl, app_name = name)
    }
    
    # Android uses score instead of rating
    if ("score" %in% names(result_tbl) && !"rating" %in% names(result_tbl)) {
      result_tbl <- dplyr::rename(result_tbl, rating = score)
    }
    
    # Handle developer info
    if ("developer" %in% names(result_tbl) && !"publisher_name" %in% names(result_tbl)) {
      result_tbl <- dplyr::rename(result_tbl, publisher_name = developer)
    }
    if ("developer_id" %in% names(result_tbl) && !"publisher_id" %in% names(result_tbl)) {
      result_tbl <- dplyr::rename(result_tbl, publisher_id = developer_id)
    }
    if ("developer_email" %in% names(result_tbl) && !"publisher_email" %in% names(result_tbl)) {
      result_tbl <- dplyr::rename(result_tbl, publisher_email = developer_email)
    }
    if ("developer_address" %in% names(result_tbl) && !"publisher_address" %in% names(result_tbl)) {
      result_tbl <- dplyr::rename(result_tbl, publisher_address = developer_address)
    }
  } else if (os == "unified") {
    if ("name" %in% names(result_tbl) && !"app_name" %in% names(result_tbl)) {
      result_tbl <- dplyr::rename(result_tbl, app_name = name)
    }

    if ("unified_app_id" %in% names(result_tbl) && !"app_id" %in% names(result_tbl)) {
      result_tbl <- dplyr::rename(result_tbl, app_id = unified_app_id)
    }

    extract_first_app_id <- function(x) {
      if (is.null(x)) return(NA_character_)
      if (!is.data.frame(x) || nrow(x) == 0 || !"app_id" %in% names(x)) return(NA_character_)
      as.character(x$app_id[1])
    }

    if ("itunes_apps" %in% names(result_tbl) && !"ios_app_id" %in% names(result_tbl)) {
      result_tbl$ios_app_id <- vapply(result_tbl$itunes_apps, extract_first_app_id, character(1))
    }

    if ("android_apps" %in% names(result_tbl) && !"android_app_id" %in% names(result_tbl)) {
      result_tbl$android_app_id <- vapply(result_tbl$android_apps, extract_first_app_id, character(1))
    }
  }
  
  # Ensure app_id is character type for consistent joining
  if ("app_id" %in% names(result_tbl)) {
    result_tbl$app_id <- as.character(result_tbl$app_id)
  }
  
  # Common field processing
  # Convert dates to Date objects
  date_fields <- c("release_date", "last_update", "current_version_release_date")
  for (field in date_fields) {
    if (field %in% names(result_tbl)) {
      result_tbl[[field]] <- as.Date(result_tbl[[field]])
    }
  }
  
  # Handle numeric fields
  numeric_fields <- c("price", "size_bytes", "rating", "rating_count")
  for (field in numeric_fields) {
    if (field %in% names(result_tbl)) {
      result_tbl[[field]] <- as.numeric(result_tbl[[field]])
    }
  }
  
  # Define preferred column order
  preferred_cols <- c(
    "app_id", "app_name", "publisher_name", "publisher_id",
    "categories", "description", "subtitle", "short_description",
    "rating", "rating_count", "rating_current_version", "rating_count_current_version",
    "content_rating", "price", "currency", "release_date", "last_update",
    "version", "size_bytes", "screenshots", "icon_url",
    "publisher_email", "publisher_address", "publisher_country"
  )
  
  # Reorder columns, keeping any additional fields at the end
  existing_cols <- intersect(preferred_cols, names(result_tbl))
  other_cols <- setdiff(names(result_tbl), preferred_cols)
  
  result_tbl <- result_tbl[, c(existing_cols, other_cols)]
  
  # Add OS information
  result_tbl$os <- os
  
  return(result_tbl)
}

Try the sensortowerR package in your browser

Any scripts or data that you put into this service are public.

sensortowerR documentation built on March 18, 2026, 5:07 p.m.