jw_client 1.0.0

A simple API wrapper for the JW Player media management API. List or stream library into native Rust structs and download renditions.
Documentation
use crate::{renditions::RenditionsPage, MediaPage};
use reqwest::header::{ACCEPT, AUTHORIZATION};
use std::{cmp::min, io::Cursor};
use tokio::{fs::File, io, sync::mpsc};
use tokio_stream::Stream;
use urlencoding::encode;

///Client struct used to store credentials
#[derive(Default, Debug, Clone, PartialEq)]
pub struct Client {
    token: String,
    site_id: String,
}

impl Client {
    ///Creates a new client instance and stores the property credentials.
    pub fn new(token: &str, site_id: &str) -> Self {
        Self {
            token: token.to_string(),
            site_id: site_id.to_string(),
        }
    }

    ///Returns a Renditions struct containing all of the necessary video rendition info.
    pub async fn get_renditions(&self, media_id: &str) -> Result<Option<RenditionsPage>, String> {
        let endpoint = format!(
            "https://api.jwplayer.com/v2/sites/{}/media/{}/media_renditions/",
            self.site_id, media_id
        );
        let client = reqwest::Client::new();
        let response = client
            .get(endpoint)
            .header(ACCEPT, "application/json")
            .header(AUTHORIZATION, &self.token)
            .send()
            .await;
        match response {
            Ok(data) => {
                let str_data = data.text().await.expect("Error getting API response");
                let media_renditions: RenditionsPage = serde_json::from_str(&str_data.as_str())
                    .expect("Error deserializing JSON response");
                Ok(Some(media_renditions))
            }
            Err(_) => Err("Error getting video renditions".to_string()),
        }
    }

    ///Returns items from the library filtered against a vector of tags.
    ///An optional chunk size can be provided to change the limit of the items per page.
    ///Default chunk size is 10 and max truncated chunk size is 10,000.
    pub async fn get_library_excluding_tags(
        &self,
        tags: Vec<&str>,
        chunk_size: Option<i32>,
    ) -> Result<Option<MediaPage>, reqwest::Error> {
        let query = match tags.len() {
            0 => String::new(),
            1 => format!("tags: NOT ({})", tags[0]),
            _ => format!("tags: NOT ({})", tags.join(" OR ")),
        };
        let page_length = min(chunk_size.unwrap_or(10), 10000).clone();
        println!("{}", query);
        let endpoint = format!(
            "https://api.jwplayer.com/v2/sites/{}/media/?&page_length={}&q={}&sort=created:dsc",
            self.site_id, page_length, query
        );
        let client = reqwest::Client::new();
        let response = client
            .get(endpoint)
            .header(ACCEPT, "application/json")
            .header(AUTHORIZATION, &self.token)
            .send()
            .await;

        match response {
            Ok(data) => {
                let str_data = data.text().await.expect("Error getting API response");
                let media: MediaPage = serde_json::from_str(&str_data.as_str())
                    .expect("Error deserializing JSON response");
                Ok(Some(media))
            }
            Err(e) => Err(e),
        }
    }

    ///Returns items from the library filtered by a vector of tags.
    ///An optional chunk size can be provided to change the limit of the items per page.
    ///Default chunk size is 10 and max truncated chunk size is 10,000.
    pub async fn get_library_by_tags(
        &self,
        tags: Vec<&str>,
        chunk_size: Option<i32>,
    ) -> Result<Option<MediaPage>, reqwest::Error> {
        let query = match tags.len() {
            0 => String::new(),
            1 => format!("tags: ({})", tags[0]),
            _ => format!("tags: ({})", tags.join(" OR ")),
        };
        let page_length = min(chunk_size.unwrap_or(10), 10000).clone();

        let endpoint = format!(
            "https://api.jwplayer.com/v2/sites/{}/media/?&page_length={}&q={}&sort=created:dsc",
            self.site_id, page_length, query
        );
        let client = reqwest::Client::new();
        let response = client
            .get(endpoint)
            .header(ACCEPT, "application/json")
            .header(AUTHORIZATION, &self.token)
            .send()
            .await;

        match response {
            Ok(data) => {
                let str_data = data.text().await.expect("Error getting API response");
                let media: MediaPage = serde_json::from_str(&str_data.as_str())
                    .expect("Error deserializing JSON response");
                Ok(Some(media))
            }
            Err(e) => Err(e),
        }
    }

    ///Returns most recent items from the library.
    ///An optional chunk size can be provided to change the limit of the items per page.
    ///Default chunk size is 10 and max truncated chunk size is 10,000.
    pub async fn get_library(
        &self,
        chunk_size: Option<i32>,
    ) -> Result<Option<MediaPage>, reqwest::Error> {
        let page_length = min(chunk_size.unwrap_or(10), 10000).clone();

        let endpoint = format!(
            "https://api.jwplayer.com/v2/sites/{}/media/?page_length={}&sort=created:dsc",
            self.site_id, page_length
        );
        let client = reqwest::Client::new();
        let response = client
            .get(endpoint)
            .header(ACCEPT, "application/json")
            .header(AUTHORIZATION, &self.token)
            .send()
            .await;

        match response {
            Ok(data) => {
                let str_data = data.text().await.expect("Error getting API response");
                let media: MediaPage = serde_json::from_str(&str_data.as_str())
                    .expect("Error deserializing JSON response");
                Ok(Some(media))
            }
            Err(e) => Err(e),
        }
    }

    ///Streams the entire library in chunks until the entire library is iterated over.
    ///An optional chunk size can be provided to change the limit of the items per page.
    ///Default chunk size is 10 and max truncated chunk size is 10,000.
    ///Additonally, an optional query string can be passed into the fuction to query.
    ///Check out the JW documentation for [all avialable JW parameters](https://docs.jwplayer.com/platform/reference/building-a-request#query-parameter-q).
    pub async fn get_library_stream(
        &self,
        chunk_size: Option<i32>,
        query: Option<String>,
    ) -> impl Stream<Item = Result<MediaPage, reqwest::Error>> {
        let (tx, rx) = mpsc::channel(10);

        tokio::spawn({
            let this = self.clone();
            let page_length = min(chunk_size.unwrap_or(10), 10000).clone();
            async move {
                let mut start_date = "2000-01-01".to_string();
                let end_date = "3000-01-01".to_string();
                loop {
                    match this
                        .stream_helper(&start_date, &end_date, &page_length, query.clone())
                        .await
                    {
                        Ok(Some(media_list)) if !media_list.media.is_empty() => {
                            if tx.send(Ok(media_list.clone())).await.is_err() {
                                break;
                            }
                            if let Some(last_item) = media_list.media.last() {
                                let new_start_date = encode(&last_item.created[..19]).to_string();

                                if new_start_date == start_date
                                    || media_list.total < media_list.page_length
                                {
                                    break;
                                }
                                start_date = new_start_date;
                            }
                        }
                        Ok(_) => {
                            break;
                        }
                        Err(e) => {
                            let _ = tx.send(Err(e)).await;
                            break;
                        }
                    }
                }
            }
        });

        tokio_stream::wrappers::ReceiverStream::new(rx)
    }

    //Helper function for returning a library chunk
    async fn stream_helper(
        &self,
        start_date: &str,
        end_date: &str,
        page_length: &i32,
        query: Option<String>,
    ) -> Result<Option<MediaPage>, reqwest::Error> {
        let full_query = match query {
            Some(q) => {
                format!("( created:[{} TO {}] ) AND ( {} )", start_date, end_date, q)
            }
            None => {
                format!("created:[{} TO {}]", start_date, end_date)
            }
        };
        let endpoint = format!(
            "https://api.jwplayer.com/v2/sites/{}/media/?page_length={}&q={}&sort=created:asc",
            self.site_id, page_length, full_query
        );
        let client = reqwest::Client::new();
        let response = client
            .get(endpoint)
            .header(ACCEPT, "application/json")
            .header(AUTHORIZATION, &self.token)
            .send()
            .await;

        match response {
            Ok(data) => {
                let str_data = data.text().await.expect("Error getting API response");
                let media: MediaPage = serde_json::from_str(&str_data.as_str())
                    .expect("Error deserializing JSON response");
                Ok(Some(media))
            }
            Err(e) => Err(e),
        }
    }

    ///Downloads an indicated media id rendition to a desired path.
    pub async fn download(&self, media_id: &str, path: &str) {
        let renditions = self.get_renditions(media_id).await;
        if let Ok(Some(data)) = renditions {
            let url = data
                .media_renditions
                .last()
                .expect("No rendition entries in the media")
                .delivery_url
                .as_str();
            let resp = reqwest::get(url).await.expect("URL Request Failed");
            let body = resp.bytes().await.expect("Invalid Video Data");

            // Wrap body in a Cursor to implement AsyncRead
            let mut body_reader = Cursor::new(body);

            let path_str = format!("{}/{}.mp4", path, data.media_renditions[0].id);
            let mut out = File::create(&path_str)
                .await
                .expect("Failed to Create File");

            // Copy content from body_reader to the file
            io::copy(&mut body_reader, &mut out)
                .await
                .expect("Failed to Download Video");
        }
    }

    ///Deletes a specified media object from the library. Use with caution as this action cannot be undone.
    pub async fn delete_meida(&self, media_id: &str) -> Result<(), String> {
        let endpoint = format!(
            "https://api.jwplayer.com/v2/sites/{}/media/{}/",
            self.site_id, media_id
        );
        let client = reqwest::Client::new();
        let response = client
            .delete(endpoint)
            .header(ACCEPT, "application/json")
            .header(AUTHORIZATION, &self.token)
            .send()
            .await;
        match response {
            Ok(_) => Ok(()),
            Err(_) => Err("Error deleting video".to_string()),
        }
    }
}