jw_client/
client.rs

1use crate::{renditions::RenditionsPage, MediaPage};
2use reqwest::header::{ACCEPT, AUTHORIZATION};
3use std::{cmp::min, io::Cursor};
4use tokio::{fs::File, io, sync::mpsc};
5use tokio_stream::Stream;
6use urlencoding::encode;
7
8///Client struct used to store credentials
9#[derive(Default, Debug, Clone, PartialEq)]
10pub struct Client {
11    token: String,
12    site_id: String,
13}
14
15impl Client {
16    ///Creates a new client instance and stores the property credentials.
17    pub fn new(token: &str, site_id: &str) -> Self {
18        Self {
19            token: token.to_string(),
20            site_id: site_id.to_string(),
21        }
22    }
23
24    ///Returns a Renditions struct containing all of the necessary video rendition info.
25    pub async fn get_renditions(&self, media_id: &str) -> Result<Option<RenditionsPage>, String> {
26        let endpoint = format!(
27            "https://api.jwplayer.com/v2/sites/{}/media/{}/media_renditions/",
28            self.site_id, media_id
29        );
30        let client = reqwest::Client::new();
31        let response = client
32            .get(endpoint)
33            .header(ACCEPT, "application/json")
34            .header(AUTHORIZATION, &self.token)
35            .send()
36            .await;
37        match response {
38            Ok(data) => {
39                let str_data = data.text().await.expect("Error getting API response");
40                let media_renditions: RenditionsPage = serde_json::from_str(&str_data.as_str())
41                    .expect("Error deserializing JSON response");
42                Ok(Some(media_renditions))
43            }
44            Err(_) => Err("Error getting video renditions".to_string()),
45        }
46    }
47
48    ///Returns items from the library filtered against a vector of tags.
49    ///An optional chunk size can be provided to change the limit of the items per page.
50    ///Default chunk size is 10 and max truncated chunk size is 10,000.
51    pub async fn get_library_excluding_tags(
52        &self,
53        tags: Vec<&str>,
54        chunk_size: Option<i32>,
55    ) -> Result<Option<MediaPage>, reqwest::Error> {
56        let query = match tags.len() {
57            0 => String::new(),
58            1 => format!("tags: NOT ({})", tags[0]),
59            _ => format!("tags: NOT ({})", tags.join(" OR ")),
60        };
61        let page_length = min(chunk_size.unwrap_or(10), 10000).clone();
62        println!("{}", query);
63        let endpoint = format!(
64            "https://api.jwplayer.com/v2/sites/{}/media/?&page_length={}&q={}&sort=created:dsc",
65            self.site_id, page_length, query
66        );
67        let client = reqwest::Client::new();
68        let response = client
69            .get(endpoint)
70            .header(ACCEPT, "application/json")
71            .header(AUTHORIZATION, &self.token)
72            .send()
73            .await;
74
75        match response {
76            Ok(data) => {
77                let str_data = data.text().await.expect("Error getting API response");
78                let media: MediaPage = serde_json::from_str(&str_data.as_str())
79                    .expect("Error deserializing JSON response");
80                Ok(Some(media))
81            }
82            Err(e) => Err(e),
83        }
84    }
85
86    ///Returns items from the library filtered by a vector of tags.
87    ///An optional chunk size can be provided to change the limit of the items per page.
88    ///Default chunk size is 10 and max truncated chunk size is 10,000.
89    pub async fn get_library_by_tags(
90        &self,
91        tags: Vec<&str>,
92        chunk_size: Option<i32>,
93    ) -> Result<Option<MediaPage>, reqwest::Error> {
94        let query = match tags.len() {
95            0 => String::new(),
96            1 => format!("tags: ({})", tags[0]),
97            _ => format!("tags: ({})", tags.join(" OR ")),
98        };
99        let page_length = min(chunk_size.unwrap_or(10), 10000).clone();
100
101        let endpoint = format!(
102            "https://api.jwplayer.com/v2/sites/{}/media/?&page_length={}&q={}&sort=created:dsc",
103            self.site_id, page_length, query
104        );
105        let client = reqwest::Client::new();
106        let response = client
107            .get(endpoint)
108            .header(ACCEPT, "application/json")
109            .header(AUTHORIZATION, &self.token)
110            .send()
111            .await;
112
113        match response {
114            Ok(data) => {
115                let str_data = data.text().await.expect("Error getting API response");
116                let media: MediaPage = serde_json::from_str(&str_data.as_str())
117                    .expect("Error deserializing JSON response");
118                Ok(Some(media))
119            }
120            Err(e) => Err(e),
121        }
122    }
123
124    ///Returns most recent items from the library.
125    ///An optional chunk size can be provided to change the limit of the items per page.
126    ///Default chunk size is 10 and max truncated chunk size is 10,000.
127    pub async fn get_library(
128        &self,
129        chunk_size: Option<i32>,
130    ) -> Result<Option<MediaPage>, reqwest::Error> {
131        let page_length = min(chunk_size.unwrap_or(10), 10000).clone();
132
133        let endpoint = format!(
134            "https://api.jwplayer.com/v2/sites/{}/media/?page_length={}&sort=created:dsc",
135            self.site_id, page_length
136        );
137        let client = reqwest::Client::new();
138        let response = client
139            .get(endpoint)
140            .header(ACCEPT, "application/json")
141            .header(AUTHORIZATION, &self.token)
142            .send()
143            .await;
144
145        match response {
146            Ok(data) => {
147                let str_data = data.text().await.expect("Error getting API response");
148                let media: MediaPage = serde_json::from_str(&str_data.as_str())
149                    .expect("Error deserializing JSON response");
150                Ok(Some(media))
151            }
152            Err(e) => Err(e),
153        }
154    }
155
156    ///Streams the entire library in chunks until the entire library is iterated over.
157    ///An optional chunk size can be provided to change the limit of the items per page.
158    ///Default chunk size is 10 and max truncated chunk size is 10,000.
159    ///Additonally, an optional query string can be passed into the fuction to query.
160    ///Check out the JW documentation for [all avialable JW parameters](https://docs.jwplayer.com/platform/reference/building-a-request#query-parameter-q).
161    pub async fn get_library_stream(
162        &self,
163        chunk_size: Option<i32>,
164        query: Option<String>,
165    ) -> impl Stream<Item = Result<MediaPage, reqwest::Error>> {
166        let (tx, rx) = mpsc::channel(10);
167
168        tokio::spawn({
169            let this = self.clone();
170            let page_length = min(chunk_size.unwrap_or(10), 10000).clone();
171            async move {
172                let mut start_date = "2000-01-01".to_string();
173                let end_date = "3000-01-01".to_string();
174                loop {
175                    match this
176                        .stream_helper(&start_date, &end_date, &page_length, query.clone())
177                        .await
178                    {
179                        Ok(Some(media_list)) if !media_list.media.is_empty() => {
180                            if tx.send(Ok(media_list.clone())).await.is_err() {
181                                break;
182                            }
183                            if let Some(last_item) = media_list.media.last() {
184                                let new_start_date = encode(&last_item.created[..19]).to_string();
185
186                                if new_start_date == start_date
187                                    || media_list.total < media_list.page_length
188                                {
189                                    break;
190                                }
191                                start_date = new_start_date;
192                            }
193                        }
194                        Ok(_) => {
195                            break;
196                        }
197                        Err(e) => {
198                            let _ = tx.send(Err(e)).await;
199                            break;
200                        }
201                    }
202                }
203            }
204        });
205
206        tokio_stream::wrappers::ReceiverStream::new(rx)
207    }
208
209    //Helper function for returning a library chunk
210    async fn stream_helper(
211        &self,
212        start_date: &str,
213        end_date: &str,
214        page_length: &i32,
215        query: Option<String>,
216    ) -> Result<Option<MediaPage>, reqwest::Error> {
217        let full_query = match query {
218            Some(q) => {
219                format!("( created:[{} TO {}] ) AND ( {} )", start_date, end_date, q)
220            }
221            None => {
222                format!("created:[{} TO {}]", start_date, end_date)
223            }
224        };
225        let endpoint = format!(
226            "https://api.jwplayer.com/v2/sites/{}/media/?page_length={}&q={}&sort=created:asc",
227            self.site_id, page_length, full_query
228        );
229        let client = reqwest::Client::new();
230        let response = client
231            .get(endpoint)
232            .header(ACCEPT, "application/json")
233            .header(AUTHORIZATION, &self.token)
234            .send()
235            .await;
236
237        match response {
238            Ok(data) => {
239                let str_data = data.text().await.expect("Error getting API response");
240                let media: MediaPage = serde_json::from_str(&str_data.as_str())
241                    .expect("Error deserializing JSON response");
242                Ok(Some(media))
243            }
244            Err(e) => Err(e),
245        }
246    }
247
248    ///Downloads an indicated media id rendition to a desired path.
249    pub async fn download(&self, media_id: &str, path: &str) {
250        let renditions = self.get_renditions(media_id).await;
251        if let Ok(Some(data)) = renditions {
252            let url = data
253                .media_renditions
254                .last()
255                .expect("No rendition entries in the media")
256                .delivery_url
257                .as_str();
258            let resp = reqwest::get(url).await.expect("URL Request Failed");
259            let body = resp.bytes().await.expect("Invalid Video Data");
260
261            // Wrap body in a Cursor to implement AsyncRead
262            let mut body_reader = Cursor::new(body);
263
264            let path_str = format!("{}/{}.mp4", path, data.media_renditions[0].id);
265            let mut out = File::create(&path_str)
266                .await
267                .expect("Failed to Create File");
268
269            // Copy content from body_reader to the file
270            io::copy(&mut body_reader, &mut out)
271                .await
272                .expect("Failed to Download Video");
273        }
274    }
275
276    ///Deletes a specified media object from the library. Use with caution as this action cannot be undone.
277    pub async fn delete_meida(&self, media_id: &str) -> Result<(), String> {
278        let endpoint = format!(
279            "https://api.jwplayer.com/v2/sites/{}/media/{}/",
280            self.site_id, media_id
281        );
282        let client = reqwest::Client::new();
283        let response = client
284            .delete(endpoint)
285            .header(ACCEPT, "application/json")
286            .header(AUTHORIZATION, &self.token)
287            .send()
288            .await;
289        match response {
290            Ok(_) => Ok(()),
291            Err(_) => Err("Error deleting video".to_string()),
292        }
293    }
294}