diff --git a/DESCRIPTION b/DESCRIPTION index ba8b6d5e..df1ab17a 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -16,11 +16,13 @@ BugReports: http://github.com/jemus42/tRakt/issues Depends: R (>= 4.1) Imports: + cli, dplyr (>= 0.7.0), - httr, + httr2, jsonlite (>= 0.9.14), lubridate, purrr (>= 0.2.3), + rappdirs, rlang (>= 0.1.2), stringr, tibble, diff --git a/NAMESPACE b/NAMESPACE index 9021f43e..86af98b1 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,5 +1,6 @@ # Generated by roxygen2: do not edit by hand +S3method(print,trakt_token) export(":=") export(.data) export(as_label) @@ -23,6 +24,7 @@ export(episodes_summary) export(episodes_translations) export(episodes_watching) export(expr) +export(get_token) export(lists_popular) export(lists_trending) export(movies_aliases) @@ -78,7 +80,6 @@ export(shows_watched) export(shows_watching) export(sym) export(syms) -export(trakt_credentials) export(trakt_get) export(user_collection) export(user_comments) @@ -97,6 +98,10 @@ export(user_ratings) export(user_stats) export(user_watched) export(user_watchlist) +importFrom(cli,cli_alert_danger) +importFrom(cli,cli_alert_info) +importFrom(cli,cli_alert_success) +importFrom(cli,cli_inform) importFrom(dplyr,across) importFrom(dplyr,arrange) importFrom(dplyr,bind_cols) @@ -124,9 +129,6 @@ importFrom(httr,config) importFrom(httr,content) importFrom(httr,message_for_status) importFrom(httr,modify_url) -importFrom(httr,oauth2.0_token) -importFrom(httr,oauth_app) -importFrom(httr,oauth_endpoint) importFrom(httr,status_code) importFrom(httr,stop_for_status) importFrom(httr,user_agent) @@ -150,6 +152,7 @@ importFrom(purrr,modify_if) importFrom(purrr,modify_in) importFrom(purrr,pluck) importFrom(purrr,set_names) +importFrom(rappdirs,user_cache_dir) importFrom(rlang,":=") importFrom(rlang,.data) importFrom(rlang,.env) @@ -170,10 +173,10 @@ importFrom(stringr,str_replace) importFrom(stringr,str_split) importFrom(stringr,str_to_lower) importFrom(stringr,str_trim) -importFrom(stringr,str_trunc) importFrom(tibble,as_tibble) importFrom(tibble,enframe) importFrom(tibble,remove_rownames) importFrom(tibble,tibble) importFrom(tidyselect,any_of) +importFrom(utils,browseURL) importFrom(utils,head) diff --git a/NEWS.md b/NEWS.md index 8e572c49..adc7b97f 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,7 @@ # tRakt 0.16.9000 (development version) - Remove `magrittr` import and use `|>` internally, hence bumping the R dependency to `>= 4.1`. +- Switch from `{httr}` to `{httr2}`. This should have been a small under the hood change, but it also enabled major changes in the way I'm handling API secrets, allowing me to include my encrypted client secret with the package directly. THis should make it more convenient for users to make authenticated requests without having to register their own app on trakt.tv. # tRakt 0.16.0 diff --git a/R/api-oauth.R b/R/api-oauth.R new file mode 100644 index 00000000..5cdf96ca --- /dev/null +++ b/R/api-oauth.R @@ -0,0 +1,197 @@ +#' Get a trakt.tv API OAuth token +#' +#' This is an unfortunately home-brewed version of what _should_ be a simple call to [`httr2::oauth_flow_device()`], +#' but since the API plays ever so slightly fast and mildly loose with RFC 8628, that's not possible. +#' +#' @note RFC 8628 expects the device token request to have the field "device_code", +#' but the trakt.tv API expects a field named "code". That's it. It's kind of silly. +#' +#' @param cache [`TRUE`]: Cache the token to the OS-specific cache directory. See [rappdirs::user_cache_dir()]. +#' +#' @export +#' @importFrom rappdirs user_cache_dir +#' @importFrom cli cli_alert_info +#' @importFrom utils browseURL +#' @examples +#' if (FALSE) { +#' +#' get_token(cache = TRUE) +#' +#' } +#' +get_token <- function(cache = TRUE) { + + # Checking cache for a token first + cache_loc <- file.path(rappdirs::user_cache_dir("tRakt"), "token.rds") + if (file.exists(cache_loc)) { + token <- readRDS(cache_loc) + + if (token_expired(token)) { + token <- refresh_token(token) + cache_token(token) + } + + return(token) + } + + # Getting a new token + device_response <- httr2::request("https://api.trakt.tv/oauth/device/code") |> + httr2::req_headers("Content-Type" = "application/json") |> + httr2::req_body_json(data = list(client_id = get_client_id())) |> + httr2::req_perform() |> + httr2::resp_body_json() + + cli::cli_alert_info("Navigate to {.url {device_response$verification_url}} and enter {.strong {device_response$user_code}}") + + if (interactive()) { + utils::browseURL(device_response$verification_url) + } + + token <- oauth_device_token_poll(device_response) + if (cache) cache_token(token) + token +} + +#' @keywords internal +#' @noRd +oauth_device_token_poll <- function(request) { + cli::cli_progress_step("Waiting for response from server", spinner = TRUE) + + delay <- request$interval %||% 5 + deadline <- Sys.time() + request$expires_in + + token <- NULL + while (Sys.time() < deadline) { + for (i in 1:20) { + cli::cli_progress_update() + Sys.sleep(delay / 20) + } + + token <- httr2::request("https://api.trakt.tv/oauth/device/token") |> + httr2::req_headers("Content-Type" = "application/json") |> + httr2::req_body_json(data = list( + code = request$device_code, + client_id = get_client_id(), + client_secret = get_client_secret(), + grant_type = "urn:ietf:params:oauth:grant-type:device_code" + )) |> + httr2::req_error(is_error = \(x) FALSE) |> + httr2::req_perform() + + # 200: All good, got a token, can move on + if (httr2::resp_status(token) == 200) break + # 400: Pending, waiting for user to authorize, keep going + # 429: Slow Down -- increase delay I guess + if (httr2::resp_status(token) == 429) { + delay <- delay + 5 + } + + # Everything else: Various failure modes + # This feels inelegant but I think it gets the job down alright so meh + reason <- switch(as.character(httr2::resp_status(token)), + "404" = "Invalid device code, please check your credentials and try again", + "409" = "Code was already used for authentication", + "410" = "Token has expired, please try again (and maybe hurry up a little)", + "418" = "Denied - user explicitly denied this code", + NULL # fall through explicitly so we can conditionally abort + ) + + if (!is.null(reason)) rlang::abort(reason) + + } + cli::cli_progress_done() + + if (is.null(token)) { + cli::cli_alert_warning("Failed to get token, please retry.") + return(NULL) + } + if (httr2::resp_status(token) != 200) { + cli::cli_alert_warning("Failed to get token, please retry.") + return(NULL) + } + + token <- httr2::resp_body_json(token) + class(token) <- "trakt_token" + token +} + + +get_client_secret <- function() { + + env_var <- Sys.getenv("trakt_client_secret", unset = "") + key_var <- Sys.getenv("tRakt_key", unset = "") + + if (env_var != "") return(env_var) + + if (key_var == "") { + return(NULL) + } + + httr2::secret_decrypt(tRakt_client_secret_scrambled, "tRakt_key") + +} + +get_client_id <- function() { + env_var <- Sys.getenv("trakt_client_id", unset = "") + if (nchar(env_var) == 64) return(env_var) + + tRakt_client_id +} + +token_expired <- function(token) { + Sys.time() >= as.POSIXct(token$created_at + token$expires_in) +} + +cache_token <- function(token) { + cache_loc <- file.path(getOption("tRakt_cache_dir"), "token.rds") + + if (!dir.exists(cache_dir)) dir.create(cache_dir) + + class(token) <- "trakt_token" + + saveRDS(token, file = cache_loc) + invisible(token) +} + +clear_cached_token <- function() { + cache_loc <- file.path(getOption("tRakt_cache_dir"), "token.rds") + file.remove(cache_loc) +} + +#' @keywords internal +refresh_token <- function(token, cache = TRUE) { + token <- httr2::request("https://api.trakt.tv/oauth/token") |> + httr2::req_headers("Content-Type" = "application/json") |> + httr2::req_body_json(data = list( + refresh_token = token$refresh_token, + client_id = get_client_id(), + client_secret = get_client_secret(), + grant_type = "refresh_token", + redirect_uri = "urn:ietf:wg:oauth:2.0:oob" + )) |> + httr2::req_perform() |> + httr2::resp_body_json() + + if (cache) cache_token(token) + token +} + + + + +#' @importFrom cli cli_inform cli_alert_success cli_alert_danger +#' @noRd +#' @export +print.trakt_token <- function(x, ...) { + created_at <- as.POSIXct(x$created_at) + expires_in <- as.POSIXct(x$created_at + x$expires_in) + + valid <- expires_in > Sys.time() + + cli::cli_inform("{.url trakt.tv} token created {created_at}") + if (valid) { + cli::cli_alert_success("Valid until {expires_in}") + } else { + cli::cli_alert_danger("Expired since {expires_in}") + } +} diff --git a/R/api-requests.R b/R/api-requests.R new file mode 100644 index 00000000..a6535559 --- /dev/null +++ b/R/api-requests.R @@ -0,0 +1,89 @@ +#' Make an API call and receive parsed output +#' +#' The most basic form of API interaction: Querying a specific URL and getting +#' its parsed result. If the response is empty, the function returns an empty +#' [tibble()][tibble::tibble-package], and if there are date-time variables +#' present in the response, they are converted to `POSIXct` via +#' [lubridate::ymd_hms()] or to `Date` via [lubridate::as_date()] if the +#' variable only contains date information. +#' +#' @details +#' See [the official API reference](https://trakt.docs.apiary.io) for a detailed +#' overview of available methods. Most methods of potential interest for data +#' collection have dedicated functions in this package. + +#' @param url `character(1)`: The API endpoint. Either a full URL like +#' `"https://api.trakt.tv/shows/breaking-bad"` or just the endpoint like +#' `shows/breaking-bad`. +#' @return The parsed ([jsonlite::fromJSON()]) content of the API response. +#' An empty [tibble()][tibble::tibble-package] if the response is an empty +#' `JSON` array. +#' @export +#' @importFrom httr user_agent config add_headers +#' @importFrom httr HEAD GET +#' @importFrom httr message_for_status status_code +#' @importFrom httr content +#' @importFrom jsonlite fromJSON +#' @importFrom purrr flatten +#' @importFrom purrr is_empty +#' @family API-basics +#' @examples +#' # A simple request to a direct URL +#' trakt_get("https://api.trakt.tv/shows/breaking-bad") +#' +#' # Optionally be lazy about URL specification by dropping the hostname: +#' trakt_get("shows/game-of-thrones") +trakt_get <- function(url) { + if (!grepl(pattern = "^https://api.trakt.tv", url)) { + url <- build_trakt_url(url) + } + + if (!grepl(pattern = "^https://api.trakt.tv/\\w+", url)) { + rlang::abort("URL does not appear to be a valid trakt.tv API url") + } + + token <- get_token() + + # Software versions for user agent + versions <- c( + tRakt = paste(utils::packageVersion("tRakt"), "(https://github.com/jemus42/tRakt)"), + httr2 = as.character(utils::packageVersion("httr2")), + `r-curl` = as.character(utils::packageVersion("curl")), + libcurl = curl::curl_version()$version + ) + versions <- paste0(names(versions), "/", versions, collapse = " ") + + # Cache directory for responses + cache_dir <- file.path(getOption("tRakt_cache_dir"), "data") + + req <- httr2::request(url) |> + httr2::req_headers( + # Additional headers required by the API + "trakt-api-key" = get_client_id(), + "Content-Type" = "application/json", + "trakt-api-version" = 2 + ) |> + httr2::req_auth_bearer_token(token = token$access_token) |> + httr2::req_retry(max_tries = 3) |> + httr2::req_cache(path = cache_dir, use_on_error = TRUE, debug = getOption("tRakt_debug")) |> + httr2::req_user_agent(versions) + + resp <- httr2::req_perform(req) + + httr2::resp_check_status(resp, info = url) + + #resp <- httr2::resp_body_json(resp, simplifyVector = TRUE) + resp <- httr2::resp_body_json(resp, simplifyVector = TRUE, check_type = FALSE) + + # Kept from previous version, should be refactored at some point + if (identical(resp, "") | length(resp) == 0) { + return(tibble()) + } + + # Do it in every other function or do it here once + if (!is.null(names(resp))) { + resp <- fix_datetime(resp) + } + + resp +} diff --git a/R/api.R b/R/api.R deleted file mode 100644 index 97b74451..00000000 --- a/R/api.R +++ /dev/null @@ -1,242 +0,0 @@ -#' Set the required trakt.tv API credentials -#' -#' `trakt_credentials` searches for your credentials and stores them -#' in the appropriate [environment variables][base::Sys.setenv] of the same name. -#' To make this work automatically, place your key as environment variables in -#' `~/.Renviron` (see `Details`). -#' Arguments to this function take precedence over any configuration file. -#' -#' This function is called automatically when the package is loaded via `library(tRakt)` -#' or `tRakt::fun` function calls – you basically never have to use it if you have -#' stored your credentials as advised. -#' Additionally, for regular (non-authenticated) API interaction, you do not have to -#' set any credentials at all because the package's `client_secret` is used as a fallback, -#' which allows you to use most functions out of the box. -#' -#' Set appropriate values in your `~/.Renviron` like this: -#' -#' ```sh -#' # tRakt -#' trakt_username=jemus42 -#' trakt_client_id=12[...]f2 -#' trakt_client_secret=f23[...]2nkjb -#' ``` -#' -#' If (and only if) the environment option `trakt_client_secret` is set to a non-empty -#' string (i.e. it's not `""`), then all requests will be made using authentication. -#' -#' @param username `character(1)`: Explicitly set your trakt.tv username -#' (optional). -#' @param client_id `character(1)`: Explicitly set your API client ID -#' (required for *any* API interaction). -#' @param client_secret `character(1)`: Explicitly set your API client secret -#' (required only for *authenticated* API interaction). -#' @param silent `logical(1) [TRUE]`: No messages are printed showing you the -#' API information. Mostly for debug purposes. -#' @return Invisibly: A `list` with elements `username`, `client_id` and `client_secret`, -#' where values are `TRUE` if the corresponding value is non-empty. -#' @export -#' @family API-basics -#' @importFrom stringr str_trunc -#' @examples -#' \dontrun{ -#' # Use a values set in ~/.Renviron in an R session: -#' # (This is automatically executed when calling library(tRakt)) -#' trakt_credentials(silent = FALSE) -#' -#' # Explicitly set values in an R session, overriding .Renviron values -#' trakt_credentials( -#' username = "jemus42", -#' client_id = "totallylegitclientsecret", -#' silent = FALSE -#' ) -#' } -trakt_credentials <- function(username, - client_id, - client_secret, - silent = TRUE) { - # Check username ---- - if (!missing(username)) { - Sys.setenv("trakt_username" = username) - } - - # Check client id (required) ---- - if (!missing(client_id)) { - Sys.setenv("trakt_client_id" = client_id) - } - - # Set internal package client id if there is none yet - if (Sys.getenv("trakt_client_id") == "") { - Sys.setenv("trakt_client_id" = tRakt_client_id) - } - - # check client secret (optional(ish)) ---- - if (!missing(client_secret)) { - Sys.setenv("trakt_client_secret" = client_secret) - } - - if (!silent) { - message("trakt username: ", Sys.getenv("trakt_username")) - message( - "trakt client id: ", - stringr::str_trunc(Sys.getenv("trakt_client_id"), width = 7, ellipsis = "...") - ) - message( - "trakt client secret: ", - stringr::str_trunc(Sys.getenv("trakt_client_secret"), width = 5, ellipsis = "...") - ) - } - - invisible( - list( - username = Sys.getenv("trakt_username") != "", - client_id = Sys.getenv("trakt_client_id") != "", - client_secret = Sys.getenv("trakt_client_secret") != "" - ) - ) -} - -#' Make an API call and receive parsed output -#' -#' The most basic form of API interaction: Querying a specific URL and getting -#' its parsed result. If the response is empty, the function returns an empty -#' [tibble()][tibble::tibble-package], and if there are date-time variables -#' present in the response, they are converted to `POSIXct` via -#' [lubridate::ymd_hms()] or to `Date` via [lubridate::as_date()] if the -#' variable only contains date information. -#' -#' @details -#' See [the official API reference](https://trakt.docs.apiary.io) for a detailed -#' overview of available methods. Most methods of potential interest for data -#' collection have dedicated functions in this package. -#' @note No OAuth2 methods are supported yet, meaning you don't have access to -#' `POST` methods or user information of non-public profiles. -#' @param url `character(1)`: The API endpoint. Either a full URL like -#' `"https://api.trakt.tv/shows/breaking-bad"` or just the endpoint like -#' `shows/breaking-bad`. -#' @param client_id `character(1)`: API client ID. If no value is set, -#' this defaults to the package's client ID. See [trakt_credentials] for -#' further information. -#' @param HEAD `logical(1) [FALSE]`: If `TRUE`, only a HTTP `HEAD` request is -#' performed and its content returned. This is useful if you are only -#' interested in status codes or other headers, and don't want to waste -#' resources/bandwidth on the response body. -#' @return The parsed ([jsonlite::fromJSON()]) content of the API response. -#' An empty [tibble()][tibble::tibble-package] if the response is an empty -#' `JSON` array. -#' @export -#' @importFrom httr user_agent config add_headers -#' @importFrom httr HEAD GET -#' @importFrom httr message_for_status status_code -#' @importFrom httr content -#' @importFrom jsonlite fromJSON -#' @importFrom purrr flatten -#' @importFrom purrr is_empty -#' @family API-basics -#' @examples -#' # A simple request to a direct URL -#' trakt_get("https://api.trakt.tv/shows/breaking-bad") -#' -#' # A HEAD-only request -#' # useful for validating a URL exists or the API is accessible -#' trakt_get("https://api.trakt.tv/users/jemus42", HEAD = TRUE) -#' -#' # Optionally be lazy about URL specification by dropping the hostname: -#' trakt_get("shows/game-of-thrones") -trakt_get <- function(url, - client_id = Sys.getenv("trakt_client_id"), - HEAD = FALSE) { - if (!grepl(pattern = "^https://api.trakt.tv", url)) { - url <- build_trakt_url(url) - } - - if (Sys.getenv("trakt_client_secret") != "") { - token <- trakt_get_token() - } else { - token <- NULL - } - - # Headers and metadata - agent <- user_agent("https://github.com/jemus42/tRakt") - - headers <- add_headers(.headers = c( - "trakt-api-key" = client_id, - "Content-Type" = "application/json", - "trakt-api-version" = 2 - )) - - # Make the call - if (HEAD) { - response <- HEAD(url, headers, agent) - response <- flatten(response$all_headers) - return(response) - } - - response <- GET(url, headers, agent, config(token = token)) - - # Fail on HTTP error, i.e. 404 or 5xx. - if (status_code(response) >= 300) { - message_for_status(response, paste0("retrieve data from\n", url)) - } - - # Parse output - response <- content(response, as = "text", encoding = "UTF-8") - - if (identical(response, "")) { - return(tibble()) - } - - response <- fromJSON(response) - - # To make empty response handling easier - if (is_empty(response)) { - return(tibble()) - } - - # Do it in every other function or do it here once - if (!is.null(names(response))) { - response <- fix_datetime(response) - } - - response -} -# nocov start -#' Get a trakt.tv OAuth2 token -#' -#' This is used internally for authenticated requests. -#' @return An OAuth2 token object. See [oauth2.0_token][httr::oauth2.0_token]. -#' @keywords internal -#' @family API-basics -#' @importFrom httr oauth_endpoint oauth_app oauth2.0_token user_agent -trakt_get_token <- function() { - if (Sys.getenv("trakt_client_secret") == "") { - stop( - "No client secret set, can't use authentication.\n", - "See ?trakt_credentials to see how to set up your credentials." - ) - } - - # Set up OAuth URLs - trakt_endpoint <- oauth_endpoint( - authorize = "https://trakt.tv/oauth/authorize", - access = "https://api.trakt.tv/oauth/token" - ) - - # Application credentials: https://trakt.tv/oauth/applications - app <- oauth_app( - appname = "trakt", - key = Sys.getenv("trakt_client_id"), - secret = Sys.getenv("trakt_client_secret"), - redirect_uri = "urn:ietf:wg:oauth:2.0:oob" - ) - - # 3. Get OAuth credentials - oauth2.0_token( - endpoint = trakt_endpoint, - app = app, - use_oob = TRUE, - config_init = user_agent("https://github.com/jemus42/tRakt"), - cache = TRUE - ) -} -# nocov end diff --git a/R/docs-common.R b/R/docs-common.R index 34f4de94..03cda260 100644 --- a/R/docs-common.R +++ b/R/docs-common.R @@ -18,14 +18,14 @@ #' @param type `character(1)`: Either `"shows"` or `"movies"`. For season/episode-specific #' functions, values `seasons` or `episodes` are also allowed. #' @param user `character(1)`: Target username (or `slug`). Defaults to -#' `getOption("trakt_username")`. +#' `"me"`, the OAuth user. #' Can also be of length greater than 1, in which case the function is called on all #' `user` values separately and the result is combined. #' @param period `character(1) ["weekly"]`: Which period to filter by. Possible values #' are `"weekly"`, `"monthly"`, `"yearly"`, `"all"`. #' @param limit `integer(1) [10L]`: Number of items to return. Must be greater #' than `0` and will be coerced via `as.integer()`. -#' @param season,episode `integer(1) [1L]`: The season and eisode number. If longer, +#' @param season,episode `integer(1) [1L]`: The season and episode number. If longer, #' e.g. `1:5`, the function is vectorized and the output will be #' combined. This may result in *a lot* of API calls. Use wisely. #' @param start_date `character(1)`: A date in the past from which diff --git a/R/user_collection.R b/R/user_collection.R index 9c227017..0de20ea8 100644 --- a/R/user_collection.R +++ b/R/user_collection.R @@ -25,7 +25,7 @@ #' user_collection(user = "sean", type = "movies") #' user_collection(user = "sean", type = "shows") #' } -user_collection <- function(user = getOption("trakt_username"), +user_collection <- function(user = "me", type = c("shows", "movies"), unnest_episodes = FALSE, extended = c("min", "full")) { diff --git a/R/user_comments.R b/R/user_comments.R index 11d20d49..f700ca69 100644 --- a/R/user_comments.R +++ b/R/user_comments.R @@ -19,7 +19,7 @@ #' \dontrun{ #' user_comments("jemus42") #' } -user_comments <- function(user = getOption("trakt_username"), +user_comments <- function(user = "me", comment_type = c("all", "reviews", "shouts"), type = c( "all", "movies", "shows", "seasons", diff --git a/R/user_history.R b/R/user_history.R index 8b07b629..668fe8de 100644 --- a/R/user_history.R +++ b/R/user_history.R @@ -28,8 +28,9 @@ #' start_at = "2015-12-24", end_at = "2015-12-28" #' ) #' } -user_history <- function(user = getOption("trakt_username"), - type = c("shows", "movies"), +user_history <- function(user = "me", + type = c("shows", "movies", "seasons", "episodes"), + item_id = NULL, limit = 10L, start_at = NULL, end_at = NULL, extended = c("min", "full")) { check_username(user) @@ -41,13 +42,14 @@ user_history <- function(user = getOption("trakt_username"), if (length(user) > 1) { names(user) <- user - return(map_df(user, ~ user_history(user = .x, type, limit, start_at, end_at, extended), + return(map_df(user, ~ user_history(user = .x, type, item_id = item_id, + limit, start_at, end_at, extended), .id = "user" )) } # Construct URL, make API call - url <- build_trakt_url("users", user, "history", type, + url <- build_trakt_url("users", user, "history", type, item_id = item_id, extended = extended, limit = limit, start_at = start_at, end_at = end_at ) response <- trakt_get(url = url) diff --git a/R/user_list_comments.R b/R/user_list_comments.R index c1954591..1438fdf8 100644 --- a/R/user_list_comments.R +++ b/R/user_list_comments.R @@ -12,7 +12,7 @@ #' \dontrun{ #' user_list_comments("donxy", "1248149") #' } -user_list_comments <- function(user = getOption("trakt_username"), +user_list_comments <- function(user = "me", list_id, sort = c("newest", "oldest", "likes", "replies"), extended = c("min", "full")) { diff --git a/R/user_list_items.R b/R/user_list_items.R index 6d7192ca..8426c742 100644 --- a/R/user_list_items.R +++ b/R/user_list_items.R @@ -31,7 +31,7 @@ #' # Only episodes #' user_list_items("sp1ti", list_id = "5615781", extended = "min", type = "episodes") #' } -user_list_items <- function(user = getOption("trakt_username"), +user_list_items <- function(user = "me", list_id, type = NULL, extended = c("min", "full")) { check_username(user) diff --git a/R/user_lists.R b/R/user_lists.R index 6cbec77e..9ca76e66 100644 --- a/R/user_lists.R +++ b/R/user_lists.R @@ -20,7 +20,7 @@ #' \dontrun{ #' user_lists("jemus42") #' } -user_lists <- function(user = getOption("trakt_username"), extended = c("min", "full")) { +user_lists <- function(user = "me", extended = c("min", "full")) { check_username(user) extended <- match.arg(extended) @@ -67,7 +67,7 @@ user_lists <- function(user = getOption("trakt_username"), extended = c("min", " #' \dontrun{ #' user_list("jemus42", list_id = 2121308) #' } -user_list <- function(user = getOption("trakt_username"), list_id, +user_list <- function(user = "me", list_id, extended = c("min", "full")) { check_username(user) extended <- match.arg(extended) diff --git a/R/user_network.R b/R/user_network.R index e7c583e8..414593e8 100644 --- a/R/user_network.R +++ b/R/user_network.R @@ -19,7 +19,7 @@ NULL #' @keywords internal #' @noRd user_network <- function(relationship = c("friends", "followers", "following"), - user = getOption("trakt_username"), + user = "me", extended = c("min", "full")) { check_username(user) extended <- match.arg(extended) diff --git a/R/user_ratings.R b/R/user_ratings.R index cf8ea0ce..83b9dc96 100644 --- a/R/user_ratings.R +++ b/R/user_ratings.R @@ -13,7 +13,7 @@ #' user_ratings(user = "jemus42", "shows") #' user_ratings(user = "sean", type = "movies") #' } -user_ratings <- function(user = getOption("trakt_username"), +user_ratings <- function(user = "me", type = c("movies", "seasons", "shows", "episodes"), rating = NULL, extended = c("min", "full")) { check_username(user) diff --git a/R/user_stats.R b/R/user_stats.R index 4000a6ea..55187bb0 100644 --- a/R/user_stats.R +++ b/R/user_stats.R @@ -20,7 +20,7 @@ #' \dontrun{ #' user_stats(user = "sean") #' } -user_stats <- function(user = getOption("trakt_username")) { +user_stats <- function(user = "me") { check_username(user) if (length(user) > 1) { diff --git a/R/user_summary.R b/R/user_summary.R index 07e27b57..f69a2fa2 100644 --- a/R/user_summary.R +++ b/R/user_summary.R @@ -12,7 +12,7 @@ #' \dontrun{ #' user_profile("sean") #' } -user_profile <- function(user = getOption("trakt_username"), +user_profile <- function(user = "me", extended = c("min", "full")) { check_username(user) extended <- match.arg(extended) diff --git a/R/user_watched.R b/R/user_watched.R index 10bcfbdb..f3e133ff 100644 --- a/R/user_watched.R +++ b/R/user_watched.R @@ -18,7 +18,7 @@ #' # Use noseasons = TRUE to avoid receiving detailed season/episode data #' user_watched(user = "sean", noseasons = TRUE) #' } -user_watched <- function(user = getOption("trakt_username"), +user_watched <- function(user = "me", type = c("shows", "movies"), noseasons = TRUE, extended = c("min", "full")) { diff --git a/R/user_watchlist.R b/R/user_watchlist.R index 9fe5865a..270bddc6 100644 --- a/R/user_watchlist.R +++ b/R/user_watchlist.R @@ -13,7 +13,7 @@ #' # Defaults to movie watchlist and minimal info #' user_watchlist(user = "sean") #' } -user_watchlist <- function(user = getOption("trakt_username"), +user_watchlist <- function(user = "me", type = c("movies", "shows"), extended = c("min", "full")) { check_username(user) diff --git a/R/utils-misc.R b/R/utils-misc.R index a184360c..4fd35d33 100644 --- a/R/utils-misc.R +++ b/R/utils-misc.R @@ -42,8 +42,8 @@ pad_episode <- function(s = "0", e = "0", s_width = 2, e_width = 2) { #' `tron-legacy-2012` and `releases` will be concatenated to #' `movies/tron-legacy-2012/releases`. Additional **named** arguments will be #' used as query parameters, usually `extended = "full"` or others. -#' @param validate `logical(1) [TRUE]`: Whether to check the URL via -#' `httr::HEAD` request. +# @param validate `logical(1) [TRUE]`: Whether to check the URL via +# `httr::HEAD` request. #' @return A URL: `character` of length 1. If `validate = TRUE`, also a message #' including the HTTP status code return by a `HEAD` request. #' @family utility functions @@ -60,9 +60,8 @@ pad_episode <- function(s = "0", e = "0", s_width = 2, e_width = 2) { #' # Path can also be partially assembled already #' build_trakt_url("users/jemus42", "ratings") #' -#' # Validate a URL works -#' build_trakt_url("shows", "popular", page = 1, limit = 5, validate = TRUE) -build_trakt_url <- function(..., validate = FALSE) { +#' build_trakt_url("shows", "popular", page = 1, limit = 5) +build_trakt_url <- function(...) { dots <- list(...) # Nuke NULL elements @@ -77,15 +76,23 @@ build_trakt_url <- function(..., validate = FALSE) { queries <- NULL } - url <- modify_url(url = "https://api.trakt.tv", path = path, query = queries) + if (!grepl(pattern = "^\\/", path)) { + path <- paste0("/", path) + } + + # url <- modify_url(url = "https://api.trakt.tv", path = path, query = queries) + url <- httr2::url_parse(url = "https://api.trakt.tv/") + url$path <- path + url$query <- queries + url <- httr2::url_build(url) # Validate - if (validate) { - response <- trakt_get(url, HEAD = TRUE) - if (!identical(response$status, 200L)) { - stop_for_status(response$status) - } - } + # if (validate) { + # response <- trakt_get(url, HEAD = TRUE) + # if (!identical(response$status, 200L)) { + # stop_for_status(response$status) + # } + # } url } diff --git a/R/utils-third-party-APIs.R b/R/utils-third-party-APIs.R index 9bb2829f..29a8198a 100644 --- a/R/utils-third-party-APIs.R +++ b/R/utils-third-party-APIs.R @@ -14,7 +14,6 @@ #' @noRd #' @inherit trakt_api_common_parameters return #' @keywords internal -#' @importFrom httr modify_url GET content stop_for_status #' @importFrom jsonlite fromJSON #' @importFrom tibble as_tibble tibble #' @examples @@ -22,25 +21,22 @@ #' omdb_get("tt0903747") #' } omdb_get <- function(imdb) { - base_url <- modify_url("https://www.omdbapi.com/", - query = list(apikey = Sys.getenv("OMDB_API_KEY")) - ) - - url <- modify_url(base_url, query = list(i = imdb)) - - res <- GET(url) - stop_for_status(res, "Getting data from OMDBapi") - - res <- content(res, as = "text") - res <- fromJSON(res) + res <- httr2::request("https://www.omdbapi.com/") |> + httr2::req_url_query(i = imdb, apikey = Sys.getenv("OMDB_API_KEY")) |> + httr2::req_perform() |> + httr2::resp_body_json(simplifyVector = TRUE) if (identical(res$Response, "False")) { warning(imdb, ": ", res$Error) return(tibble()) } - res <- as_tibble(res) - res <- res[names(res) != "Ratings"] + res <- dplyr::bind_cols( + res[which(names(res) != "Ratings")], + rating_rotten_tomatoes = res$Ratings$Value[[1]], + rating_imdb = res$Ratings$Value[[2]] + ) + res$imdbRating <- as.numeric(res$imdbRating) res$imdbVotes <- as.numeric(gsub(",", "", res$imdbVotes)) @@ -57,7 +53,6 @@ omdb_get <- function(imdb) { #' @return A [tibble()][tibble::tibble-package] with multiple list-columns. #' @keywords internal #' @noRd -#' @importFrom httr modify_url GET content #' @importFrom purrr map #' @importFrom tibble tibble as_tibble #' @importFrom jsonlite fromJSON @@ -67,15 +62,14 @@ omdb_get <- function(imdb) { #' fanarttv_get(tvdb = "81189") #' } fanarttv_get <- function(tvdb) { - url <- modify_url( - url = "http://webservice.fanart.tv", - path = paste0("v3/tv/", tvdb), - query = list(api_key = Sys.getenv("fanarttv_api_key")) - ) - res <- GET(url) - res <- fromJSON(content(res, as = "text")) - res <- map(res, as_tibble) + res <- httr2::request("http://webservice.fanart.tv") |> + httr2::req_url_path_append("v3/tv", tvdb) |> + httr2::req_url_query(api_key = Sys.getenv("fanarttv_api_key")) |> + httr2::req_perform() + + res <- httr2::resp_body_json(res, simplifyVector = TRUE) + res <- lapply(res, as_tibble) res_x <- tibble( name = res$name$value, diff --git a/R/zzz.R b/R/zzz.R index f4df81ff..a29d91a1 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -1,6 +1,7 @@ # nocov start .onLoad <- function(...) { - trakt_credentials() + options("tRakt_cache_dir" = rappdirs::user_cache_dir("tRakt")) + options("tRakt_debug" = FALSE) } #' The tRakt client ID for this particular app @@ -8,6 +9,14 @@ #' @noRd tRakt_client_id <- "12fc1de7671c7f2fb4a8ac08ba7c9f45b447f4d5bad5e11e3490823d629afdf2" +#' The tRakt client secret for this particular app +#' +#' Decrypt with `httr2::secret_decrypt(client_secret_scrambled, "tRakt_key")` +#' @keywords internal +#' @noRd +tRakt_client_secret_scrambled <- "3WPkxM7csJKm_a4MP4NdDA1jhzQv6N91bNv4JhUXuDTSjqwXR9kZvg12rKtu6qqIuG2-pHfyYWFUGOTxSjiee08UVfhtswL7EdiFSwUTBI0" + + #' Useful global internal variables #' Used in two functions. In case of changes, I want to only have to change it once. #' @keywords internal diff --git a/codemeta.json b/codemeta.json index c77c611d..7c5e18a3 100644 --- a/codemeta.json +++ b/codemeta.json @@ -14,7 +14,7 @@ "name": "R", "url": "https://r-project.org" }, - "runtimePlatform": "R version 4.3.0 (2023-04-21)", + "runtimePlatform": "R version 4.3.1 (2023-06-16)", "author": [ { "@type": "Person", @@ -176,6 +176,18 @@ "version": ">= 4.1" }, "2": { + "@type": "SoftwareApplication", + "identifier": "cli", + "name": "cli", + "provider": { + "@id": "https://cran.r-project.org", + "@type": "Organization", + "name": "Comprehensive R Archive Network (CRAN)", + "url": "https://cran.r-project.org" + }, + "sameAs": "https://CRAN.R-project.org/package=cli" + }, + "3": { "@type": "SoftwareApplication", "identifier": "dplyr", "name": "dplyr", @@ -188,19 +200,19 @@ }, "sameAs": "https://CRAN.R-project.org/package=dplyr" }, - "3": { + "4": { "@type": "SoftwareApplication", - "identifier": "httr", - "name": "httr", + "identifier": "httr2", + "name": "httr2", "provider": { "@id": "https://cran.r-project.org", "@type": "Organization", "name": "Comprehensive R Archive Network (CRAN)", "url": "https://cran.r-project.org" }, - "sameAs": "https://CRAN.R-project.org/package=httr" + "sameAs": "https://CRAN.R-project.org/package=httr2" }, - "4": { + "5": { "@type": "SoftwareApplication", "identifier": "jsonlite", "name": "jsonlite", @@ -213,7 +225,7 @@ }, "sameAs": "https://CRAN.R-project.org/package=jsonlite" }, - "5": { + "6": { "@type": "SoftwareApplication", "identifier": "lubridate", "name": "lubridate", @@ -225,7 +237,7 @@ }, "sameAs": "https://CRAN.R-project.org/package=lubridate" }, - "6": { + "7": { "@type": "SoftwareApplication", "identifier": "purrr", "name": "purrr", @@ -238,7 +250,19 @@ }, "sameAs": "https://CRAN.R-project.org/package=purrr" }, - "7": { + "8": { + "@type": "SoftwareApplication", + "identifier": "rappdirs", + "name": "rappdirs", + "provider": { + "@id": "https://cran.r-project.org", + "@type": "Organization", + "name": "Comprehensive R Archive Network (CRAN)", + "url": "https://cran.r-project.org" + }, + "sameAs": "https://CRAN.R-project.org/package=rappdirs" + }, + "9": { "@type": "SoftwareApplication", "identifier": "rlang", "name": "rlang", @@ -251,7 +275,7 @@ }, "sameAs": "https://CRAN.R-project.org/package=rlang" }, - "8": { + "10": { "@type": "SoftwareApplication", "identifier": "stringr", "name": "stringr", @@ -263,7 +287,7 @@ }, "sameAs": "https://CRAN.R-project.org/package=stringr" }, - "9": { + "11": { "@type": "SoftwareApplication", "identifier": "tibble", "name": "tibble", @@ -275,7 +299,7 @@ }, "sameAs": "https://CRAN.R-project.org/package=tibble" }, - "10": { + "12": { "@type": "SoftwareApplication", "identifier": "tidyselect", "name": "tidyselect", @@ -287,14 +311,14 @@ }, "sameAs": "https://CRAN.R-project.org/package=tidyselect" }, - "11": { + "13": { "@type": "SoftwareApplication", "identifier": "utils", "name": "utils" }, "SystemRequirements": null }, - "fileSize": "503.331KB", + "fileSize": "512.483KB", "releaseNotes": "https://github.com/jemus42/tRakt/blob/master/NEWS.md", "readme": "https://github.com/jemus42/tRakt/blob/main/README.md", "contIntegration": ["https://github.com/jemus42/tRakt/actions/workflows/R-CMD-check.yaml", "https://codecov.io/github/jemus42/tRakt?branch=master"], diff --git a/data-raw/get-data-episodes.R b/data-raw/get-data-episodes.R index e68e62f8..e0030f80 100644 --- a/data-raw/get-data-episodes.R +++ b/data-raw/get-data-episodes.R @@ -6,19 +6,24 @@ library(stringr) library(tidyr) # Futurama ---- -futurama <- seasons_season("futurama", seasons = 1:7, extended = "full") -usethis::use_data(futurama, overwrite = TRUE) +n_seasons <- nrow(seasons_summary("futurama")) +futurama <- seasons_season("futurama", seasons = seq_len(n_seasons), extended = "full") + +# Only update this if we're not mid-season or something. +if (max(futurama$first_aired) < Sys.time()) { + usethis::use_data(futurama, overwrite = TRUE) +} # Game of Thrones ---- got_trakt <- seasons_season("game-of-thrones", seasons = 1:8, extended = "full") # Wiki -got_wiki <- read_html("https://en.wikipedia.org/wiki/List_of_Game_of_Thrones_episodes") %>% - html_table(fill = TRUE) %>% +got_wiki <- rvest::read_html("https://en.wikipedia.org/wiki/List_of_Game_of_Thrones_episodes") %>% + rvest::html_table(fill = TRUE) %>% magrittr::extract(c(2:9)) %>% bind_rows() %>% - set_colnames(c( + setNames(c( "episode_abs", "episode", "title", "director", "writer", "firstaired", "viewers" )) %>% diff --git a/data-raw/get-data-internal.R b/data-raw/get-data-internal.R index 719b4eeb..6d115b0c 100644 --- a/data-raw/get-data-internal.R +++ b/data-raw/get-data-internal.R @@ -10,9 +10,10 @@ library(stringr) trakt_networks <- trakt_get("networks") %>% mutate( name_clean = str_to_lower(name) %>% - str_trim("both") + str_trim("both"), + .after = "name" ) %>% - as_tibble() + tidyr::unnest("ids") use_data(trakt_networks, overwrite = TRUE, compress = "xz") diff --git a/data/trakt_certifications.rda b/data/trakt_certifications.rda index 769d3e6d..93b67ca5 100644 Binary files a/data/trakt_certifications.rda and b/data/trakt_certifications.rda differ diff --git a/data/trakt_countries.rda b/data/trakt_countries.rda index ea2a1446..cc482ecf 100644 Binary files a/data/trakt_countries.rda and b/data/trakt_countries.rda differ diff --git a/data/trakt_genres.rda b/data/trakt_genres.rda index a89f8994..a094d9cb 100644 Binary files a/data/trakt_genres.rda and b/data/trakt_genres.rda differ diff --git a/data/trakt_languages.rda b/data/trakt_languages.rda index ac9b6445..3119eb42 100644 Binary files a/data/trakt_languages.rda and b/data/trakt_languages.rda differ diff --git a/data/trakt_networks.rda b/data/trakt_networks.rda index 791c1762..d16313c6 100644 Binary files a/data/trakt_networks.rda and b/data/trakt_networks.rda differ diff --git a/man/build_trakt_url.Rd b/man/build_trakt_url.Rd index 60fcbf4f..cafd5d02 100644 --- a/man/build_trakt_url.Rd +++ b/man/build_trakt_url.Rd @@ -38,8 +38,7 @@ build_trakt_url("shows", "popular", page = 3, limit = 5) # Path can also be partially assembled already build_trakt_url("users/jemus42", "ratings") -# Validate a URL works -build_trakt_url("shows", "popular", page = 1, limit = 5, validate = TRUE) +build_trakt_url("shows", "popular", page = 1, limit = 5) } \seealso{ Other utility functions: diff --git a/man/get_token.Rd b/man/get_token.Rd new file mode 100644 index 00000000..c495c855 --- /dev/null +++ b/man/get_token.Rd @@ -0,0 +1,27 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/api-oauth.R +\name{get_token} +\alias{get_token} +\title{Get a trakt.tv API OAuth token} +\usage{ +get_token(cache = TRUE) +} +\arguments{ +\item{cache}{\code{\link{TRUE}}: Cache the token to the OS-specific cache directory. See \code{\link[rappdirs:user_cache_dir]{rappdirs::user_cache_dir()}}.} +} +\description{ +This is an unfortunately home-brewed version of what \emph{should} be a simple call to \code{\link[httr2:oauth_flow_device]{httr2::oauth_flow_device()}}, +but since the API plays ever so slightly fast and mildly loose with RFC 8628, that's not possible. +} +\note{ +RFC 8628 expects the device token request to have the field "device_code", +but the trakt.tv API expects a field named "code". That's it. It's kind of silly. +} +\examples{ +if (FALSE) { + +get_token(cache = TRUE) + +} + +} diff --git a/man/trakt_credentials.Rd b/man/trakt_credentials.Rd deleted file mode 100644 index 3bf946d7..00000000 --- a/man/trakt_credentials.Rd +++ /dev/null @@ -1,71 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/api.R -\name{trakt_credentials} -\alias{trakt_credentials} -\title{Set the required trakt.tv API credentials} -\usage{ -trakt_credentials(username, client_id, client_secret, silent = TRUE) -} -\arguments{ -\item{username}{\code{character(1)}: Explicitly set your trakt.tv username -(optional).} - -\item{client_id}{\code{character(1)}: Explicitly set your API client ID -(required for \emph{any} API interaction).} - -\item{client_secret}{\code{character(1)}: Explicitly set your API client secret -(required only for \emph{authenticated} API interaction).} - -\item{silent}{\code{logical(1) [TRUE]}: No messages are printed showing you the -API information. Mostly for debug purposes.} -} -\value{ -Invisibly: A \code{list} with elements \code{username}, \code{client_id} and \code{client_secret}, -where values are \code{TRUE} if the corresponding value is non-empty. -} -\description{ -\code{trakt_credentials} searches for your credentials and stores them -in the appropriate \link[base:Sys.setenv]{environment variables} of the same name. -To make this work automatically, place your key as environment variables in -\verb{~/.Renviron} (see \code{Details}). -Arguments to this function take precedence over any configuration file. -} -\details{ -This function is called automatically when the package is loaded via \code{library(tRakt)} -or \code{tRakt::fun} function calls – you basically never have to use it if you have -stored your credentials as advised. -Additionally, for regular (non-authenticated) API interaction, you do not have to -set any credentials at all because the package's \code{client_secret} is used as a fallback, -which allows you to use most functions out of the box. - -Set appropriate values in your \verb{~/.Renviron} like this: - -\if{html}{\out{
}}\preformatted{# tRakt -trakt_username=jemus42 -trakt_client_id=12[...]f2 -trakt_client_secret=f23[...]2nkjb -}\if{html}{\out{
}} - -If (and only if) the environment option \code{trakt_client_secret} is set to a non-empty -string (i.e. it's not \code{""}), then all requests will be made using authentication. -} -\examples{ -\dontrun{ -# Use a values set in ~/.Renviron in an R session: -# (This is automatically executed when calling library(tRakt)) -trakt_credentials(silent = FALSE) - -# Explicitly set values in an R session, overriding .Renviron values -trakt_credentials( - username = "jemus42", - client_id = "totallylegitclientsecret", - silent = FALSE -) -} -} -\seealso{ -Other API-basics: -\code{\link{trakt_get_token}()}, -\code{\link{trakt_get}()} -} -\concept{API-basics} diff --git a/man/trakt_get.Rd b/man/trakt_get.Rd index e523de33..8c977ac7 100644 --- a/man/trakt_get.Rd +++ b/man/trakt_get.Rd @@ -1,24 +1,15 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/api.R +% Please edit documentation in R/api-requests.R \name{trakt_get} \alias{trakt_get} \title{Make an API call and receive parsed output} \usage{ -trakt_get(url, client_id = Sys.getenv("trakt_client_id"), HEAD = FALSE) +trakt_get(url) } \arguments{ \item{url}{\code{character(1)}: The API endpoint. Either a full URL like \code{"https://api.trakt.tv/shows/breaking-bad"} or just the endpoint like \code{shows/breaking-bad}.} - -\item{client_id}{\code{character(1)}: API client ID. If no value is set, -this defaults to the package's client ID. See \link{trakt_credentials} for -further information.} - -\item{HEAD}{\code{logical(1) [FALSE]}: If \code{TRUE}, only a HTTP \code{HEAD} request is -performed and its content returned. This is useful if you are only -interested in status codes or other headers, and don't want to waste -resources/bandwidth on the response body.} } \value{ The parsed (\code{\link[jsonlite:fromJSON]{jsonlite::fromJSON()}}) content of the API response. @@ -38,24 +29,11 @@ See \href{https://trakt.docs.apiary.io}{the official API reference} for a detail overview of available methods. Most methods of potential interest for data collection have dedicated functions in this package. } -\note{ -No OAuth2 methods are supported yet, meaning you don't have access to -\code{POST} methods or user information of non-public profiles. -} \examples{ # A simple request to a direct URL trakt_get("https://api.trakt.tv/shows/breaking-bad") -# A HEAD-only request -# useful for validating a URL exists or the API is accessible -trakt_get("https://api.trakt.tv/users/jemus42", HEAD = TRUE) - # Optionally be lazy about URL specification by dropping the hostname: trakt_get("shows/game-of-thrones") } -\seealso{ -Other API-basics: -\code{\link{trakt_credentials}()}, -\code{\link{trakt_get_token}()} -} \concept{API-basics} diff --git a/man/trakt_get_token.Rd b/man/trakt_get_token.Rd deleted file mode 100644 index 882511b3..00000000 --- a/man/trakt_get_token.Rd +++ /dev/null @@ -1,21 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/api.R -\name{trakt_get_token} -\alias{trakt_get_token} -\title{Get a trakt.tv OAuth2 token} -\usage{ -trakt_get_token() -} -\value{ -An OAuth2 token object. See \link[httr:oauth2.0_token]{oauth2.0_token}. -} -\description{ -This is used internally for authenticated requests. -} -\seealso{ -Other API-basics: -\code{\link{trakt_credentials}()}, -\code{\link{trakt_get}()} -} -\concept{API-basics} -\keyword{internal} diff --git a/tests/testthat/setup-tests.R b/tests/testthat/setup-tests.R index 2d909132..67095f9d 100644 --- a/tests/testthat/setup-tests.R +++ b/tests/testthat/setup-tests.R @@ -1,5 +1,5 @@ skip_if_no_auth <- function() { - if (identical(Sys.getenv("trakt_client_secret"), "")) { + if (!inherits(get_token(), "trakt_token")) { skip("No authentication available") } } diff --git a/tests/testthat/test-api.R b/tests/testthat/test-api.R index 0fa1e596..f598e33b 100644 --- a/tests/testthat/test-api.R +++ b/tests/testthat/test-api.R @@ -1,25 +1,25 @@ -test_that("Client ID is set without .Renviron", { - # Save current settings for later - username_pre <- Sys.getenv("trakt_username") - client_id_pre <- Sys.getenv("trakt_client_id") - - client_id <- "12fc1de7671c7f2fb4a8ac08ba7c9f45b447f4d5bad5e11e3490823d629afdf2" - expect_message(trakt_credentials(silent = FALSE)) - - Sys.setenv("trakt_client_id" = "") - expect_failure(expect_message(trakt_credentials())) - expect_equal(Sys.getenv("trakt_client_id"), client_id) - - expect_message( - trakt_credentials(username = "arbitraryusername", silent = FALSE) - ) - expect_equal(Sys.getenv("trakt_username"), "arbitraryusername") - expect_message(trakt_credentials(client_id = client_id, silent = FALSE)) - - # Restore previous settings - Sys.setenv("trakt_username" = username_pre) - Sys.setenv("trakt_client_id" = client_id_pre) -}) +# test_that("Client ID is set without .Renviron", { +# # Save current settings for later +# username_pre <- Sys.getenv("trakt_username") +# client_id_pre <- Sys.getenv("trakt_client_id") +# +# client_id <- "12fc1de7671c7f2fb4a8ac08ba7c9f45b447f4d5bad5e11e3490823d629afdf2" +# expect_message(trakt_credentials(silent = FALSE)) +# +# Sys.setenv("trakt_client_id" = "") +# expect_failure(expect_message(trakt_credentials())) +# expect_equal(Sys.getenv("trakt_client_id"), client_id) +# +# expect_message( +# trakt_credentials(username = "arbitraryusername", silent = FALSE) +# ) +# expect_equal(Sys.getenv("trakt_username"), "arbitraryusername") +# expect_message(trakt_credentials(client_id = client_id, silent = FALSE)) +# +# # Restore previous settings +# Sys.setenv("trakt_username" = username_pre) +# Sys.setenv("trakt_client_id" = client_id_pre) +# }) test_that("trakt_get can make API calls", { # skip_on_cran() @@ -28,7 +28,7 @@ test_that("trakt_get can make API calls", { result <- trakt_get(url) expect_is(result, "list") - expect_message(trakt_get("https://example.com")) + expect_error(trakt_get("https://example.com")) }) test_that("authenticated requests work", { diff --git a/tests/testthat/test-media_stats.R b/tests/testthat/test-media_stats.R index c2b03439..3b093390 100644 --- a/tests/testthat/test-media_stats.R +++ b/tests/testthat/test-media_stats.R @@ -1,20 +1,24 @@ -test_that("user_stats works", { +test_that("user_stats works for 1 user", { skip_on_cran() - user <- "jemus42" - - userstats <- user_stats(user = user) + userstats <- user_stats(user = "jemus42") expect_is(userstats, "list") expect_named(userstats, c( "movies", "shows", "seasons", "episodes", "network", "ratings" )) +}) + +test_that("user_stats works for multiple users", { + skip_on_cran() + + users <- c("jemus42", "sean") - userstats <- user_stats(user = c(user, "sean")) + userstats <- user_stats(user = users) expect_is(userstats, "list") - expect_named(userstats, c(user, "sean")) - expect_named(userstats[[user]], c( + expect_named(userstats, users) + expect_named(userstats[[users[[1]]]], c( "movies", "shows", "seasons", "episodes", "network", "ratings" )) diff --git a/tests/testthat/test-seasons.R b/tests/testthat/test-seasons.R index 4b4ca658..2031b99b 100644 --- a/tests/testthat/test-seasons.R +++ b/tests/testthat/test-seasons.R @@ -24,7 +24,7 @@ test_that("seasons_season works", { expect_error(seasons_season(id = id, seasons = NA)) expect_error(seasons_season(id = id, seasons = "seven")) expect_error(seasons_season(id = id, seasons = NULL)) - expect_message(seasons_season(id = id, seasons = 10)) + expect_error(seasons_season(id = id, seasons = 10)) # Multi-length input seasons expect_identical( @@ -56,7 +56,6 @@ test_that("seasons_summary works", { expect_equal(ncol(result_min), 4) expect_equal(ncol(result_max), 13) - expect_lt(length(result_min), length(result_max)) expect_equal(nrow(result_min), nrow(result_max)) expect_identical( @@ -67,7 +66,7 @@ test_that("seasons_summary works", { seasons_summary(c(id, id)) ) - expect_message(seasons_summary(id = "bvkjqbkqjbf")) + expect_error(seasons_summary(id = "bvkjqbkqjbf")) }) test_that("seasons_summary works for episodes and matches seasons_season", { diff --git a/tests/testthat/test-shows_next_episode.R b/tests/testthat/test-shows_next_episode.R index 7be7b5c3..c0dec7d9 100644 --- a/tests/testthat/test-shows_next_episode.R +++ b/tests/testthat/test-shows_next_episode.R @@ -14,8 +14,8 @@ test_that("shows_(next|last)_episode() works", { shows_last_episode("one-piece", extended = "full") |> expect_is("tbl_df") |> expect_named(c("season", "number", "title", "number_abs", "overview", "rating", - "votes", "comment_count", "first_aired", "updated_at", "runtime", - "trakt", "tvdb", "imdb", "tmdb")) |> + "votes", "comment_count", "first_aired", "updated_at", "available_translations", + "runtime", "trakt", "tvdb", "imdb", "tmdb")) |> nrow() |> expect_equal(1) })