Nothing
key_lookup <- tibble::tibble(
key = 0:11,
key_name = c("C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B")
)
# spotify ----------------------------------------------------------------------
#' Spotify track information
#'
#' Access the Spotify API to get metadata for audio tracks.
#'
#' @param track_id The Spotify ID for a track.
#' @param api_key A Spotify access token, from
#' [get_spotify_access_token()].
#'
#' @returns A [tibble][tibble::tibble-package] with track metadata, including:
#' * `album_name`: The name of the album the track appears on (if relevant).
#' * `track_name`: The name of the track.
#' * `artist`: The artist of the track.
#' * `featuring`: The artist(s) featured on the track (if relevant).
#' * `duration_ms`: Duration of the track in milliseconds.
#' * `explicit`: Logical. Does the track contain explicit lyrics (`TRUE`) or
#' not (`FALSE`).
#' @export
#' @family API access
#'
#' @examplesIf taylor_examples()
#' # So High School
#' get_spotify_track_info(track_id = "7Mts0OfPorF4iwOomvfqn1")
get_spotify_track_info <- function(
track_id,
api_key = get_spotify_access_token()
) {
check_character(track_id, allow_na = TRUE)
check_character(api_key$client_id)
check_character(api_key$client_secret)
api_key <- spotifyr::get_spotify_access_token(
client_id = api_key$client_id,
client_secret = api_key$client_secret
)
if (is.na(track_id) || track_id == "") {
return(NULL)
}
spotify_track <- spotifyr::get_track(track_id, authorization = api_key)
tibble::tibble(
spotify_album = spotify_track$album$name,
duration_ms = spotify_track$duration_ms,
explicit = spotify_track$explicit
) |>
tibble::add_column(
artist = paste(spotify_track$artists$name, collapse = ", "),
.before = 1
) |>
tidyr::separate_wider_delim(
cols = "artist",
delim = ", ",
names = c("artist", "featuring"),
too_few = "align_start",
too_many = "merge"
)
}
#' Spotify API helpers
#'
#' Set and retrieve Spotify client information.
#'
#' @param id A Spotify Client ID.
#' @param secret A Spotify Client Secret.
#'
#' @returns
#' * `get_spotify_api_key()` returns a previously stored Client ID and Secret.
#' * `set_spotify_api_key()` is called for side effects only.
#' @name spotify-api
#' @export
#'
#' @examplesIf taylor_examples()
#' \donttest{get_spotify_access_token()}
get_spotify_access_token <- function() {
client_id <- Sys.getenv("SPOTIFY_CLIENT_ID")
client_secret <- Sys.getenv("SPOTIFY_CLIENT_SECRET")
if (all(nzchar(client_id), nzchar(client_secret))) {
token <- list(
client_id = client_id,
client_secret = client_secret
)
return(token)
}
if (is_testing() || is_pkgdown()) {
return(spotify_testing_key()) # nolint: return_linter
} else {
cli::cli_abort(
cli::format_message(
c(
"Client ID or Secret not found, please see",
"{.fun spotifyr::get_spotify_access_token}"
)
)
)
}
}
#' @export
#' @rdname spotify-api
set_spotify_api_key <- function(id = NULL, secret = NULL) {
check_character(id, allow_null = TRUE)
check_character(secret, allow_null = TRUE)
if (is.null(id)) {
id <- askpass::askpass("Please enter your Client ID")
if (is.null(id)) {
cli::cli_abort("Client ID not provided")
}
}
Sys.setenv("SPOTIFY_CLIENT_ID" = id)
if (is.null(secret)) {
secret <- askpass::askpass("Please enter your Client Secret")
if (is.null(secret)) {
cli::cli_abort("Client Secret not provided")
}
}
Sys.setenv("SPOTIFY_CLIENT_SECRET" = secret)
}
spotify_testing_key <- function() {
list(
client_id = httr2::secret_decrypt(
"UOF5NVolAFuZUfPsrqB6zRGiuT2U6kZTly16hmop_vkzywAmTyHJaDuWl13gymsI",
"TAYLOR_KEY"
),
client_secret = httr2::secret_decrypt(
"LZneUpdwTawqZOBb6Qx481OvOL9U9Jxz9QZhm9FwUQ6QsPLkQTV1FbMweVKFKUR9",
"TAYLOR_KEY"
)
)
}
# soundstat --------------------------------------------------------------------
#' SoundStat audio features
#'
#' Access the SoundStat API to get audio features for tracks.
#'
#' @param track_id The Spotify ID for a track.
#' @param convert_values Logical. For SoundStat features, should audio features
#' be converted to the Spotify scale. See details for conversion formulas.
#' @param api_key A SoundStat API key, e.g., [get_soundstat_api_key()].
#'
#' @details
#' Due to differences in algorithms and methodologies, the SoundStat audio
#' features are on a slightly different scale than the audio features that were
#' originally included in [taylor] prior to the [changes to the Spotify
#' API](https://developer.spotify.com/blog/2024-11-27-changes-to-the-web-api).
#' We can convert the SoundStat values to the Spotify scale using the formulas
#' in the [SoundStat
#' docs](https://soundstat.info/article/Understanding-Audio-Analysis.html):
#'
#' ```
#' acousticness = sound_value * 0.005
#' energy = sound_value * 2.25
#' instrumentalness = sound_value * 0.03
#' loudness = -(1 - sound_value) * 14
#' ```
#'
#' To automatically perform these conversions, set `convert_values = TRUE`.
#'
#' @returns A [tibble][tibble::tibble-package] with track audio features,
#' including:
#' * `danceability`: Danceability score (0-1).
#' * `energy`: Energy level (0-1).
#' * `loudness`: Loudness level (0-1).
#' * `acousticness`: Acousticness score (0-1).
#' * `instrumentalness`: Instrumentalness score (0-1).
#' * `valence`: Mood/positiveness (0-1).
#' * `tempo`: Track tempo in beats per minute (BPM).
#' * `key`: Track key (0-11).
#' * `mode`: Mode (0 - minor, 1 - major).
#' * `key_name`: Corresponds directly to the key, but the integer is
#' converted to the key name using Pitch Class notation (e.g., 0 becomes
#' `C`).
#' * `mode_name`: Corresponds directly to the mode, but the integer is
#' converted to the mode name (e.g., 0 becomes `minor`).
#' * `key_mode`: A combination of the `key_name` and `mode_name` variables
#' (e.g., `C minor`).
#' @export
#' @family API access
#'
#' @examplesIf taylor_examples()
#' get_soundstat_audio_features(track_id = "7Mts0OfPorF4iwOomvfqn1")
get_soundstat_audio_features <- function(
track_id,
convert_values = FALSE,
api_key = get_soundstat_api_key()
) {
check_character(track_id, allow_na = TRUE)
check_logical(convert_values)
check_character(api_key)
if (is.na(track_id) || track_id == "") {
return(NULL)
}
api_call <- httr2::request("https://soundstat.info/api/v1") |>
httr2::req_url_path_append("/track") |>
httr2::req_url_path_append(track_id) |>
httr2::req_headers(`x-api-key` = api_key) |>
httr2::req_retry(max_tries = 10)
resp <- httr2::req_perform(api_call)
if (httr2::resp_status(resp) == 202) {
# nocov start
# nolint start: object_usage_linter
monitor <- api_call |>
httr2::req_url_path_append("status") |>
httr2::req_perform()
# nolint end
resp <- httr2::req_perform(api_call)
# nocov end
}
raw_dat <- httr2::resp_body_json(resp)
raw_dat$features$segments <- NULL
raw_dat$features$beats <- NULL
res <- tibble::as_tibble(raw_dat$features) |>
dplyr::select(
"danceability",
"energy",
"loudness",
"acousticness",
"instrumentalness",
"valence",
"tempo",
"key",
"mode"
) |>
dplyr::left_join(key_lookup, by = "key") |>
dplyr::mutate(
mode_name = dplyr::case_when(
.data$mode == 0L ~ "minor",
.data$mode == 1L ~ "major"
),
key_mode = paste(.data$key_name, .data$mode_name)
)
if (convert_values) {
res <- res |>
dplyr::mutate(
acousticness = .data$acousticness * 0.005,
energy = .data$energy * 2.25,
instrumentalness = .data$instrumentalness * 0.03,
loudness = -1 * (1 - .data$loudness) * 14
)
}
res
}
#' SoundStat API helpers
#'
#' Set and retrieve a SoundStat API key
#'
#' @param key A SoundStat API key.
#'
#' @returns
#' * `get_soundstat_api_key()` returns a previously stored API key.
#' * `set_soundstat_api_key()` is called for side effects only.
#' @name soundstat-api
#' @export
#'
#' @examplesIf taylor_examples()
#' \donttest{get_soundstat_api_key()}
get_soundstat_api_key <- function() {
key <- Sys.getenv("SOUNDSTAT_KEY")
if (!identical(key, "")) {
return(key)
}
if (is_testing()) {
return(soundstat_testing_key()) # nolint: return_linter
} else {
cli::cli_abort(
cli::format_message(
c(
"No API key found, please supply with {.arg api_key} argument or",
"with the",
"{.help [{.envvar SOUNDSTAT_KEY} env var](get_soundstat_api_key)}"
)
)
)
}
}
#' @export
#' @rdname soundstat-api
set_soundstat_api_key <- function(key = NULL) {
check_character(key, allow_null = TRUE)
if (is.null(key)) {
key <- askpass::askpass("Please enter your API key")
if (is.null(key)) {
cli::cli_abort("API key not provided")
}
}
Sys.setenv("SOUNDSTAT_KEY" = key)
}
soundstat_testing_key <- function() {
httr2::secret_decrypt(
paste0(
"cFg1OO1frsH8Up0AhTQu09k86iUHZmK-rtok8wcVJMCfChKc6Oyc5GRqhVQJ_",
"s34RFw8qdhKJZY0aco"
),
"TAYLOR_KEY"
)
}
# reccobeats -------------------------------------------------------------------
#' Reccobeats audio features
#'
#' Access the Reccobeats API to get audio features for tracks.
#'
#' @param track_id The Spotify ID for a track.
#'
#' @returns
#' * `get_reccobeats_audio_features()` returns a
#' [tibble][tibble::tibble-package] with track audio features, including:
#' * `danceability`: Suitability for dancing (0.0 to 1.0). Higher values
#' indicate more rhythmically engaging tracks.
#' * `energy`: Intensity and liveliness (0.0 to 1.0). Higher values indicate
#' more energetic tracks.
#' * `loudness`: Average loudness in decibels (dB). Typically ranges between
#' -60 and 0 dB.
#' * `speechiness`: Presence of spoken words (0.0 to 1.0). Values above 0.66
#' indicate mostly speech.
#' * `acousticness`: Confidence (0.0 to 1.0) that the track is acoustic.
#' Higher values indicate more natural sounds.
#' * `instrumentalness`: Likelihood of no vocals (0.0 to 1.0). Values above
#' 0.5 suggest instrumental tracks.
#' * `liveness`: Probability of a live audience (0.0 to 1.0). Values above
#' 0.8 strongly suggest a live track.
#' * `valence`: Emotional tone (0.0 to 1.0). Higher values indicate a
#' happier mood, lower values a sadder one.
#' * `tempo`: Estimated tempo in beats per minute (BPM). Typically ranges
#' between 0 and 250.
#' * `key`: The key the track is in. Integers map to pitches using standard
#' Pitch Class notation. If no key was detected, the value is -1.
#' * `mode`: Mode indicates the modality (major or minor) of a track.
#' Major is represented by 1 and minor is 0.
#' * `key_name`: Corresponds directly to the key, but the integer is
#' converted to the key name using Pitch Class notation (e.g., 0 becomes
#' `C`).
#' * `mode_name`: Corresponds directly to the mode, but the integer is
#' converted to the mode name (e.g., 0 becomes `minor`).
#' * `key_mode`: A combination of the `key_name` and `mode_name` variables
#' (e.g., `C minor`).
#' @export
#' @family API access
#'
#' @examplesIf taylor_examples()
#' # So High School
#' get_reccobeats_audio_features(track_id = "7Mts0OfPorF4iwOomvfqn1")
get_reccobeats_audio_features <- function(track_id) {
check_character(track_id, allow_na = TRUE)
if (is.na(track_id) || track_id == "") {
return(NULL)
}
resp <- httr2::request("https://api.reccobeats.com/v1") |>
httr2::req_url_path_append("/audio-features") |>
httr2::req_url_query(ids = track_id) |>
httr2::req_retry(max_tries = 10) |>
httr2::req_perform() |>
httr2::resp_body_json()
if (identical(resp$content, list())) {
return(NULL)
}
resp$content[[1]] |>
tibble::as_tibble() |>
dplyr::select(
"danceability",
"energy",
"loudness",
"speechiness",
"acousticness",
"instrumentalness",
"liveness",
"valence",
"tempo",
"key",
"mode"
) |>
dplyr::left_join(key_lookup, by = "key") |>
dplyr::mutate(
mode_name = dplyr::case_when(
.data$mode == 0L ~ "minor",
.data$mode == 1L ~ "major"
),
key_mode = paste(.data$key_name, .data$mode_name)
)
}
# testing helpers --------------------------------------------------------------
#' Determine if code is executed interactively or in pkgdown
#'
#' Used for determining examples that shouldn't be run on CRAN, but can be run
#' for the pkgdown website.
#'
#' @return A logical value indicating whether or not the examples should be run.
#'
#' @export
#' @examples
#' taylor_examples()
taylor_examples <- function() {
httr2::secret_has_key("TAYLOR_KEY") && is_pkgdown()
}
is_testing <- function() {
identical(Sys.getenv("TESTTHAT"), "true")
}
is_pkgdown <- function() {
identical(Sys.getenv("IN_PKGDOWN"), "true")
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.