From 7cafdec6096512aeec9da17d348c5a0c0d9988bc Mon Sep 17 00:00:00 2001 From: Lukas Burk Date: Fri, 11 Aug 2023 02:55:06 +0200 Subject: [PATCH 1/9] Update internal data --- data-raw/get-data-internal.R | 5 +++-- data/trakt_certifications.rda | Bin 456 -> 456 bytes data/trakt_countries.rda | Bin 2456 -> 2464 bytes data/trakt_genres.rda | Bin 664 -> 664 bytes data/trakt_languages.rda | Bin 1836 -> 1880 bytes data/trakt_networks.rda | Bin 25016 -> 36456 bytes 6 files changed, 3 insertions(+), 2 deletions(-) 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 769d3e6dad5ce16a77c881d03010cb04b05291dc..93b67ca57d4980a09ec7119fcb801aeb2f8a4725 100644 GIT binary patch delta 401 zcmV;C0dD@t1IPoAEq`p1sKSpqtoaRmvj1J-ve&ZvA^4y#b;XuxIlXU0AA!=K;4sP* z5eGbt$h-|qfLZuT@xgZe*W|3<@roqtpt^P7W7bf`49Y6vd07ZAEKBKI+j#Jrwtfwk`kX;p^6`4?pk|;NgMEk4^O%7LPS~Qywn*nr$ zZu+S$GW@%vS$~wxYH?60oZM6)fLx?%S3OfdMwo<8iGc<|3O;GZ|I;xmHH?|%u_Kro z15;k3q1nlHIM(N=Y5;;Ls{jef8``P&+YykE{9Qa{&ru$sld0T74yT_eflhm0t6H4Y vc#k-MCaWdgQNGL1FDZ2&YmufrMq0APaxaTnEB4sI00sa6RF!q6ldu68aY)Ep delta 401 zcmV;C0dD@t1IPoAEq`G=vmD26`+jvH)%3kA!`O1`y|%D#5p;lMzKsmc^BMUXc)q}I z0s}4ROQS`c~4BgB` z?{WkfaTTv#wT4jlJ#29p!tJ+*v3b!X<#`VV?DO;iRlfSK#k;|^u>vAwV;yhVg>Ob& z>06b@yw^-o+*+Sjj=LX@Bi?6(O&)zVY&j6Tw+Z~6yWA}!k0c#Q04dz21$S~X6Z*}N z$hbe-#@MVEZht5~phcJdw<1PeTs>g1YV0L{#I>TP#ALz}Xk(~exvU78L&jz2A64-e zR(Ja{E%FzENL|m1Ux-X^yNg}{6QUW95fKq_c;rmdm;-%@uAiL&w~Wu00sa6GcRI&ldu68o@2wK diff --git a/data/trakt_countries.rda b/data/trakt_countries.rda index ea2a1446f309840c254ab8bead3ec4b88974141d..cc482ecfd7aa462c495399dfbcae93d4b40a8ec4 100644 GIT binary patch literal 2464 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 literal 2456 zcmV;J31{~GH+ooF0004LBHlIv03iV!0000G&sfahIw%QRT>vQ&2UJ%gRpOV=m zVLY=O$8P(6bs^RCy)47f-(&pc#v37ud8|M=**0`S4z@1LEJR!7(SX$3eb?s#nbtV{ zS13Fwe25*)4`4!`|3nCoHWmui*2t+uBeX{FB zm{yJp-=+X_o~_^)LdQ|u*xap6E9f%S=6^&9t@{Xbt`e0~HMHxfg8*p`EZD)1n=Gd+hH zyXCF2xob{*VJFU1jk57*s5?BA_I^h|^pcZAYoOuNqqY)q9jrO%Owpq0EbeHhmW^V5 z?V9{yddba%OTa%g$?*D3E0u)=#dooIdAE77kJ}^?UqDr0#Oo379*|Reyuu5AJdqF> z!Nk?s3@@|v6g#_aXwNmf+O==G=sbC+JD|d)09V1#HDoDN!!#arU7w3%9Yk}T1L zzHne?MbokQK7jxMXiHtg6+9t`E8~oLQpPaVC|KJ$B4^?;B~NRj3`2^^Kzp9fn?j8U z`?;x>Vn5zyvEV!=p5ODu)j?eU*w9Sl{%O4=eYrNUvPxuMd1Y^hLuhIcwu@A=1Z(Km zg;<8RX|}bs7m`ntP(vQF?Y@j@gN1sbZE`IJuVaV1knQ|A1xKy@H|w;rYCXEh<)3ce z?U>7%3)+g9MlXHlu&f4Z_@wqK+G*K+;GUMbpM9SJIrzxR-FmYp!_2dI-)=PxsxO(g zO65-M;DbPMTp{&_t+JjT$c{C&v{O9y1C4Z{fWS3CNQ;;0QOo)HZ5fC3;O>@_*5zEvp_YAa)w$?(yY}`BW?1HVa%ZZWU=e5lp&P zUX~;%a&ce#<6lx|yxs=DD>q~Ii@>@m zKKb(v*9-x)`{(|Pu+0mV1$_Tv&Qj9G%7@5I+OcY{e*Z^y939%{2yCMRkc3;V`CTsg zV8l`VG%6Q{xPXF*Wgg!)2{4QE1amy_#Pc1j?<>JhVO)l1)xd7`jXwF7)LlZqR5dae zSPd7Etu-$5>8Yt6veTZoNM6q(p_{KO+~=U@2%|4$n-Y!A+t^v%eSXU;w2e8Mhsgej zXoS)s5P&UCkQ}?#p2r624e;AM(ih5ONz_49?k$}APoB(SJkB48hYP{8Fy;4(d+>3H-`L-0`8D^px4PqITG=1+&mEoNf3Wm5_=DMo0QBAd$Lq z_;@6q$U0j}0!(Ytf1%r_L_HyGPK{seTze5{1+N2gJ`Om#xV){Wsv;PZzxc!&>g|v) zFUHGYI}K7{hXLPT->?(@WcQZTGub|JlB34>k(!(TGR(CBjqeIURVsNcMqt#kM((!K z6F%_$cA6jntDpml!`!dM1;iR4N0|1;k~Sk06^`P6l8A=7^R}j1Hy>+{ zI0PGSvK*1BGSCP0{B-6yVTE^^ZpLpfEF&#Hp(9E~8-aqP zcr*ISj~YSxduqwOFvIXfuqWQ);37wiHg$nU#=> zTKvt_P+cSL6wEnUopEtV$vf{-!poD6K2P&l?IFRXv}G2J`tlhbl=h>nni_~@7MFo7 z32UZ^cB1I2R3sT=DaFaA*yQos(qiK~WwaOyQ9T(%eHFk2t)_%BNJP3AOTOOY3$kh9 zni#wP{z1Np#fQQWjl>pq_(t~7(S9RYrw0pOV#_s!4U{R?!M%ldfj96)*%PHjOza2uV-w(HCDim5l>+H(feX}5D*7g zLSO9pFK_rf&z3os@XbcFP6(wLZtNwfZUe+{!>UR;tX2I0>`?*7T}>fjyTGHns;i01 z3$xN+^yt5 WbN~P@KD6RKFb#_W000000a;pMAGsd@ diff --git a/data/trakt_genres.rda b/data/trakt_genres.rda index a89f8994111dc48c8ec8600300fb7253327939af..a094d9cb7563927129608c97de0abaa4a28313cb 100644 GIT binary patch delta 610 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 OFb#_W000000a;oCD3`(j delta 1832 zcmV+@2iN%64y+E48Gk5k2JKw{DG>)$S0dzYQp%r_*(6~+vmD26`+jvH)%3kA!_ePj z{N=_QA<;u1IhWaS)Zr!+MTsB}?r-h>O@e<`Vrl-;B@N~+5@)Pq^b9Gp_i+^~WUjNYLumuf7NIvFg zQICDvc8do|t-k?Yuy1Dgx%NHc7CIm7&UP$lH4(=u?6^zAg2Q-4*Di&eEO zStic!josb^S5og1u$}^x(f=xU(UAW*_$^aRG9|Z$oL83xXl&k8!*3&G+-x+g5A~`h&(bs!_ zf@K+5PQ%-Egd*rs#R>S9|KGFPSgK=kPs zr4|lS;LZ+T1Xq1#Gy7eT9Xfg5-W2>T_F%JjIA;u@UC$SC-h)exZ!5}gIX6TGi|94q zTl+ewgU<~gZ^;rFSXXv#?-8^FkqWPck1 zE!`)6PxFiFt4y6>vpInXL3g(hz=tyWeXo_~*5rs*l2#j}G#*k3bJ_J!0TRO}=wYKO z<^BY)Vw)!%-p|B#1c?rz(9giwFc9cNT^3^l!zfagwAH&HP>wnzx!_IhS$anOe5vUYsm zD1$eZ9W4<7CR$O$NLqlB|H{t$?npRQ4-9J-FNj4L%{&7DJ~Et%Q`P1zuF*Q^&)V+4 z($!ZYc?n1ru5V@ALFaLAmh&xK&utWoCE(g<2l>%R%W5kmS|Q?>SNO)2a(|%gI6A#f z*`_8s68xUAQ`6A?>E^k{$LLp)r2z#X@ulW-Hke3hgQp+F9)Y5=%GNX30GqZ(!VH35 z)b5GT+wbq&ztn>{?bIIYy>xs{euac5lrISJmXM*DAreEeBgaz(^OQrDpdB?Wgc+H- zUzkV~HyDMcE9V(~4_=@geShO9Yiz7CsW&8l3(y4!=|@l|v4L~CCA#1^kp8@iCUMo;P&~UF4Rto z2vNO&kb~Oib2y>(97E>u9#S3@irZr+g7wtq-_;k?rY`=AznHx*Jb#;m1;eeS-T3|8 zkrl&VWnf0?q`08g)4O90Vn|wS2p|oDJWhUlBEG0d>Fcj+9dQJZw=a&>WHK(>g@#5F zcjxr=@l+971smE!$!IXdqk9;H$uH5PY=8v$kJ#Tp4xIUF-D6y%)nKVXRZlmQ!4jH&pouMV~;B1{;Xn_%RSz-ucjF_G|r!ft;gg!L9 z&8`vV16swiQc-4kp9y@`>^_zNAnt1Zd7d%^M$B$*lPj# z&rowEVU0t2;$J*`U#i?xLWoQBRmu&50V^7ZzN&b&F2` diff --git a/data/trakt_networks.rda b/data/trakt_networks.rda index 791c1762c5a20688401897dbc77e81c5a2b7724f..d16313c62f6c5f80a9120a74c6b48dcc09edd295 100644 GIT binary patch literal 36456 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 YoC37}2}}sFk3KLBivj=u00045T4EMd(EtDd literal 25016 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 Date: Fri, 11 Aug 2023 02:56:52 +0200 Subject: [PATCH 2/9] Basic move to httr2 --- DESCRIPTION | 4 +- NAMESPACE | 13 +- NEWS.md | 1 + R/api-oauth.R | 199 +++++++++++++++++++++++++++++++ R/api-requests.R | 78 ++++++++++++ R/api.R | 242 -------------------------------------- R/utils-misc.R | 31 +++-- R/zzz.R | 14 ++- codemeta.json | 52 +++++--- man/build_trakt_url.Rd | 3 +- man/get_token.Rd | 27 +++++ man/trakt_credentials.Rd | 71 ----------- man/trakt_get.Rd | 26 +--- man/trakt_get_token.Rd | 21 ---- tests/testthat/test-api.R | 46 ++++---- 15 files changed, 410 insertions(+), 418 deletions(-) create mode 100644 R/api-oauth.R create mode 100644 R/api-requests.R delete mode 100644 R/api.R create mode 100644 man/get_token.Rd delete mode 100644 man/trakt_credentials.Rd delete mode 100644 man/trakt_get_token.Rd 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..f50073d3 --- /dev/null +++ b/R/api-oauth.R @@ -0,0 +1,199 @@ +#' 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 (env_var != "") 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_dir <- rappdirs::user_cache_dir("tRakt") + cache_loc <- file.path(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_dir <- rappdirs::user_cache_dir("tRakt") + cache_loc <- file.path(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..429ed1b1 --- /dev/null +++ b/R/api-requests.R @@ -0,0 +1,78 @@ +#' 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() + + 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 = tempdir()) |> + httr2::req_user_agent( + paste0("tRakt (https://github.com/jemus42/tRakt)", + "via httr2 (https://github.com/r-lib/httr2)")) + + resp <- httr2::req_perform(req) + + httr2::resp_check_status(resp, info = url) + + resp <- httr2::resp_body_json(resp, simplifyVector = TRUE) + + # Kept from previous version, should be refactored at some point + if (identical(resp, "") | is_empty(resp)) { + 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/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/zzz.R b/R/zzz.R index f4df81ff..463734ef 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -1,13 +1,21 @@ # nocov start -.onLoad <- function(...) { - trakt_credentials() -} +# .onLoad <- function(...) { +# trakt_credentials() +# } #' The tRakt client ID for this particular app #' @keywords internal #' @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/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/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", { From 533cc89a14845cc38d5d9056143dcca837793f59 Mon Sep 17 00:00:00 2001 From: Lukas Burk Date: Fri, 11 Aug 2023 17:49:03 +0200 Subject: [PATCH 3/9] More migration --- R/api-oauth.R | 8 +++----- R/api-requests.R | 21 +++++++++++++++----- R/docs-common.R | 4 ++-- R/utils-third-party-APIs.R | 40 ++++++++++++++++---------------------- R/zzz.R | 7 ++++--- 5 files changed, 42 insertions(+), 38 deletions(-) diff --git a/R/api-oauth.R b/R/api-oauth.R index f50073d3..5cdf96ca 100644 --- a/R/api-oauth.R +++ b/R/api-oauth.R @@ -133,7 +133,7 @@ get_client_secret <- function() { get_client_id <- function() { env_var <- Sys.getenv("trakt_client_id", unset = "") - if (env_var != "") return(env_var) + if (nchar(env_var) == 64) return(env_var) tRakt_client_id } @@ -143,8 +143,7 @@ token_expired <- function(token) { } cache_token <- function(token) { - cache_dir <- rappdirs::user_cache_dir("tRakt") - cache_loc <- file.path(cache_dir, "token.rds") + cache_loc <- file.path(getOption("tRakt_cache_dir"), "token.rds") if (!dir.exists(cache_dir)) dir.create(cache_dir) @@ -155,8 +154,7 @@ cache_token <- function(token) { } clear_cached_token <- function() { - cache_dir <- rappdirs::user_cache_dir("tRakt") - cache_loc <- file.path(cache_dir, "token.rds") + cache_loc <- file.path(getOption("tRakt_cache_dir"), "token.rds") file.remove(cache_loc) } diff --git a/R/api-requests.R b/R/api-requests.R index 429ed1b1..6898bd0b 100644 --- a/R/api-requests.R +++ b/R/api-requests.R @@ -44,6 +44,18 @@ trakt_get <- function(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 @@ -53,16 +65,15 @@ trakt_get <- function(url) { ) |> httr2::req_auth_bearer_token(token = token$access_token) |> httr2::req_retry(max_tries = 3) |> - httr2::req_cache(path = tempdir()) |> - httr2::req_user_agent( - paste0("tRakt (https://github.com/jemus42/tRakt)", - "via httr2 (https://github.com/r-lib/httr2)")) + 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) + resp <- httr2::resp_body_json(resp, simplifyVector = TRUE, check_type = FALSE) # Kept from previous version, should be refactored at some point if (identical(resp, "") | is_empty(resp)) { 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/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 463734ef..a29d91a1 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -1,7 +1,8 @@ # nocov start -# .onLoad <- function(...) { -# trakt_credentials() -# } +.onLoad <- function(...) { + options("tRakt_cache_dir" = rappdirs::user_cache_dir("tRakt")) + options("tRakt_debug" = FALSE) +} #' The tRakt client ID for this particular app #' @keywords internal From 378bf6ffcaa86e422e9a29a5d15c1614f7f1f143 Mon Sep 17 00:00:00 2001 From: Lukas Burk Date: Fri, 11 Aug 2023 17:49:18 +0200 Subject: [PATCH 4/9] Start readjusting tests --- tests/testthat/setup-tests.R | 2 +- tests/testthat/test-media_stats.R | 18 +++++++++++------- tests/testthat/test-seasons.R | 5 ++--- tests/testthat/test-shows_next_episode.R | 4 ++-- 4 files changed, 16 insertions(+), 13 deletions(-) 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-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) }) From 1bd1266d42319a04557a1959a0091e70e0361553 Mon Sep 17 00:00:00 2001 From: Lukas Burk Date: Fri, 11 Aug 2023 17:51:10 +0200 Subject: [PATCH 5/9] Use API default "me" instead of option "trakt_username", defaulting to the current API OAuth user. --- R/user_collection.R | 2 +- R/user_comments.R | 2 +- R/user_history.R | 2 +- R/user_list_comments.R | 2 +- R/user_list_items.R | 2 +- R/user_lists.R | 4 ++-- R/user_network.R | 2 +- R/user_ratings.R | 2 +- R/user_stats.R | 2 +- R/user_summary.R | 2 +- R/user_watched.R | 2 +- R/user_watchlist.R | 2 +- 12 files changed, 13 insertions(+), 13 deletions(-) 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..f763db4a 100644 --- a/R/user_history.R +++ b/R/user_history.R @@ -28,7 +28,7 @@ #' start_at = "2015-12-24", end_at = "2015-12-28" #' ) #' } -user_history <- function(user = getOption("trakt_username"), +user_history <- function(user = "me", type = c("shows", "movies"), limit = 10L, start_at = NULL, end_at = NULL, extended = c("min", "full")) { 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) From bd94627020232f2acada1ea69f2bddba2a22f34c Mon Sep 17 00:00:00 2001 From: Lukas Burk Date: Fri, 20 Oct 2023 19:43:41 +0200 Subject: [PATCH 6/9] Try dependabot --- .github/workflows/dependabot.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .github/workflows/dependabot.yml diff --git a/.github/workflows/dependabot.yml b/.github/workflows/dependabot.yml new file mode 100644 index 00000000..8ac6b8c4 --- /dev/null +++ b/.github/workflows/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" From 498aa0fc3dfcf69bdb4b7c7e82e9b3b5a9c66550 Mon Sep 17 00:00:00 2001 From: Lukas Burk Date: Fri, 20 Oct 2023 19:47:47 +0200 Subject: [PATCH 7/9] ok --- .github/workflows/dependabot.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/dependabot.yml b/.github/workflows/dependabot.yml index 8ac6b8c4..36c9e8fc 100644 --- a/.github/workflows/dependabot.yml +++ b/.github/workflows/dependabot.yml @@ -1,3 +1,7 @@ +on: + push: + branches: [main, master] + version: 2 updates: - package-ecosystem: "github-actions" From 11cdfa4ae1ea2a0e951100431ab2285da0da7d71 Mon Sep 17 00:00:00 2001 From: Lukas Burk Date: Fri, 20 Oct 2023 19:51:13 +0200 Subject: [PATCH 8/9] ok --- .github/workflows/dependabot.yml | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 .github/workflows/dependabot.yml diff --git a/.github/workflows/dependabot.yml b/.github/workflows/dependabot.yml deleted file mode 100644 index 36c9e8fc..00000000 --- a/.github/workflows/dependabot.yml +++ /dev/null @@ -1,10 +0,0 @@ -on: - push: - branches: [main, master] - -version: 2 -updates: - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "monthly" From a5739a74db698e9c9bf692bb4a764af303be2ed0 Mon Sep 17 00:00:00 2001 From: Lukas Burk Date: Fri, 20 Oct 2023 19:52:16 +0200 Subject: [PATCH 9/9] 20231020195216 --- R/api-requests.R | 2 +- R/user_history.R | 8 +++++--- data-raw/get-data-episodes.R | 15 ++++++++++----- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/R/api-requests.R b/R/api-requests.R index 6898bd0b..a6535559 100644 --- a/R/api-requests.R +++ b/R/api-requests.R @@ -76,7 +76,7 @@ trakt_get <- function(url) { resp <- httr2::resp_body_json(resp, simplifyVector = TRUE, check_type = FALSE) # Kept from previous version, should be refactored at some point - if (identical(resp, "") | is_empty(resp)) { + if (identical(resp, "") | length(resp) == 0) { return(tibble()) } diff --git a/R/user_history.R b/R/user_history.R index f763db4a..668fe8de 100644 --- a/R/user_history.R +++ b/R/user_history.R @@ -29,7 +29,8 @@ #' ) #' } user_history <- function(user = "me", - type = c("shows", "movies"), + 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 = "me", 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/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" )) %>%