1use failure::Error;
2use oauth2::basic::BasicClient;
3use reqwest::{Client, get, RequestBuilder, StatusCode};
4use serde::Serialize;
5
6use crate::models::*;
7use crate::token::AuthToken;
8
9const SEARCH_URL: &str = "https://www.googleapis.com/youtube/v3/search";
10const LIST_PLAYLISTS_URL: &str = "https://www.googleapis.com/youtube/v3/playlists";
11const LIST_PLAYLIST_ITEMS_URL: &str = "https://www.googleapis.com/youtube/v3/playlistItems";
12
13#[derive(Debug, Clone)]
14pub(crate) struct YoutubeOAuth {
15 pub(crate) client_id: String,
16 pub(crate) client_secret: String,
17 pub(crate) client: BasicClient,
18 pub(crate) token: AuthToken,
19}
20
21#[derive(Debug, Clone)]
22pub struct YoutubeApi {
23 pub(crate) api_key: String,
24 pub(crate) oauth: Option<YoutubeOAuth>,
25 pub(crate) client: Client,
26}
27
28mod auth;
29
30impl YoutubeApi {
31 pub async fn get_video_info(id: &str) -> Result<VideoMetadata, failure::Error> {
32 let url = format!("https://www.youtube.com/get_video_info?video_id={}", id);
33 let res = get(&url).await?.error_for_status()?.text().await?;
34 let response: VideoMetadataResponse = serde_urlencoded::from_str(&res)?;
35 let metadata: VideoMetadata = serde_json::from_str(&response.player_response)?;
36
37 Ok(metadata)
38 }
39
40 pub fn new<S: Into<String>>(api_key: S) -> Self {
41 YoutubeApi {
42 api_key: api_key.into(),
43 oauth: None,
44 client: Client::new(),
45 }
46 }
47
48
49 pub async fn search(&self, search_request: SearchRequestBuilder) -> Result<SearchResponse, failure::Error> {
50 let request = search_request.build(&self.api_key);
51 let response = self.client.get(SEARCH_URL)
52 .query(&request)
53 .send()
54 .await?
55 .error_for_status()?
56 .json()
57 .await?;
58
59 Ok(response)
60 }
61
62 pub async fn list_playlists(&self, request: ListPlaylistsRequestBuilder) -> Result<ListPlaylistsResponse, failure::Error> {
63 let request = request.build();
64 let response = self.api_get(LIST_PLAYLISTS_URL, request)
65 .await?
66 .json()
67 .await?;
68
69 Ok(response)
70 }
71
72 pub async fn list_playlist_items(&self, request: ListPlaylistItemsRequestBuilder) -> Result<ListPlaylistItemsResponse, failure::Error> {
73 let request = request.build();
74 let response = self.api_get(LIST_PLAYLIST_ITEMS_URL, request)
75 .await?
76 .json()
77 .await?;
78
79 Ok(response)
80 }
81
82 async fn api_get<S: Into<String>, T: Serialize>(
83 &self,
84 url: S,
85 params: T,
86 ) -> Result<reqwest::Response, Error> {
87 let req = self.client.get(&url.into()).query(¶ms);
88 if let Some(oauth) = self.oauth.as_ref() {
89 if oauth.token.requires_new_token().await {
90 oauth.token.refresh(&oauth.client).await?;
91 }
92 let res = req
93 .try_clone()
94 .unwrap()
95 .bearer_auth(oauth.token.get_auth_header().await?)
96 .send()
97 .await?
98 .error_for_status();
99 if let Err(err) = res {
100 self.retry_request(req, err, oauth).await
101 } else {
102 let res = res.unwrap();
103 Ok(res)
104 }
105 } else {
106 let res = req.send().await?.error_for_status()?;
107 Ok(res)
108 }
109 }
110
111 async fn retry_request(
112 &self,
113 req: RequestBuilder,
114 err: reqwest::Error,
115 oauth: &YoutubeOAuth,
116 ) -> Result<reqwest::Response, Error> {
117 if let Some(StatusCode::UNAUTHORIZED) = err.status() {
118 oauth.token.refresh(&oauth.client).await?;
119 let res = req
120 .bearer_auth(oauth.token.get_auth_header().await?)
121 .send()
122 .await?
123 .error_for_status()?;
124 Ok(res)
125 } else {
126 Err(err.into())
127 }
128 }
129}
130
131#[cfg(test)]
132mod test {
133 use crate::models::SearchRequestBuilder;
134 use crate::YoutubeApi;
135
136 fn create_api() -> YoutubeApi {
137 YoutubeApi::new(env!("YOUTUBE_API_KEY"))
138 }
139
140 #[tokio::test]
141 async fn get_video_info() {
142 let video_ids = vec![
143 "yfqTCWepx4U",
144 "ZGIfJHeZKKE",
145 "btecuyQKH-E",
146 "uM7JjfHDuFM",
147 "BgWpK28dt6I",
148 "8xe6nLVXEC0",
149 "O3WKbJLai1g"
150 ];
151 for video_id in video_ids {
152 let metadata = YoutubeApi::get_video_info(video_id).await;
153 if metadata.is_err() {
154 println!("{:?}", metadata);
155 }
156 assert!(metadata.is_ok());
157 }
158 }
159
160 #[tokio::test]
161 async fn search_should_work() {
162 let api = create_api();
163 let request = SearchRequestBuilder {
164 query: Some(String::from("Don't stay in school")),
165 ..SearchRequestBuilder::default()
166 };
167
168 let res = api.search(request).await;
169
170 println!("{:?}", res);
171 assert!(res.is_ok())
172 }
173}