Tracker for TV series using theTVdb.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

286 lines
7.6KB

  1. library(tidyverse)
  2. library(httr)
  3. library(rvest)
  4. library(DBI)
  5. library(RSQLite)
  6. options(scipen = 999)
  7. #' Authenticate to theTVdbAPI
  8. #'
  9. #' @param username User name
  10. #' @param userkey User key
  11. #' @param apikey API key
  12. #' @return A token for theTVdbAPI
  13. authenticate <- function(username, userkey, apikey)
  14. {
  15. list(apikey = apikey,
  16. userkey = userkey,
  17. username = username) -> body
  18. POST("https://api.thetvdb.com/login",
  19. config = add_headers(ContentType = "application/json",
  20. Accept = "application/json"),
  21. body = body,
  22. encode = "json") %>%
  23. content %>%
  24. .$token
  25. }
  26. #' Search for a show
  27. #'
  28. #' @param db A db object created with `openDb`
  29. #' @param search A search query
  30. #' @return The search matches
  31. search <- function(db, search)
  32. {
  33. GET(str_c("https://api.thetvdb.com/search/series?name=", search),
  34. config = add_headers(Accept = "application/json",
  35. Authorization = str_c("Bearer ", db$token))) %>%
  36. content %>%
  37. .$data -> res
  38. tibble(id = res %>% map_int("id"),
  39. name = res %>% map_chr("seriesName"))
  40. }
  41. help <- function(db)
  42. {
  43. print("Pipe db to the following verbs\n
  44. - list for a list of shows\n
  45. - search to search a show\n
  46. - addSeries to add a show\n
  47. - removeSeries to remove a show\n
  48. - watch to mark an episode as watched\n
  49. - watchTill to mark all previous episodes as watched\n
  50. - nextUp to see all available and upcoming episodes\n
  51. - toWatch to see all available episodes\n
  52. \n
  53. Then finally closeDb to save the changes to the database")
  54. }
  55. #' Get the name of a series from its id
  56. #'
  57. #' @param series Series id
  58. #' @param token The API token
  59. #' @return The series name
  60. getSeries <- function(series, token)
  61. {
  62. GET(str_c("https://api.thetvdb.com/series/", series),
  63. config = add_headers(Accept = "application/json",
  64. Authorization = str_c("Bearer ", token))) %>%
  65. content -> content
  66. content$data$seriesName
  67. }
  68. #' Get the details for an episode from its id
  69. #'
  70. #' @param episode Episode id
  71. #' @param token The API token
  72. #' @return The airing date and series id for the episode
  73. getEpisode <- function(episode, token)
  74. {
  75. GET(str_c("https://api.thetvdb.com/episodes/", episode),
  76. config = add_headers(Accept = "application/json",
  77. Authorization = str_c("Bearer ", token))) %>%
  78. content -> content
  79. content$data -> data
  80. tibble(id = data$id,
  81. date = data$firstAired,
  82. series = data$seriesId)
  83. }
  84. #' Get the list of episodes for a series
  85. #'
  86. #' @param series The series id
  87. #' @param token The API token
  88. #' @param page The page of the results to get
  89. #' @return The list of episodes with season and episode numbers, the name, and the airing date
  90. getEpisodes <- function(series, token, page = 1)
  91. {
  92. GET(str_c("https://api.thetvdb.com/series/", series, "/episodes?page=", page),
  93. config = add_headers(Accept = "application/json",
  94. Authorization = str_c("Bearer ", token))) %>%
  95. content -> content
  96. content$data %>%
  97. discard(~ .x$episodeName %>% is.null)-> data
  98. tibble(id = data %>% map_int("id"),
  99. season = data %>% map_int("airedSeason"),
  100. episode = data %>% map_int("airedEpisodeNumber"),
  101. name = data %>% map_chr("episodeName"),
  102. date = data %>% map_chr("firstAired")) %>%
  103. filter(season > 0) %>%
  104. filter(date != "") %>%
  105. mutate(date = date %>% as.POSIXct) %>%
  106. arrange(season, episode) -> res
  107. if (!content$links$`next` %>% is.null)
  108. res %>%
  109. bind_rows(getEpisodes(series, token, content$links$`next`))
  110. else
  111. res
  112. }
  113. #' Open or create the database
  114. #'
  115. #' @param username User name to use to create the db
  116. #' @param userkey User key to use to create the db
  117. #' @param apikey Api key to use to create the db
  118. #' @return An initialized db objet
  119. openDb <- function(username = NULL, userkey = NULL, apikey = NULL)
  120. {
  121. dbConnect(RSQLite::SQLite(), "~/tv.db") -> db
  122. if (dbListTables(db) %>% length == 0)
  123. {
  124. if (any(list(username, userkey, apikey) %>% map_lgl(is.null)))
  125. stop("Please provide username, userkey, and apikey")
  126. list(db = db,
  127. credentials = data.frame(username = username,
  128. userkey = userkey,
  129. apikey = apikey),
  130. token = authenticate(username, userkey, apikey),
  131. shows = data.frame(id = integer(0), name = character(0)),
  132. watched = data.frame(id = integer(0)))
  133. } else {
  134. credentials = dbGetQuery(db, "SELECT * from credentials")
  135. list(db = db,
  136. credentials = credentials,
  137. token = authenticate(credentials$username, credentials$userkey, credentials$apikey),
  138. shows = dbGetQuery(db, "SELECT * FROM shows"),
  139. watched = dbGetQuery(db, "SELECT * FROM watched"))
  140. }
  141. }
  142. #' Save and close the database
  143. #'
  144. #' Writes all information and disconnects the database
  145. #'
  146. #' @param db The db object to use
  147. closeDb <- function(db)
  148. {
  149. dbWriteTable(db$db, "credentials", db$credentials, overwrite = T)
  150. dbWriteTable(db$db, "shows", db$shows, overwrite = T)
  151. dbWriteTable(db$db, "watched", db$watched, overwrite = T)
  152. dbDisconnect(db$db)
  153. }
  154. #' Add a series
  155. #'
  156. #' @param db The db object to use
  157. #' @param id The series id to add
  158. #' @return The db object with the added series
  159. addSeries <- function(db, id)
  160. {
  161. db$shows <- db$shows %>%
  162. add_row(id = id, name = getSeries(id, db$token)) %>%
  163. distinct
  164. db
  165. }
  166. #' Add a series
  167. #'
  168. #' @param db The db object to use
  169. #' @param showId The series id to remove
  170. #' @return The db object with the removed series
  171. removeSeries <- function(db, showId)
  172. {
  173. db$shows <- db$shows %>%
  174. filter(id != showId)
  175. db$watched <- db$watched %>%
  176. anti_join(getEpisodes(showId, db$token))
  177. db
  178. }
  179. #' Watch an episode
  180. #'
  181. #' @param db The db object to use
  182. #' @param episode The episode to watch
  183. #' @return The db object with the watched episode
  184. watchEp <- function(db, episode)
  185. {
  186. db$watched <- db$watched %>%
  187. add_row(id = episode) %>%
  188. distinct
  189. db
  190. }
  191. #' Watch multiple episodes
  192. #'
  193. #' @param db The db object to use
  194. #' @param episodes A vector of episodes
  195. #' @return The db object with the watch episodes
  196. watch <- function(db, episodes) reduce(episodes, watchEp, .init = db)
  197. #' Watch all episodes from a series up to the given one
  198. #'
  199. #' @param db The db object to use
  200. #' @param episode The episode to watch up to
  201. #' @return The db object with the removed episodes
  202. watchTill <- function(db, episode)
  203. {
  204. ep <- getEpisode(episode, db$token)
  205. db$watched <- db$watched %>%
  206. bind_rows(getEpisodes(ep$series, db$token) %>%
  207. anti_join(db$watched) %>%
  208. filter(date <= ep$date) %>%
  209. select(id)) %>%
  210. distinct
  211. db
  212. }
  213. #' Show the upcoming episodes for the series in the db
  214. #'
  215. #' @param db The db object to use
  216. #' @return The next episodes to watch, sorted by airing date
  217. upNext <- function(db)
  218. {
  219. tibble(showId = db$shows$id) %>%
  220. mutate(data = showId %>% map(getEpisodes, token = db$token)) %>%
  221. unnest(data) %>%
  222. bind_rows %>%
  223. anti_join(db$watched) %>%
  224. inner_join(db$shows, by = c("showId" = "id")) %>%
  225. select(show = name.y,
  226. id,
  227. season,
  228. episode,
  229. name = name.x,
  230. date) %>%
  231. arrange(date)
  232. }
  233. #' Show only the available episodes not yet watched
  234. #'
  235. #' @param db The db object to use
  236. #' @return The available episodes to watch
  237. toWatch <- function(db)
  238. {
  239. db %>%
  240. upNext %>%
  241. filter(date < lubridate::today())
  242. }
  243. ################################################################################
  244. openDb() -> db
  245. db %>% toWatch
  246. db %>% upNext %>% print(n = Inf)
  247. db %>%
  248. watch(db %>% toWatch %>% pull(id)) %>%
  249. closeDb