Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
Package: tinyspotifyr
Title: Tinyverse R Wrapper for the 'Spotify' Web API
Version: 0.2.3
Version: 0.2.3.2
Date: 2026-06-06
Authors@R: c(person("Troy", "Hernandez", email = "troy@cornball.ai", role = c("aut", "cre", "cph"), comment = c(ORCID = "0009-0005-4248-604X")), person("Charlie", "Thompson", email = "chuck@rcharlie.com", role = c("aut", "cph")), person("Josiah", "Parry", email = "josiah.parry@yahoo.com", role = "aut"), person("Donal", "Phipps", email = "donal.phipps@gmail.com", role = "aut"), person("Tom", "Wolff", email = "tom.wolff@duke.edu", role = "aut"))
Description: An R wrapper for the 'Spotify' Web API
<https://developer.spotify.com/web-api/>.
Depends: R (>= 3.2)
Depends: R (>= 4.0)
Imports:
httr,
jsonlite
tinyoauth
Remotes: cornball-ai/tinyoauth
License: MIT + file LICENSE
Encoding: UTF-8
LazyData: true
Expand Down
13 changes: 0 additions & 13 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -66,16 +66,3 @@ export(toggle_my_shuffle)
export(transfer_my_playback)
export(unfollow_playlist)
export(verify_result)

importFrom(httr,accept_json)
importFrom(httr,authenticate)
importFrom(httr,config)
importFrom(httr,content)
importFrom(httr,GET)
importFrom(httr,oauth_app)
importFrom(httr,oauth_endpoint)
importFrom(httr,oauth2.0_token)
importFrom(httr,RETRY)
importFrom(httr,stop_for_status)
importFrom(jsonlite,fromJSON)
importFrom(jsonlite,toJSON)
12 changes: 12 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# tinyspotifyr 0.2.3.2

* Fixed `get_shows_episodes()` sending `market` twice (in the URL and via the
query), producing a malformed query and a Spotify "Invalid market parameter"
400. Another migration leftover, like the get_artist()/get_album() fixes in
0.2.3.1.

# tinyspotifyr 0.2.3.1

* Replaced the httr backend with tinyoauth (curl + jsonlite + serverSocket); Imports trimmed to tinyoauth alone. Existing httr .httr-oauth caches carry over via oauth_import_httr() (no re-login).
* Fixed pre-existing get_artist() and get_album() bugs surfaced during the migration.

53 changes: 18 additions & 35 deletions R/albums.R
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
#' Returns a data frame of results containing album data. See the \href{https://developer.spotify.com/documentation/web-api}{official documentation} for more information.
#' @export

get_album <- function(id, market = NULL, authorization = get_spotify_access_token()) {

get_album <- function(id, market = NULL,
authorization = get_spotify_access_token()) {
base_url <- 'https://api.spotify.com/v1/albums'

if (!is.null(market)) {
Expand All @@ -18,18 +18,11 @@ get_album <- function(id, market = NULL, authorization = get_spotify_access_toke
}
}

params <- list(
market = market,
access_token = authorization
)
url <- paste0(base_url, "/", playlist_id, "/tracks?market=", market)
params <- list(market = market)

url <- paste0(base_url, "/", id)
res <- RETRY('GET', url, query = params, encode = 'json')
stop_for_status(res)

res <- jsonlite::fromJSON(content(res, as = 'text', encoding = 'UTF-8'),
flatten = TRUE)
res <- tinyoauth::oauth_request(authorization, url, "GET",
query = params, flatten = TRUE)

return(res)
}
Expand All @@ -45,8 +38,9 @@ get_album <- function(id, market = NULL, authorization = get_spotify_access_toke
#' Returns a data frame of results containing album data. See \url{https://developer.spotify.com/documentation/web-api} for more information.
#' @export

get_albums <- function(ids, market = NULL, authorization = get_spotify_access_token(), include_meta_info = FALSE) {

get_albums <- function(ids, market = NULL,
authorization = get_spotify_access_token(),
include_meta_info = FALSE) {
base_url <- 'https://api.spotify.com/v1/albums'

if (!is.null(market)) {
Expand All @@ -55,15 +49,9 @@ get_albums <- function(ids, market = NULL, authorization = get_spotify_access_to
}
}

params <- list(
ids = paste(ids, collapse = ','),
market = market,
access_token = authorization
)
res <- RETRY('GET', base_url, query = params, encode = 'json')
stop_for_status(res)

res <- fromJSON(content(res, as = 'text', encoding = 'UTF-8'), flatten = TRUE)
params <- list(ids = paste(ids, collapse = ','), market = market)
res <- tinyoauth::oauth_request(authorization, base_url, "GET",
query = params, flatten = TRUE)

if (!include_meta_info) {
res <- res$albums
Expand Down Expand Up @@ -95,8 +83,9 @@ get_albums <- function(ids, market = NULL, authorization = get_spotify_access_to
#' Returns a data frame of results containing album data. See the official API \href{https://developer.spotify.com/documentation/web-api}{documentation} for more information.
#' @export

get_album_tracks <- function(id, limit = 20, offset = 0, market = NULL, authorization = get_spotify_access_token(), include_meta_info = FALSE) {

get_album_tracks <- function(id, limit = 20, offset = 0, market = NULL,
authorization = get_spotify_access_token(),
include_meta_info = FALSE) {
base_url <- 'https://api.spotify.com/v1/albums'

if (!is.null(market)) {
Expand All @@ -105,21 +94,15 @@ get_album_tracks <- function(id, limit = 20, offset = 0, market = NULL, authoriz
}
}

params <- list(
market = market,
offset = offset,
limit = limit,
access_token = authorization
)
params <- list(market = market, offset = offset, limit = limit)
url <- paste0(base_url, "/", id, "/tracks")
res <- RETRY('GET', url, query = params, encode = 'json')
stop_for_status(res)

res <- fromJSON(content(res, as = 'text', encoding = 'UTF-8'), flatten = TRUE)
res <- tinyoauth::oauth_request(authorization, url, "GET",
query = params, flatten = TRUE)

if (!include_meta_info) {
res <- res$items
}

return(res)
}

81 changes: 30 additions & 51 deletions R/artists.R
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,12 @@
#' @export

get_artist <- function(id, authorization = get_spotify_access_token()) {

base_url <- 'https://api.spotify.com/v1/artists'

params <- list(
access_token = authorization
)
url <- paste0(base_url, "/", id=)
res <- GET(url, query = params, encode = 'json')
stop_for_status(res)

res <- fromJSON(content(res, as = 'text', encoding = 'UTF-8'), flatten = TRUE)
params <- list()
url <- paste0(base_url, "/", id)
res <- tinyoauth::oauth_request(authorization, url, "GET",
query = params, flatten = TRUE)

return(res)
}
Expand All @@ -31,18 +26,13 @@ get_artist <- function(id, authorization = get_spotify_access_token()) {
#' Returns a data frame of results containing artist data. See \url{https://developer.spotify.com/documentation/web-api} for more information.
#' @export

get_artists <- function(ids, authorization = get_spotify_access_token(), include_meta_info = FALSE) {

get_artists <- function(ids, authorization = get_spotify_access_token(),
include_meta_info = FALSE) {
base_url <- 'https://api.spotify.com/v1/artists'

params <- list(
ids = paste(ids, collapse = ','),
access_token = authorization
)
res <- GET(base_url, query = params, encode = 'json')
stop_for_status(res)

res <- fromJSON(content(res, as = 'text', encoding = 'UTF-8'), flatten = TRUE)
params <- list(ids = paste(ids, collapse = ','))
res <- tinyoauth::oauth_request(authorization, base_url, "GET",
query = params, flatten = TRUE)

if (!include_meta_info) {
res <- res$artists
Expand Down Expand Up @@ -79,8 +69,11 @@ get_artists <- function(ids, authorization = get_spotify_access_token(), include
#' Returns a data frame of results containing artist data. See \url{https://developer.spotify.com/documentation/web-api} for more information.
#' @export

get_artist_albums <- function(id, include_groups = c('album', 'single', 'appears_on', 'compilation'), market = NULL, limit = 20, offset = 0, authorization = get_spotify_access_token(), include_meta_info = FALSE) {

get_artist_albums <- function(id,
include_groups = c('album', 'single', 'appears_on', 'compilation'),
market = NULL, limit = 20, offset = 0,
authorization = get_spotify_access_token(),
include_meta_info = FALSE) {
base_url <- 'https://api.spotify.com/v1/artists'

if (!is.null(market)) {
Expand All @@ -89,18 +82,10 @@ get_artist_albums <- function(id, include_groups = c('album', 'single', 'appears
}
}

params <- list(
include_groups = paste(include_groups, collapse = ','),
market = market,
limit = limit,
offset = offset,
access_token = authorization
)
params <- list(include_groups = paste(include_groups, collapse = ','),
market = market, limit = limit, offset = offset)
url <- paste0(base_url, "/", id, "/albums")
res <- GET(url, query = params, encode = 'json')
stop_for_status(res)

res <- fromJSON(content(res, as = 'text', encoding = 'UTF-8'), flatten = TRUE)
res <- tinyoauth::oauth_request(authorization, url, "GET", query = params, flatten = TRUE)

if (!include_meta_info) {
res <- res$items
Expand All @@ -120,8 +105,9 @@ get_artist_albums <- function(id, include_groups = c('album', 'single', 'appears
#' Returns a data frame of results containing artist data. See \url{https://developer.spotify.com/documentation/web-api} for more information.
#' @export

get_artist_top_tracks <- function(id, market = 'US', authorization = get_spotify_access_token(), include_meta_info = FALSE) {

get_artist_top_tracks <- function(id, market = 'US',
authorization = get_spotify_access_token(),
include_meta_info = FALSE) {
base_url <- 'https://api.spotify.com/v1/artists'

if (!is.null(market)) {
Expand All @@ -130,15 +116,10 @@ get_artist_top_tracks <- function(id, market = 'US', authorization = get_spotify
}
}

params <- list(
market = market,
access_token = authorization
)
params <- list(market = market)
url <- paste0(base_url, "/", id, "/top-tracks")
res <- GET(url, query = params, encode = 'json')
stop_for_status(res)

res <- fromJSON(content(res, as = 'text', encoding = 'UTF-8'), flatten = TRUE)
res <- tinyoauth::oauth_request(authorization, url, "GET",
query = params, flatten = TRUE)

if (!include_meta_info) {
res <- res$tracks
Expand All @@ -156,22 +137,20 @@ get_artist_top_tracks <- function(id, market = 'US', authorization = get_spotify
#' Returns a data frame of results containing artist data. See \url{https://developer.spotify.com/documentation/web-api} for more information.
#' @export

get_related_artists <- function(id, authorization = get_spotify_access_token(), include_meta_info = FALSE) {

get_related_artists <- function(id,
authorization = get_spotify_access_token(),
include_meta_info = FALSE) {
base_url <- 'https://api.spotify.com/v1/artists'

params <- list(
access_token = authorization
)
params <- list()
url <- paste0(base_url, "/", id, "/related-artists")
res <- GET(url, query = params, encode = 'json')
stop_for_status(res)

res <- fromJSON(content(res, as = 'text', encoding = 'UTF-8'), flatten = TRUE)
res <- tinyoauth::oauth_request(authorization, url, "GET",
query = params, flatten = TRUE)

if (!include_meta_info) {
res <- res$artists
}

return(res)
}

83 changes: 48 additions & 35 deletions R/authenticate.R
Original file line number Diff line number Diff line change
@@ -1,54 +1,67 @@
# authenticate.R
# Spotify OAuth via tinyoauth. Client-credentials and user (authorization-code)
# tokens, with a back-compat path that carries over an existing httr .httr-oauth
# cache so prior authorizations keep working without a fresh login.

#' Spotify OAuth client
#' @param client_id Spotify client id (default env SPOTIFY_CLIENT_ID).
#' @param client_secret Spotify client secret (default env SPOTIFY_CLIENT_SECRET).
#' @return A tinyoauth client.
#' @keywords internal
.spotify_client <- function(client_id = Sys.getenv("SPOTIFY_CLIENT_ID"),
client_secret = Sys.getenv("SPOTIFY_CLIENT_SECRET")) {
tinyoauth::oauth_client(
id = client_id, secret = client_secret,
token_url = "https://accounts.spotify.com/api/token",
auth_url = "https://accounts.spotify.com/authorize")
}

#' Get Spotify Access Token
#'
#' This function creates a Spotify access token.
#' @param client_id Defaults to System Environment variable "SPOTIFY_CLIENT_ID"
#' @param client_secret Defaults to System Environment variable "SPOTIFY_CLIENT_SECRET"
#' Creates a Spotify access token via the client-credentials grant (app-only).
#' @param client_id Defaults to system environment variable "SPOTIFY_CLIENT_ID"
#' @param client_secret Defaults to system environment variable "SPOTIFY_CLIENT_SECRET"
#' @keywords auth
#' @return
#' Returns an environment with access token data.
#' @return An access token string.
#' @export
#' @examples
#' \dontrun{
#' get_spotify_access_token()
#' }

get_spotify_access_token <- function(client_id = Sys.getenv('SPOTIFY_CLIENT_ID'), client_secret = Sys.getenv('SPOTIFY_CLIENT_SECRET')) {

post0 <- RETRY('POST', 'https://accounts.spotify.com/api/token',
accept_json(), httr::authenticate(client_id, client_secret),
body = list(grant_type = 'client_credentials'),
encode = 'form', httr::config(http_version = 2))

post <- httr::content(post0)

if (!is.null(post$error)) {
stop(paste0("Could not authenticate with given Spotify credentials:\n\t",
post$error_description))
}

access_token <- post$access_token

return(access_token)
get_spotify_access_token <- function(client_id = Sys.getenv('SPOTIFY_CLIENT_ID'),
client_secret = Sys.getenv('SPOTIFY_CLIENT_SECRET')) {
tok <- tinyoauth::oauth_token_client(.spotify_client(client_id,
client_secret))
tok$access_token
}

#' Get Spotify authorization Code
#'
#' This function creates a Spotify access token.
#' @param client_id Defaults to System Envioronment variable "SPOTIFY_CLIENT_ID"
#' @param client_secret Defaults to System Envioronment variable "SPOTIFY_CLIENT_SECRET"
#' @param scope Space delimited string of spotify scopes, found here: https://developer.spotify.com/documentation/general/guides/scopes/. All scopes are selected by default
#' Obtains a user-authorized token via the authorization-code grant, with
#' caching and refresh. If a legacy httr \code{.httr-oauth} file is present, its
#' authorization is carried over (refreshed) so no new browser login is needed.
#' @param client_id Defaults to system environment variable "SPOTIFY_CLIENT_ID"
#' @param client_secret Defaults to system environment variable "SPOTIFY_CLIENT_SECRET"
#' @param scope Spotify scopes; all are requested by default.
#' @keywords auth
#' @return
#' Returns an environment with access token data.
#' @return A tinyoauth token (carries the access and refresh tokens).
#' @export
#' @examples
#' \dontrun{
#' get_spotify_authorization_code()
#' }

get_spotify_authorization_code <- function(client_id = Sys.getenv("SPOTIFY_CLIENT_ID"), client_secret = Sys.getenv("SPOTIFY_CLIENT_SECRET"), scope = tinyspotifyr::scopes) {
endpoint <- httr::oauth_endpoint(authorize = 'https://accounts.spotify.com/authorize',
access = 'https://accounts.spotify.com/api/token')
app <- httr::oauth_app('spotifyr', client_id, client_secret)
httr::oauth2.0_token(endpoint = endpoint, app = app, scope = scope)
get_spotify_authorization_code <- function(client_id = Sys.getenv("SPOTIFY_CLIENT_ID"),
client_secret = Sys.getenv("SPOTIFY_CLIENT_SECRET"),
scope = tinyspotifyr::scopes) {
client <- .spotify_client(client_id, client_secret)
legacy <- ".httr-oauth"
if (file.exists(legacy)) {
imp <- tryCatch(tinyoauth::oauth_import_httr(legacy),
error = function(e) NULL)
if (!is.null(imp)) {
return(tinyoauth::oauth_refresh(imp$client, imp$token))
}
}
tinyoauth::oauth_token(client, scope = paste(scope, collapse = " "))
}

Loading