maguro/
lib.rs

1//! # maguro
2//!
3//! An async library for downloading and streaming media, with
4//! out-of-the-box support for YouTube.
5//!
6//! ## Example
7//!
8//! ```
9//! use maguro;
10//! use tokio::fs::OpenOptions;
11//!
12//! // ...
13//!
14//! // Get our video information and location the first format
15//! // available.
16//! let video_info = maguro::get_video_info("VfWgE7D1pYY").await?;
17//! let format = video_info.all_formats().first().cloned()?;
18//!
19//! // Open an asynchronous file handle.
20//! let mut output = OpenOptions::new()
21//!     .read(false)
22//!     .write(true)
23//!     .create(true)
24//!     .open("maguro.mp4")
25//!     .await?;
26//!
27//! // Download the video.
28//! format.download(&mut output).await?;
29//! ```
30
31use ::serde::{Deserialize, Serialize};
32use hyper::{
33    body::{self, HttpBody},
34    Client,
35};
36use hyper_tls::HttpsConnector;
37use std::{
38    error,
39    fmt::{self, Display},
40    str,
41    time::Duration,
42};
43use tokio::{fs::File, io::AsyncWriteExt};
44
45pub mod serde;
46
47/// Endpoint to request against.
48const ENDPOINT_URI: &'static str = "https://www.youtube.com/get_video_info";
49
50/// Form an endpoint URI for the given video ID.
51fn endpoint_from_id<T: Display>(id: T) -> String {
52    format!("{}?video_id={}", ENDPOINT_URI, id)
53}
54
55#[derive(Serialize, Deserialize, Clone, Debug)]
56/// Describes a single streaming format for a YouTube video.
57pub struct Format {
58    itag: u32,
59    url: String,
60
61    // Width and height are optional in the case formats
62    // are audio only.
63    width: Option<u32>,
64    height: Option<u32>,
65
66    #[serde(rename = "mimeType")]
67    mime_type: String,
68
69    #[serde(
70        default,
71        rename = "contentLength",
72        deserialize_with = "serde::u32::from_str_option"
73    )]
74    // A stream may not have a defined size.
75    content_length: Option<u32>,
76
77    quality: String,
78    fps: Option<u32>,
79
80    #[serde(
81        default,
82        rename = "approxDurationMs",
83        deserialize_with = "serde::duration::from_millis_option"
84    )]
85    // A stream may not have a defined length.
86    approx_duration: Option<Duration>,
87}
88
89impl Format {
90    /// Whether the given streaming format is a video.
91    pub fn is_video(&self) -> bool {
92        match self.width {
93            Some(_) => true,
94            None => false,
95        }
96    }
97
98    pub fn itag(&self) -> u32 {
99        self.itag
100    }
101
102    pub fn size(&self) -> Option<u32> {
103        self.content_length.clone()
104    }
105
106    /// Read the entire YouTube video into a vector.
107    pub async fn to_vec(&self) -> Result<Vec<u8>, Box<dyn error::Error + Send + Sync>> {
108        self.to_vec_callback(|_| Ok(())).await
109    }
110
111    /// Downloads the entire YouTube video in chunks with the given closure.
112    /// On receipt of a new chunk of bytes, it calls the closure.
113    pub async fn to_vec_callback<T>(
114        &self,
115        on_chunk: T,
116    ) -> Result<Vec<u8>, Box<dyn error::Error + Send + Sync>>
117    where
118        T: Fn(Vec<u8>) -> Result<(), Box<dyn error::Error + Send + Sync>>,
119    {
120        let https = HttpsConnector::new();
121        let client = Client::builder().build::<_, hyper::Body>(https);
122
123        let mut res = client.get(self.url.parse().unwrap()).await.unwrap();
124
125        let mut v: Vec<u8> = Vec::new();
126        while let Some(chunk) = res.body_mut().data().await {
127            let as_bytes: Vec<u8> = chunk?.iter().cloned().collect();
128            on_chunk(as_bytes.clone())?;
129            v.extend(as_bytes.iter());
130        }
131        Ok(v)
132    }
133
134    /// Downloads the entire YouTube video into a `File`.
135    pub async fn download(
136        &self,
137        dest: &mut File,
138    ) -> Result<(), Box<dyn error::Error + Send + Sync>> {
139        let https = HttpsConnector::new();
140        let client = Client::builder().build::<_, hyper::Body>(https);
141
142        let mut res = client.get(self.url.parse().unwrap()).await.unwrap();
143
144        while let Some(chunk) = res.body_mut().data().await {
145            dest.write(&chunk?).await?;
146        }
147
148        Ok(())
149    }
150}
151
152impl Display for Format {
153    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154        write!(
155            f,
156            "itag: {:03}\tQuality: {}\tMime Type: {}",
157            self.itag, self.quality, self.mime_type
158        )
159    }
160}
161
162#[derive(Serialize, Deserialize, Clone, Debug)]
163/// The set of sources available to download a YouTube
164/// video with.
165pub struct StreamingData {
166    #[serde(
167        rename = "expiresInSeconds",
168        deserialize_with = "serde::duration::from_secs"
169    )]
170    expires_in_seconds: Duration,
171
172    // In the case of streams, the `formats` field is empty.
173    formats: Option<Vec<Format>>,
174
175    #[serde(rename = "adaptiveFormats")]
176    adaptive_formats: Vec<Format>,
177}
178
179#[derive(Serialize, Deserialize, Clone, Debug)]
180/// Details about some YouTube video.
181pub struct VideoDetails {
182    #[serde(rename = "videoId")]
183    video_id: String,
184
185    title: String,
186
187    #[serde(rename = "author")]
188    author: String,
189
190    #[serde(
191        rename = "lengthSeconds",
192        deserialize_with = "serde::duration::from_secs_option"
193    )]
194    approx_length: Option<Duration>,
195
196    #[serde(rename = "viewCount", deserialize_with = "serde::u32::from_str")]
197    views: u32,
198
199    #[serde(rename = "isPrivate")]
200    private: bool,
201
202    #[serde(rename = "isLiveContent")]
203    live: bool,
204}
205
206impl VideoDetails {
207    pub fn id(&self) -> String {
208        self.video_id.clone()
209    }
210}
211
212#[derive(Deserialize, Clone, Debug)]
213/// YouTube get_video_info response.
214pub struct InfoResponse {
215    #[serde(rename = "streamingData")]
216    streaming_data: StreamingData,
217
218    #[serde(rename = "videoDetails")]
219    video_details: VideoDetails,
220}
221
222impl InfoResponse {
223    pub fn formats(&self) -> Option<Vec<Format>> {
224        self.streaming_data.formats.clone()
225    }
226
227    pub fn adaptive_formats(&self) -> Vec<Format> {
228        self.streaming_data.adaptive_formats.clone()
229    }
230
231    pub fn details(&self) -> VideoDetails {
232        self.video_details.clone()
233    }
234
235    /// Returns a vector of all formats available for the given
236    /// video.
237    pub fn all_formats(&self) -> Vec<Format> {
238        if let Some(fmts) = self.formats() {
239            return fmts
240                .iter()
241                .cloned()
242                .chain(self.adaptive_formats().iter().cloned())
243                .collect();
244        }
245        self.adaptive_formats().iter().cloned().collect()
246    }
247}
248
249#[derive(Serialize, Deserialize, Clone, Debug)]
250/// Wrapper describing the outermost URL-encoded parameters of
251/// a get_video_info response.
252struct InfoWrapper {
253    pub player_response: String,
254}
255
256/// Acquires the [InfoResponse] struct for a given video ID.
257pub async fn get_video_info(id: &str) -> Result<InfoResponse, Box<dyn error::Error>> {
258    let https = HttpsConnector::new();
259    let client = Client::builder().build::<_, hyper::Body>(https);
260
261    let mut res = client
262        .get(endpoint_from_id(id).parse().unwrap())
263        .await
264        .unwrap();
265    let body = body::to_bytes(res.body_mut()).await.unwrap();
266
267    let stream_info: InfoResponse =
268        serde_json::from_str(&serde_urlencoded::from_bytes::<InfoWrapper>(&body)?.player_response)?;
269    Ok(stream_info)
270}