youtube_api/api/
mod.rs

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(&params);
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}