R/espn_basketball_athlete_helpers.R

Defines functions .espn_basketball_athlete_statisticslog .espn_basketball_athlete_awards .espn_basketball_athlete_eventlog .espn_basketball_athlete_splits .espn_basketball_athlete_gamelog .espn_basketball_athlete_stats .espn_basketball_athlete_overview .espn_basketball_athlete_info

# espn_basketball_athlete_helpers.R
# Internal helpers shared by WBB and WNBA athlete endpoint wrappers.
# Each helper accepts league = "wnba" or league = "womens-college-basketball".
# None of these functions are exported.

# ---------------------------------------------------------------------------
# .espn_basketball_athlete_info  (2.1)
# site-v2 /athletes/{athlete_id}
# Returns named list: Bio, Team, Position, Status, College, Draft
# ---------------------------------------------------------------------------

#' Internal: ESPN basketball athlete info
#'
#' Fetches
#' `sports.core.api.espn.com/v2/sports/basketball/leagues/{league}/athletes/{athlete_id}`
#' and returns a named list of tibbles: `Bio`, `Team`, `Position`, `Status`,
#' `College`, `Draft`. The legacy `site.api.espn.com/apis/site/v2/...` path
#' was discontinued and now 404s for both leagues.
#'
#' @param league character. `"wnba"` or `"womens-college-basketball"`.
#' @param athlete_id character or numeric. ESPN athlete identifier.
#' @param ... Unused; absorbed for forward compatibility.
#' @return Named list of data frames.
#' @noRd
.espn_basketball_athlete_info <- function(league, athlete_id, ...) {
  .espn_bball_validate_league(league)
  .args <- list(league = league, athlete_id = athlete_id)

  result <- list()

  url <- paste0(
    "https://sports.core.api.espn.com/v2/sports/basketball/leagues/",
    league,
    "/athletes/",
    athlete_id
  )

  tryCatch(
    expr = {
      res <- .retry_request(url)
      check_status(res)
      raw <- res %>% .resp_text() %>% jsonlite::fromJSON(simplifyDataFrame = TRUE)

      ath <- raw[["athlete"]] %||% raw

      # Helper: extract a 1-row data.frame of selected keys from either a
      # data.frame (legacy site.api shape) or a named list (current core.api
      # shape). Skips list-valued fields ($ref siblings, etc.).
      .extract_row <- function(obj, keep) {
        if (is.null(obj)) return(data.frame(stringsAsFactors = FALSE))
        if (is.data.frame(obj)) {
          cols <- intersect(keep, colnames(obj))
          if (length(cols) == 0) return(data.frame(stringsAsFactors = FALSE))
          return(as.data.frame(obj[cols], stringsAsFactors = FALSE))
        }
        if (is.list(obj)) {
          present <- intersect(keep, names(obj))
          scalars <- Filter(function(v) length(v) == 1 && !is.list(v), obj[present])
          if (length(scalars) == 0) return(data.frame(stringsAsFactors = FALSE))
          return(as.data.frame(scalars, stringsAsFactors = FALSE))
        }
        data.frame(stringsAsFactors = FALSE)
      }

      # ---------- Bio ----------
      bio_keep <- c(
        "id", "uid", "guid", "firstName", "lastName", "fullName",
        "displayName", "shortName", "weight", "displayWeight",
        "height", "displayHeight", "age", "dateOfBirth",
        "debutYear", "jersey", "active"
      )
      bio_data <- as.list(.extract_row(ath, bio_keep))
      hs <- ath[["headshot"]]
      if (!is.null(hs)) {
        bio_data[["headshot_href"]] <- if (is.data.frame(hs)) hs[["href"]][[1]] %||% NA_character_
                                       else hs[["href"]] %||% NA_character_
      }
      bp <- ath[["birthPlace"]]
      if (!is.null(bp)) {
        get_bp <- function(k) {
          v <- if (is.data.frame(bp)) bp[[k]][[1]] else bp[[k]]
          v %||% NA_character_
        }
        bio_data[["birth_city"]]    <- get_bp("city")
        bio_data[["birth_state"]]   <- get_bp("state")
        bio_data[["birth_country"]] <- get_bp("country")
      }
      result[["Bio"]] <- data.frame(bio_data, stringsAsFactors = FALSE) %>%
        dplyr::as_tibble() %>%
        janitor::clean_names() %>%
        make_wehoop_data(
          paste0("ESPN ", toupper(league), " Athlete Bio from ESPN.com"),
          Sys.time()
        )

      # ---------- Team ----------
      team_keep <- c("id", "uid", "slug", "abbreviation", "displayName",
                     "shortDisplayName", "name", "location", "color", "$ref")
      team_df <- .extract_row(ath[["team"]], team_keep)
      # Rename "$ref" -> "ref" before clean_names() to avoid problematic glyph
      if ("$ref" %in% colnames(team_df)) {
        names(team_df)[names(team_df) == "$ref"] <- "ref"
      }
      result[["Team"]] <- team_df %>%
        dplyr::as_tibble() %>%
        janitor::clean_names() %>%
        make_wehoop_data(
          paste0("ESPN ", toupper(league), " Athlete Team from ESPN.com"),
          Sys.time()
        )

      # ---------- Position ----------
      pos_keep <- c("id", "name", "displayName", "abbreviation", "leaf")
      result[["Position"]] <- .extract_row(ath[["position"]], pos_keep) %>%
        dplyr::as_tibble() %>%
        janitor::clean_names() %>%
        make_wehoop_data(
          paste0("ESPN ", toupper(league), " Athlete Position from ESPN.com"),
          Sys.time()
        )

      # ---------- Status ----------
      stat_keep <- c("id", "name", "type", "abbreviation")
      result[["Status"]] <- .extract_row(ath[["status"]], stat_keep) %>%
        dplyr::as_tibble() %>%
        janitor::clean_names() %>%
        make_wehoop_data(
          paste0("ESPN ", toupper(league), " Athlete Status from ESPN.com"),
          Sys.time()
        )

      # ---------- College ----------
      coll_keep <- c("id", "mascot", "name", "shortName", "abbrev", "$ref")
      college_df <- .extract_row(ath[["college"]], coll_keep)
      if ("$ref" %in% colnames(college_df)) {
        names(college_df)[names(college_df) == "$ref"] <- "ref"
      }
      result[["College"]] <- college_df %>%
        dplyr::as_tibble() %>%
        janitor::clean_names() %>%
        make_wehoop_data(
          paste0("ESPN ", toupper(league), " Athlete College from ESPN.com"),
          Sys.time()
        )

      # ---------- Draft ----------
      draft_df <- data.frame(stringsAsFactors = FALSE)
      d_obj <- ath[["draft"]]
      if (!is.null(d_obj) && (is.list(d_obj) || is.data.frame(d_obj))) {
        get_d <- function(k) {
          v <- if (is.data.frame(d_obj)) d_obj[[k]][[1]] else d_obj[[k]]
          if (is.null(v) || is.list(v)) NA_character_ else as.character(v)
        }
        draft_df <- data.frame(
          year      = get_d("year"),
          round     = get_d("round"),
          selection = get_d("selection"),
          display_text = get_d("displayText"),
          stringsAsFactors = FALSE
        )
      }
      result[["Draft"]] <- draft_df %>%
        dplyr::as_tibble() %>%
        janitor::clean_names() %>%
        make_wehoop_data(
          paste0("ESPN ", toupper(league), " Athlete Draft from ESPN.com"),
          Sys.time()
        )
    },
    error = function(e) .report_api_error(
      e,
      hint = paste0("Failed to retrieve ESPN ", league,
                    " athlete info for athlete_id=", athlete_id),
      args = .args
    ),
    warning = function(w) .report_api_warning(
      w,
      hint = paste0("Warning retrieving ESPN ", league,
                    " athlete info for athlete_id=", athlete_id),
      args = .args
    ),
    finally = {}
  )
  return(result)
}

# ---------------------------------------------------------------------------
# .espn_basketball_athlete_overview  (2.2)
# web-common-v3 /athletes/{athlete_id}/overview?season={year}
# Returns named list: Statistics, NextGame, Last5Games, Headlines, FantasyOutlook
# ---------------------------------------------------------------------------

#' Internal: ESPN basketball athlete overview
#'
#' Fetches
#' `site.web.api.espn.com/apis/common/v3/sports/basketball/{league}/athletes/{athlete_id}/overview`
#' and returns a named list of tibbles.
#'
#' @param league character.
#' @param athlete_id character or numeric.
#' @param season numeric. Season year.
#' @param ... Unused.
#' @return Named list of data frames.
#' @noRd
.espn_basketball_athlete_overview <- function(league, athlete_id, season, ...) {
  .espn_bball_validate_league(league)
  .args <- list(league = league, athlete_id = athlete_id, season = season)

  result <- list()

  url <- paste0(
    "https://site.web.api.espn.com/apis/common/v3/sports/basketball/",
    league,
    "/athletes/",
    athlete_id,
    "/overview?season=",
    season
  )

  tryCatch(
    expr = {
      res <- .retry_request(url)
      check_status(res)
      raw <- res %>% .resp_text() %>% jsonlite::fromJSON(simplifyDataFrame = TRUE)

      # ---------- Statistics ----------
      stats_df <- data.frame(stringsAsFactors = FALSE)
      stat_raw <- raw[["statistics"]] %||% raw[["stats"]]
      if (!is.null(stat_raw) && is.data.frame(stat_raw) && nrow(stat_raw) > 0) {
        stats_df <- stat_raw %>%
          data.frame(stringsAsFactors = FALSE)
      }
      result[["Statistics"]] <- stats_df %>%
        dplyr::as_tibble() %>%
        janitor::clean_names() %>%
        make_wehoop_data(
          paste0("ESPN ", toupper(league), " Athlete Overview Statistics from ESPN.com"),
          Sys.time()
        )

      # ---------- NextGame ----------
      next_df <- data.frame(stringsAsFactors = FALSE)
      ng_raw <- raw[["nextOpponent"]] %||% raw[["nextGame"]] %||% raw[["nextEvent"]]
      if (!is.null(ng_raw) && is.data.frame(ng_raw) && nrow(ng_raw) > 0) {
        ng_keep <- c("id", "date", "name", "shortName")
        ng_data <- ng_raw[intersect(ng_keep, colnames(ng_raw))]
        next_df <- data.frame(ng_data, stringsAsFactors = FALSE)
      } else if (!is.null(ng_raw) && is.list(ng_raw)) {
        next_df <- data.frame(
          id         = as.character(ng_raw[["id"]] %||% NA_character_),
          date       = as.character(ng_raw[["date"]] %||% NA_character_),
          name       = as.character(ng_raw[["name"]] %||% NA_character_),
          short_name = as.character(ng_raw[["shortName"]] %||% NA_character_),
          stringsAsFactors = FALSE
        )
      }
      result[["NextGame"]] <- next_df %>%
        dplyr::as_tibble() %>%
        janitor::clean_names() %>%
        make_wehoop_data(
          paste0("ESPN ", toupper(league), " Athlete Overview NextGame from ESPN.com"),
          Sys.time()
        )

      # ---------- Last5Games ----------
      last5_df <- data.frame(stringsAsFactors = FALSE)
      l5_raw <- raw[["last5Games"]] %||% raw[["recentGames"]] %||% raw[["gameLog"]]
      if (!is.null(l5_raw) && is.data.frame(l5_raw) && nrow(l5_raw) > 0) {
        last5_df <- l5_raw %>% data.frame(stringsAsFactors = FALSE)
      }
      result[["Last5Games"]] <- last5_df %>%
        dplyr::as_tibble() %>%
        janitor::clean_names() %>%
        make_wehoop_data(
          paste0("ESPN ", toupper(league), " Athlete Overview Last5Games from ESPN.com"),
          Sys.time()
        )

      # ---------- Headlines ----------
      hdl_df <- data.frame(stringsAsFactors = FALSE)
      hdl_raw <- raw[["news"]] %||% raw[["headlines"]] %||% raw[["articles"]]
      if (!is.null(hdl_raw) && is.data.frame(hdl_raw) && nrow(hdl_raw) > 0) {
        hdl_keep <- c("headline", "description", "published", "byline", "type")
        hdl_df <- hdl_raw %>%
          dplyr::select(dplyr::any_of(hdl_keep)) %>%
          data.frame(stringsAsFactors = FALSE)
      }
      result[["Headlines"]] <- hdl_df %>%
        dplyr::as_tibble() %>%
        janitor::clean_names() %>%
        make_wehoop_data(
          paste0("ESPN ", toupper(league), " Athlete Overview Headlines from ESPN.com"),
          Sys.time()
        )

      # ---------- FantasyOutlook ----------
      fant_df <- data.frame(stringsAsFactors = FALSE)
      fo_raw <- raw[["fantasyOutlook"]] %||% raw[["fantasy"]]
      if (!is.null(fo_raw) && is.data.frame(fo_raw) && nrow(fo_raw) > 0) {
        fant_df <- fo_raw %>% data.frame(stringsAsFactors = FALSE)
      } else if (!is.null(fo_raw) && is.list(fo_raw)) {
        fant_df <- data.frame(
          outlook = as.character(fo_raw[["outlook"]] %||% NA_character_),
          stringsAsFactors = FALSE
        )
      }
      result[["FantasyOutlook"]] <- fant_df %>%
        dplyr::as_tibble() %>%
        janitor::clean_names() %>%
        make_wehoop_data(
          paste0("ESPN ", toupper(league), " Athlete Overview FantasyOutlook from ESPN.com"),
          Sys.time()
        )
    },
    error = function(e) .report_api_error(
      e,
      hint = paste0("Failed to retrieve ESPN ", league,
                    " athlete overview for athlete_id=", athlete_id,
                    ", season=", season),
      args = .args
    ),
    warning = function(w) .report_api_warning(
      w,
      hint = paste0("Warning retrieving ESPN ", league,
                    " athlete overview for athlete_id=", athlete_id),
      args = .args
    ),
    finally = {}
  )
  return(result)
}

# ---------------------------------------------------------------------------
# .espn_basketball_athlete_stats  (2.3)
# web-common-v3 /athletes/{athlete_id}/stats?season={year}
# Returns named list per category: General, Offensive, Defensive, Rebounding,
#   Shooting, Misc
# ---------------------------------------------------------------------------

#' Internal: ESPN basketball athlete stats
#'
#' Fetches
#' `site.web.api.espn.com/apis/common/v3/sports/basketball/{league}/athletes/{athlete_id}/stats`
#' and returns a single wide tibble: one row per (athlete_id, season, team)
#' with the per-category stats spread across prefixed columns
#' (`avg_*` season averages, `tot_*` season totals, `misc_*` miscellaneous
#' totals). ESPN supplies each category's positional stat keys in a parallel
#' `names` array, which we use to label the stat values.
#'
#' @param league character.
#' @param athlete_id character or numeric.
#' @param season numeric.
#' @param ... Unused.
#' @return A wide `wehoop_data` tibble (one row per athlete-season-team).
#' @noRd
.espn_basketball_athlete_stats <- function(league, athlete_id, season, ...) {
  .espn_bball_validate_league(league)
  .args <- list(league = league, athlete_id = athlete_id, season = season)

  desc <- paste0("ESPN ", toupper(league), " Athlete Stats from ESPN.com")
  result <- data.frame() %>%
    dplyr::as_tibble() %>%
    make_wehoop_data(desc, Sys.time())

  url <- paste0(
    "https://site.web.api.espn.com/apis/common/v3/sports/basketball/",
    league, "/athletes/", athlete_id, "/stats?season=", season
  )

  # Short, stable column prefixes per ESPN category name. Unknown categories
  # fall back to a cleaned version of their own name.
  prefix_for <- function(nm) {
    switch(tolower(nm),
      "averages"      = "avg",
      "totals"        = "tot",
      "miscellaneous" = "misc",
      janitor::make_clean_names(nm))
  }

  tryCatch(
    expr = {
      res <- .retry_request(url)
      check_status(res)
      raw <- res %>% .resp_text() %>% jsonlite::fromJSON(simplifyDataFrame = TRUE)

      cats <- raw[["categories"]] %||% raw[["statCategories"]]
      if (is.null(cats) || !is.data.frame(cats) || nrow(cats) == 0) {
        return(result)
      }

      per_cat <- list()
      for (i in seq_len(nrow(cats))) {
        cat_name   <- as.character(cats[["name"]][[i]] %||% paste0("category", i))
        pfx        <- prefix_for(cat_name)
        stat_keys  <- cats[["names"]][[i]] %||% character(0)
        st         <- cats[["statistics"]][[i]]
        if (is.null(st) || !is.data.frame(st) || nrow(st) == 0) next

        # `stats` is a list-column: one length-K character vector per row,
        # positionally aligned with `stat_keys`.
        vals <- st[["stats"]]
        if (is.null(vals)) next
        mat <- do.call(rbind, lapply(vals, function(v) {
          length(v) <- length(stat_keys)
          suppressWarnings(as.numeric(v))
        }))
        if (is.null(mat) || ncol(mat) == 0) next
        colnames(mat) <- paste0(pfx, "_", janitor::make_clean_names(stat_keys))
        stat_df <- dplyr::as_tibble(as.data.frame(mat, stringsAsFactors = FALSE))

        # Flatten the row-level meta (season is a nested {year, displayName}).
        season_col <- st[["season"]]
        season_yr  <- if (is.data.frame(season_col)) season_col[["year"]]
                      else suppressWarnings(as.integer(season_col))
        meta <- dplyr::tibble(
          season       = as.integer(season_yr %||% NA_integer_),
          team_id      = as.character(st[["teamId"]] %||% st[["team_id"]] %||% NA),
          team_slug    = as.character(st[["teamSlug"]] %||% st[["team_slug"]] %||% NA)
        )
        per_cat[[length(per_cat) + 1L]] <- dplyr::bind_cols(meta, stat_df)
      }

      if (length(per_cat) == 0L) return(result)

      # Merge categories on the shared (season, team) grain.
      join_keys <- c("season", "team_id", "team_slug")
      wide <- Reduce(function(a, b) {
        dplyr::full_join(a, b, by = intersect(join_keys, intersect(names(a), names(b))))
      }, per_cat)

      result <- wide %>%
        dplyr::mutate(athlete_id = as.character(athlete_id)) %>%
        dplyr::relocate(dplyr::any_of(c("athlete_id", "season", "team_id", "team_slug"))) %>%
        dplyr::arrange(dplyr::across(dplyr::any_of("season"))) %>%
        make_wehoop_data(desc, Sys.time())
    },
    error = function(e) .report_api_error(
      e,
      hint = paste0("Failed to retrieve ESPN ", league,
                    " athlete stats for athlete_id=", athlete_id,
                    ", season=", season),
      args = .args
    ),
    warning = function(w) .report_api_warning(
      w,
      hint = paste0("Warning retrieving ESPN ", league,
                    " athlete stats for athlete_id=", athlete_id),
      args = .args
    ),
    finally = {}
  )
  return(result)
}

# ---------------------------------------------------------------------------
# .espn_basketball_athlete_gamelog  (2.4)
# web-common-v3 /athletes/{athlete_id}/gamelog?season={year}
# Returns single tibble (one row per game)
# ---------------------------------------------------------------------------

#' Internal: ESPN basketball athlete gamelog
#'
#' Fetches
#' `site.web.api.espn.com/apis/common/v3/sports/basketball/{league}/athletes/{athlete_id}/gamelog`
#' and returns a single tidy tibble (one row per game).
#'
#' @param league character.
#' @param athlete_id character or numeric.
#' @param season numeric.
#' @param ... Unused.
#' @return A tibble.
#' @noRd
.espn_basketball_athlete_gamelog <- function(league, athlete_id, season, ...) {
  .espn_bball_validate_league(league)
  .args <- list(league = league, athlete_id = athlete_id, season = season)

  result <- data.frame()

  url <- paste0(
    "https://site.web.api.espn.com/apis/common/v3/sports/basketball/",
    league,
    "/athletes/",
    athlete_id,
    "/gamelog?season=",
    season
  )

  tryCatch(
    expr = {
      res <- .retry_request(url)
      check_status(res)
      raw <- res %>% .resp_text() %>% jsonlite::fromJSON(simplifyDataFrame = TRUE)

      # The gamelog payload nests stats under events/categories.
      # Attempt to parse the flat events table first; fall back to
      # stats[[]] if the response shape differs.
      events_raw <- raw[["events"]] %||% raw[["games"]] %||% raw[["gameLog"]]
      labels_raw <- raw[["labels"]] %||% raw[["statNames"]] %||% raw[["names"]]

      if (is.null(events_raw) ||
          (!is.data.frame(events_raw) && !is.list(events_raw))) {
        result <- data.frame(stringsAsFactors = FALSE) %>% dplyr::as_tibble()
      } else {
        if (is.data.frame(events_raw) && nrow(events_raw) > 0) {
          result <- events_raw %>%
            data.frame(stringsAsFactors = FALSE) %>%
            dplyr::as_tibble() %>%
            janitor::clean_names() %>%
            make_wehoop_data(
              paste0("ESPN ", toupper(league), " Athlete Gamelog from ESPN.com"),
              Sys.time()
            )
        } else if (is.list(events_raw) && length(events_raw) > 0) {
          # Each element is keyed by event id; the value is a list with
          # per-game metadata (gameDate, opponent, score, atVs, gameResult,
          # eventNote, leagueName, team, week, links, etc.). Stats themselves
          # live in raw$seasonTypes and are merged separately if present.
          rows <- lapply(seq_along(events_raw), function(i) {
            ev <- events_raw[[i]]
            if (!is.list(ev)) return(NULL)

            event_id <- as.character(
              ev[["eventId"]] %||% ev[["id"]] %||%
                names(events_raw)[i] %||% NA_character_
            )

            # Capture scalar (length-1, non-list) fields verbatim.
            scalars <- Filter(function(v) length(v) == 1 && !is.list(v), ev)
            row_df <- if (length(scalars) > 0) {
              as.data.frame(lapply(scalars, function(v) as.character(v[[1]])),
                            stringsAsFactors = FALSE)
            } else {
              data.frame(stringsAsFactors = FALSE)
            }

            # Pull common nested 1-row objects (team, opponent) onto the row.
            for (nm in c("team", "opponent")) {
              sub <- ev[[nm]]
              if (is.list(sub) && !is.data.frame(sub)) {
                sub_scalars <- Filter(function(v) length(v) == 1 && !is.list(v), sub)
                if (length(sub_scalars) > 0) {
                  add <- as.data.frame(
                    lapply(sub_scalars, function(v) as.character(v[[1]])),
                    stringsAsFactors = FALSE
                  )
                  names(add) <- paste0(nm, "_", names(add))
                  row_df <- if (nrow(row_df) > 0) cbind(row_df, add) else add
                }
              }
            }

            # Optionally append stats vector if it's bundled inline with labels.
            stats_v <- ev[["stats"]] %||% ev[["values"]]
            if (!is.null(stats_v) && !is.null(labels_raw) &&
                length(labels_raw) == length(stats_v)) {
              stat_cols <- as.list(as.character(stats_v))
              names(stat_cols) <- as.character(labels_raw)
              stat_df <- as.data.frame(stat_cols, stringsAsFactors = FALSE)
              row_df <- if (nrow(row_df) > 0) cbind(row_df, stat_df) else stat_df
            }

            if (nrow(row_df) == 0) {
              row_df <- data.frame(stringsAsFactors = FALSE)
            }
            row_df[["event_id"]] <- event_id
            row_df
          })
          rows <- Filter(Negate(is.null), rows)
          if (length(rows) > 0) {
            result <- dplyr::bind_rows(rows) %>%
              dplyr::as_tibble() %>%
              janitor::clean_names() %>%
              make_wehoop_data(
                paste0("ESPN ", toupper(league), " Athlete Gamelog from ESPN.com"),
                Sys.time()
              )
          } else {
            result <- data.frame(stringsAsFactors = FALSE) %>% dplyr::as_tibble()
          }
        } else {
          result <- data.frame(stringsAsFactors = FALSE) %>% dplyr::as_tibble()
        }
      }
    },
    error = function(e) .report_api_error(
      e,
      hint = paste0("Failed to retrieve ESPN ", league,
                    " athlete gamelog for athlete_id=", athlete_id,
                    ", season=", season),
      args = .args
    ),
    warning = function(w) .report_api_warning(
      w,
      hint = paste0("Warning retrieving ESPN ", league,
                    " athlete gamelog for athlete_id=", athlete_id),
      args = .args
    ),
    finally = {}
  )
  result <- .echo_identity(result, athlete_id = athlete_id, season = season)
  return(result)
}

# ---------------------------------------------------------------------------
# .espn_basketball_athlete_splits  (2.5)
# web-common-v3 /athletes/{athlete_id}/splits?season={year}
# Returns single tibble (long-format splits)
# ---------------------------------------------------------------------------

#' Internal: ESPN basketball athlete splits
#'
#' Fetches
#' `site.web.api.espn.com/apis/common/v3/sports/basketball/{league}/athletes/{athlete_id}/splits`
#' and returns a single long-format tibble.
#'
#' @param league character.
#' @param athlete_id character or numeric.
#' @param season numeric.
#' @param ... Unused.
#' @return A tibble.
#' @noRd
.espn_basketball_athlete_splits <- function(league, athlete_id, season, ...) {
  .espn_bball_validate_league(league)
  .args <- list(league = league, athlete_id = athlete_id, season = season)

  result <- data.frame()

  url <- paste0(
    "https://site.web.api.espn.com/apis/common/v3/sports/basketball/",
    league,
    "/athletes/",
    athlete_id,
    "/splits?season=",
    season
  )

  tryCatch(
    expr = {
      res <- .retry_request(url)
      check_status(res)
      raw <- res %>% .resp_text() %>% jsonlite::fromJSON(simplifyDataFrame = TRUE)

      splits_raw <- raw[["splitCategories"]] %||% raw[["splits"]] %||%
                      raw[["categories"]] %||% raw[["data"]]
      labels_raw <- raw[["labels"]] %||% raw[["names"]] %||% raw[["statNames"]]

      if (is.null(splits_raw)) {
        result <- data.frame(stringsAsFactors = FALSE) %>% dplyr::as_tibble()
      } else if (is.data.frame(splits_raw) && nrow(splits_raw) > 0) {
        result <- splits_raw %>%
          data.frame(stringsAsFactors = FALSE) %>%
          dplyr::as_tibble() %>%
          janitor::clean_names() %>%
          make_wehoop_data(
            paste0("ESPN ", toupper(league), " Athlete Splits from ESPN.com"),
            Sys.time()
          )
      } else if (is.list(splits_raw) && length(splits_raw) > 0) {
        # Flatten each split category into long rows
        rows <- lapply(splits_raw, function(cat_item) {
          cat_name   <- as.character(cat_item[["displayName"]] %||%
                                      cat_item[["name"]] %||% NA_character_)
          splits_lst <- cat_item[["splits"]] %||% cat_item[["rows"]]

          if (is.null(splits_lst) || length(splits_lst) == 0) {
            return(data.frame(category = cat_name, stringsAsFactors = FALSE))
          }

          if (is.data.frame(splits_lst)) {
            cbind(
              data.frame(category = rep(cat_name, nrow(splits_lst)),
                         stringsAsFactors = FALSE),
              splits_lst
            )
          } else if (is.list(splits_lst)) {
            inner_rows <- lapply(splits_lst, function(sp) {
              split_name <- as.character(sp[["displayName"]] %||%
                                          sp[["name"]] %||% NA_character_)
              stats_v    <- sp[["stats"]] %||% sp[["values"]]
              if (!is.null(stats_v) && !is.null(labels_raw) &&
                  length(labels_raw) == length(stats_v)) {
                row_vals <- as.list(as.character(stats_v))
                names(row_vals) <- as.character(labels_raw)
                row_vals[["category"]]   <- cat_name
                row_vals[["split_name"]] <- split_name
                data.frame(row_vals, stringsAsFactors = FALSE)
              } else {
                data.frame(category   = cat_name,
                           split_name = split_name,
                           stringsAsFactors = FALSE)
              }
            })
            dplyr::bind_rows(inner_rows)
          } else {
            data.frame(category = cat_name, stringsAsFactors = FALSE)
          }
        })
        rows <- Filter(Negate(is.null), rows)
        if (length(rows) > 0) {
          result <- dplyr::bind_rows(rows) %>%
            dplyr::as_tibble() %>%
            janitor::clean_names() %>%
            make_wehoop_data(
              paste0("ESPN ", toupper(league), " Athlete Splits from ESPN.com"),
              Sys.time()
            )
        } else {
          result <- data.frame(stringsAsFactors = FALSE) %>% dplyr::as_tibble()
        }
      } else {
        result <- data.frame(stringsAsFactors = FALSE) %>% dplyr::as_tibble()
      }
    },
    error = function(e) .report_api_error(
      e,
      hint = paste0("Failed to retrieve ESPN ", league,
                    " athlete splits for athlete_id=", athlete_id,
                    ", season=", season),
      args = .args
    ),
    warning = function(w) .report_api_warning(
      w,
      hint = paste0("Warning retrieving ESPN ", league,
                    " athlete splits for athlete_id=", athlete_id),
      args = .args
    ),
    finally = {}
  )
  result <- .echo_identity(result, athlete_id = athlete_id, season = season)
  return(result)
}

# ---------------------------------------------------------------------------
# .espn_basketball_athlete_eventlog  (2.6)
# core-v2 /athletes/{athlete_id}/eventlog?season={year}
# Returns single tibble; $ref URLs returned as character columns, not resolved
# ---------------------------------------------------------------------------

#' Internal: ESPN basketball athlete eventlog
#'
#' Fetches
#' `sports.core.api.espn.com/v2/sports/basketball/leagues/{league}/athletes/{athlete_id}/eventlog`
#' and returns a single tidy tibble. Per-event `statistics.$ref` URLs are
#' returned as a character column `statistics_ref` -- they are NOT resolved.
#'
#' @param league character.
#' @param athlete_id character or numeric.
#' @param season numeric.
#' @param ... Unused.
#' @return A tibble.
#' @noRd
.espn_basketball_athlete_eventlog <- function(league, athlete_id, season, ...) {
  .espn_bball_validate_league(league)
  .args <- list(league = league, athlete_id = athlete_id, season = season)

  result <- data.frame()

  # Eventlog is scoped under /seasons/{year}/athletes/{id}/eventlog;
  # the flat /athletes/{id}/eventlog?season= form 404s.
  url <- paste0(
    "https://sports.core.api.espn.com/v2/sports/basketball/leagues/",
    league,
    "/seasons/", as.integer(season),
    "/athletes/", athlete_id,
    "/eventlog"
  )

  tryCatch(
    expr = {
      res <- .retry_request(url)
      check_status(res)
      raw <- res %>% .resp_text() %>% jsonlite::fromJSON(simplifyDataFrame = TRUE)

      # core-v2 eventlog returns:
      #   raw$events = {count, pageIndex, pageSize, pageCount, items[]}
      # where items is a data frame with cols
      #   {event, competition, statistics, teamId, played}.
      # The `event/competition/statistics` cols may be character $ref vectors
      # (collapsed by simplifyDataFrame) or 1-col data frames named "$ref".
      events_obj <- raw[["events"]]
      events_raw <- if (is.list(events_obj) && !is.data.frame(events_obj))
                      events_obj[["items"]] %||% events_obj
                    else events_obj
      events_raw <- events_raw %||% raw[["items"]]

      if (is.null(events_raw) ||
          (!is.data.frame(events_raw) && !is.list(events_raw)) ||
          length(events_raw) == 0) {
        result <- data.frame(stringsAsFactors = FALSE) %>% dplyr::as_tibble()
      } else {
        if (is.data.frame(events_raw) && nrow(events_raw) > 0) {
          # Flatten $ref columns from nested data frames or scalar URL columns
          ev <- events_raw

          extract_ref <- function(col_name) {
            x <- ev[[col_name]]
            if (is.null(x)) return(rep(NA_character_, nrow(ev)))
            if (is.character(x)) return(x)
            if (is.data.frame(x) && "$ref" %in% colnames(x)) {
              return(as.character(x[["$ref"]]))
            }
            if (is.list(x)) {
              return(vapply(x, function(item) {
                if (is.list(item) && !is.null(item[["$ref"]])) {
                  as.character(item[["$ref"]])
                } else {
                  NA_character_
                }
              }, character(1)))
            }
            rep(NA_character_, nrow(ev))
          }

          flat_df <- data.frame(
            event_ref       = extract_ref("event"),
            competition_ref = extract_ref("competition"),
            team_ref        = extract_ref("team"),
            statistics_ref  = extract_ref("statistics"),
            stringsAsFactors = FALSE
          )

          # Append any plain scalar columns not already in flat_df
          scalar_cols <- setdiff(
            names(ev)[vapply(ev, function(x) {
              !is.data.frame(x) && !is.list(x)
            }, logical(1))],
            names(flat_df)
          )
          if (length(scalar_cols) > 0) {
            flat_df <- cbind(
              flat_df,
              ev[scalar_cols],
              stringsAsFactors = FALSE
            )
          }

          result <- flat_df %>%
            dplyr::as_tibble() %>%
            janitor::clean_names() %>%
            make_wehoop_data(
              paste0("ESPN ", toupper(league), " Athlete Eventlog from ESPN.com"),
              Sys.time()
            )
        } else {
          # list form
          rows <- lapply(events_raw, function(ev_item) {
            ev_ref   <- ev_item[["event"]][["$ref"]] %||%
                        ev_item[["event"]]           %||% NA_character_
            comp_ref <- ev_item[["competition"]][["$ref"]] %||%
                        ev_item[["competition"]]      %||% NA_character_
            team_ref <- ev_item[["team"]][["$ref"]] %||%
                        ev_item[["team"]]            %||% NA_character_
            stats_ref <- ev_item[["statistics"]][["$ref"]] %||%
                         ev_item[["statistics"]]      %||% NA_character_
            data.frame(
              event_ref       = as.character(ev_ref),
              competition_ref = as.character(comp_ref),
              team_ref        = as.character(team_ref),
              statistics_ref  = as.character(stats_ref),
              stringsAsFactors = FALSE
            )
          })
          result <- dplyr::bind_rows(rows) %>%
            dplyr::as_tibble() %>%
            janitor::clean_names() %>%
            make_wehoop_data(
              paste0("ESPN ", toupper(league), " Athlete Eventlog from ESPN.com"),
              Sys.time()
            )
        }
      }
    },
    error = function(e) .report_api_error(
      e,
      hint = paste0("Failed to retrieve ESPN ", league,
                    " athlete eventlog for athlete_id=", athlete_id,
                    ", season=", season),
      args = .args
    ),
    warning = function(w) .report_api_warning(
      w,
      hint = paste0("Warning retrieving ESPN ", league,
                    " athlete eventlog for athlete_id=", athlete_id),
      args = .args
    ),
    finally = {}
  )
  result <- .echo_identity(result, athlete_id = athlete_id, season = season)
  return(result)
}

# ---------------------------------------------------------------------------
# .espn_basketball_athlete_awards  (2.7)
# core-v2 /athletes/{athlete_id}/awards
# Returns single tibble; often empty
# ---------------------------------------------------------------------------

#' Internal: ESPN basketball athlete awards
#'
#' Fetches
#' `sports.core.api.espn.com/v2/sports/basketball/leagues/{league}/athletes/{athlete_id}/awards`
#' and returns a single tidy tibble. This endpoint is sparse; many athletes
#' return no data, in which case an empty tibble with canonical columns is
#' returned.
#'
#' @param league character.
#' @param athlete_id character or numeric.
#' @param ... Unused.
#' @return A tibble with columns `season`, `award_id`, `name`, `description`,
#'   `date`, `type`.
#' @noRd
.espn_basketball_athlete_awards <- function(league, athlete_id, ...) {
  .espn_bball_validate_league(league)
  .args <- list(league = league, athlete_id = athlete_id)

  empty_awards <- data.frame(
    season      = character(0),
    award_id    = character(0),
    name        = character(0),
    description = character(0),
    date        = character(0),
    type        = character(0),
    stringsAsFactors = FALSE
  ) %>%
    dplyr::as_tibble() %>%
    make_wehoop_data(
      paste0("ESPN ", toupper(league), " Athlete Awards from ESPN.com"),
      Sys.time()
    )

  result <- empty_awards

  url <- paste0(
    "https://sports.core.api.espn.com/v2/sports/basketball/leagues/",
    league,
    "/athletes/",
    athlete_id,
    "/awards"
  )

  tryCatch(
    expr = {
      res <- .retry_request(url)
      check_status(res)
      raw <- res %>% .resp_text() %>% jsonlite::fromJSON(simplifyDataFrame = TRUE)

      items_raw <- raw[["items"]] %||% raw[["awards"]]

      if (is.null(items_raw) ||
          (!is.data.frame(items_raw) && !is.list(items_raw)) ||
          length(items_raw) == 0) {
        return(result)  # empty tibble
      }

      if (is.data.frame(items_raw) && nrow(items_raw) > 0) {
        keep_cols <- c("season", "award_id", "id", "name", "displayName",
                       "description", "date", "type", "$ref")
        avail <- intersect(keep_cols, colnames(items_raw))
        award_df <- items_raw[avail] %>%
          data.frame(stringsAsFactors = FALSE, check.names = FALSE)

        # Normalize to canonical column names
        if ("id" %in% colnames(award_df) && !"award_id" %in% colnames(award_df)) {
          award_df[["award_id"]] <- award_df[["id"]]
        }
        if ("displayName" %in% colnames(award_df) && !"name" %in% colnames(award_df)) {
          award_df[["name"]] <- award_df[["displayName"]]
        }
        # Surface $ref URLs (core-v2 often returns ref-only payloads) without
        # auto-resolving them. NA-fill canonical columns so the output schema
        # is stable.
        n <- nrow(award_df)
        ref_url <- if ("$ref" %in% colnames(items_raw)) {
          as.character(items_raw[["$ref"]])
        } else {
          rep(NA_character_, n)
        }
        canonical <- data.frame(
          season      = if ("season" %in% colnames(award_df)) as.character(award_df[["season"]]) else rep(NA_character_, n),
          award_id    = if ("award_id" %in% colnames(award_df)) as.character(award_df[["award_id"]]) else rep(NA_character_, n),
          name        = if ("name" %in% colnames(award_df)) as.character(award_df[["name"]]) else rep(NA_character_, n),
          description = if ("description" %in% colnames(award_df)) as.character(award_df[["description"]]) else rep(NA_character_, n),
          date        = if ("date" %in% colnames(award_df)) as.character(award_df[["date"]]) else rep(NA_character_, n),
          type        = if ("type" %in% colnames(award_df)) as.character(award_df[["type"]]) else rep(NA_character_, n),
          ref_url     = ref_url,
          stringsAsFactors = FALSE
        )

        result <- canonical %>%
          dplyr::as_tibble() %>%
          janitor::clean_names() %>%
          make_wehoop_data(
            paste0("ESPN ", toupper(league), " Athlete Awards from ESPN.com"),
            Sys.time()
          )
      }
    },
    error = function(e) .report_api_error(
      e,
      hint = paste0("Failed to retrieve ESPN ", league,
                    " athlete awards for athlete_id=", athlete_id),
      args = .args
    ),
    warning = function(w) .report_api_warning(
      w,
      hint = paste0("Warning retrieving ESPN ", league,
                    " athlete awards for athlete_id=", athlete_id),
      args = .args
    ),
    finally = {}
  )
  return(result)
}

# ---------------------------------------------------------------------------
# .espn_basketball_athlete_statisticslog  (2.8)
# core-v2 /athletes/{athlete_id}/statisticslog?season={year}
# Returns single tibble
# ---------------------------------------------------------------------------

#' Internal: ESPN basketball athlete statisticslog
#'
#' Fetches
#' `sports.core.api.espn.com/v2/sports/basketball/leagues/{league}/athletes/{athlete_id}/statisticslog`
#' and returns a single tidy tibble.
#'
#' @param league character.
#' @param athlete_id character or numeric.
#' @param season numeric.
#' @param ... Unused.
#' @return A tibble.
#' @noRd
.espn_basketball_athlete_statisticslog <- function(league, athlete_id, season, ...) {
  .espn_bball_validate_league(league)
  .args <- list(league = league, athlete_id = athlete_id, season = season)

  result <- data.frame()

  # ESPN's core-v2 statisticslog endpoint does NOT accept ?season=YYYY
  # (returns 404). It returns the full chronological log; the season arg
  # is retained on the public wrapper signature for API symmetry but only
  # used to filter the resulting frame downstream.
  invisible(season)
  url <- paste0(
    "https://sports.core.api.espn.com/v2/sports/basketball/leagues/",
    league,
    "/athletes/",
    athlete_id,
    "/statisticslog"
  )

  tryCatch(
    expr = {
      res <- .retry_request(url)
      check_status(res)
      raw <- res %>% .resp_text() %>% jsonlite::fromJSON(simplifyDataFrame = TRUE)

      entries_raw <- raw[["entries"]] %||% raw[["items"]] %||% raw[["statistics"]]

      if (is.null(entries_raw) ||
          (!is.data.frame(entries_raw) && !is.list(entries_raw)) ||
          length(entries_raw) == 0) {
        result <- data.frame(stringsAsFactors = FALSE) %>% dplyr::as_tibble()
      } else if (is.data.frame(entries_raw) && nrow(entries_raw) > 0) {
        result <- entries_raw %>%
          data.frame(stringsAsFactors = FALSE) %>%
          dplyr::as_tibble() %>%
          janitor::clean_names() %>%
          make_wehoop_data(
            paste0("ESPN ", toupper(league), " Athlete Statisticslog from ESPN.com"),
            Sys.time()
          )
      } else if (is.list(entries_raw) && length(entries_raw) > 0) {
        rows <- lapply(entries_raw, function(entry) {
          event_ref <- NA_character_
          stats_ref <- NA_character_

          if (!is.null(entry[["event"]]) && is.list(entry[["event"]])) {
            event_ref <- as.character(entry[["event"]][["$ref"]] %||% NA_character_)
          }
          if (!is.null(entry[["statistics"]]) && is.list(entry[["statistics"]])) {
            stats_ref <- as.character(entry[["statistics"]][["$ref"]] %||% NA_character_)
          }

          # Collect scalar fields
          scalars <- Filter(function(x) !is.list(x) && !is.data.frame(x), entry)
          row_df <- data.frame(
            event_ref      = event_ref,
            statistics_ref = stats_ref,
            stringsAsFactors = FALSE
          )
          if (length(scalars) > 0) {
            sc_df <- data.frame(
              lapply(scalars, function(v) as.character(v[[1]] %||% NA_character_)),
              stringsAsFactors = FALSE
            )
            names(sc_df) <- names(scalars)
            row_df <- cbind(row_df, sc_df)
          }
          row_df
        })
        result <- dplyr::bind_rows(rows) %>%
          dplyr::as_tibble() %>%
          janitor::clean_names() %>%
          make_wehoop_data(
            paste0("ESPN ", toupper(league), " Athlete Statisticslog from ESPN.com"),
            Sys.time()
          )
      } else {
        result <- data.frame(stringsAsFactors = FALSE) %>% dplyr::as_tibble()
      }
    },
    error = function(e) .report_api_error(
      e,
      hint = paste0("Failed to retrieve ESPN ", league,
                    " athlete statisticslog for athlete_id=", athlete_id,
                    ", season=", season),
      args = .args
    ),
    warning = function(w) .report_api_warning(
      w,
      hint = paste0("Warning retrieving ESPN ", league,
                    " athlete statisticslog for athlete_id=", athlete_id),
      args = .args
    ),
    finally = {}
  )
  result <- .echo_identity(result, athlete_id = athlete_id, season = season)
  return(result)
}

Try the wehoop package in your browser

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

wehoop documentation built on Aug. 25, 2026, 1:06 a.m.