//! YouTube API request and response models mod ordering; mod paginator; pub mod richtext; pub use paginator::ContinuationEndpoint; pub use paginator::Paginator; use std::ops::Range; use chrono::{DateTime, Local, Utc}; use serde::{Deserialize, Serialize}; use crate::{error::Error, util}; use self::richtext::RichText; /* #COMMON */ /// Video thumbnail or other image #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[non_exhaustive] pub struct Thumbnail { pub url: String, pub width: u32, pub height: u32, } /// Entities extracted from a YouTube URL #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub enum UrlTarget { Video { id: String, start_time: u32 }, Channel { id: String }, Playlist { id: String }, } impl ToString for UrlTarget { fn to_string(&self) -> String { self.to_url() } } impl UrlTarget { pub fn to_url(&self) -> String { self.to_url_yt_host("https://www.youtube.com") } pub fn to_url_yt_host(&self, yt_host: &str) -> String { match self { UrlTarget::Video { id, start_time, .. } => match start_time { 0 => format!("{}/watch?v={}", yt_host, id), n => format!("{}/watch?v={}&t={}s", yt_host, id, n), }, UrlTarget::Channel { id } => { format!("{}/channel/{}", yt_host, id) } UrlTarget::Playlist { id } => { format!("{}/playlist?list={}", yt_host, id) } } } pub(crate) fn validate(&self) -> Result<(), Error> { match self { UrlTarget::Video { id, .. } => { match util::VIDEO_ID_REGEX.is_match(id).unwrap_or_default() { true => Ok(()), false => Err(Error::Other("invalid video id".into())), } } UrlTarget::Channel { id } => { match util::CHANNEL_ID_REGEX.is_match(id).unwrap_or_default() { true => Ok(()), false => Err(Error::Other("invalid channel id".into())), } } UrlTarget::Playlist { id } => { match util::PLAYLIST_ID_REGEX.is_match(id).unwrap_or_default() { true => Ok(()), false => Err(Error::Other("invalid playlist id".into())), } } } } } /* #PLAYER */ pub trait FileFormat { /// Get the file extension (".xyz") of the file format fn extension(&self) -> &str; } /// Video player data #[derive(Clone, Debug, Serialize, Deserialize)] #[non_exhaustive] pub struct VideoPlayer { /// Video metadata pub details: VideoPlayerDetails, /// List of streams containing both audio and video pub video_streams: Vec, /// List of streams containing video only pub video_only_streams: Vec, /// List of streams containing audio only pub audio_streams: Vec, /// List of subtitles pub subtitles: Vec, /// Lifetime of the stream URLs in seconds pub expires_in_seconds: u32, pub hls_manifest_url: Option, pub dash_manifest_url: Option, } /// Video metadata from the player #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[non_exhaustive] pub struct VideoPlayerDetails { /// Unique YouTube video ID pub id: String, /// Video title pub title: String, /// Video description in plaintext format pub description: Option, /// Video length in seconds /// /// Is zero for livestreams pub length: u32, /// Video thumbnail pub thumbnail: Vec, /// Channel of the video pub channel: ChannelId, /// Number of views / current viewers in case of a livestream. pub view_count: u64, /// List of words that describe the topic of the video pub keywords: Vec, /// True if the video is an active livestream pub is_live: bool, /// True if the video is/was livestreamed pub is_live_content: bool, } /// Video stream #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[non_exhaustive] pub struct VideoStream { /// Video stream URL pub url: String, /// YouTube stream format identifier pub itag: u32, pub bitrate: u32, pub average_bitrate: u32, /// Video file size in bytes pub size: Option, pub index_range: Option>, pub init_range: Option>, /// Video width in pixels pub width: u32, /// Video height in pixels pub height: u32, /// Video frames per second pub fps: u8, /// Quality text (e.g. "1080p60") pub quality: String, /// True if the video is HDR pub hdr: bool, /// MIME file type pub mime: String, /// Video file format pub format: VideoFormat, /// Video codec pub codec: VideoCodec, /// True if the deobfuscation of the nsig url parameter failed /// and the stream will be throttled pub throttled: bool, } /// Audio stream #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[non_exhaustive] pub struct AudioStream { /// Audio stream URL pub url: String, /// YouTube stream format identifier pub itag: u32, pub bitrate: u32, pub average_bitrate: u32, /// Audio file size in bytes pub size: u64, pub index_range: Option>, pub init_range: Option>, /// MIME file type pub mime: String, /// Audio file format pub format: AudioFormat, /// Audio codec pub codec: AudioCodec, /// True if the deobfuscation of the nsig url parameter failed /// and the stream will be throttled pub throttled: bool, /// Audio track information /// /// Videos can have multiple audio tracks (different languages). /// In this case, this object shows to which track the stream belongs to. /// /// This is None if the video contains only 1 audio track. pub track: Option, } /// Video codec #[derive( Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash, )] #[serde(rename_all = "snake_case")] #[non_exhaustive] pub enum VideoCodec { #[default] Unknown, /// MPEG-4 Part 14 Mp4v, /// avc1 aka H.264: Avc1, /// VP9: Vp9, /// AV1, the latest codec: Av01, } /// Audio codec #[derive( Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash, )] #[serde(rename_all = "snake_case")] #[non_exhaustive] pub enum AudioCodec { #[default] Unknown, /// MP4A aka AAC: Mp4a, /// Opus: Opus, } /// Video file type #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)] #[serde(rename_all = "snake_case")] #[non_exhaustive] pub enum VideoFormat { /// `*.3gp` #[serde(rename = "3gp")] ThreeGp, /// `*.mp4` Mp4, /// `*.webm` Webm, } /// Audio track information /// /// Videos can have multiple audio tracks (different languages). /// In this case, this object shows to which track the stream belongs to. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[non_exhaustive] pub struct AudioTrack { /// Track ID (e.g. `en.0`) pub id: String, /// 2/3 letter language code (e.g. `en`) /// /// Extracted from the track ID pub lang: Option, /// Language name (e.g. "English") pub lang_name: String, /// True if this is the default audio track pub is_default: bool, } impl FileFormat for VideoFormat { fn extension(&self) -> &str { match self { VideoFormat::ThreeGp => ".3gp", VideoFormat::Mp4 => ".mp4", VideoFormat::Webm => ".webm", } } } /// Audio file type #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)] #[serde(rename_all = "snake_case")] #[non_exhaustive] pub enum AudioFormat { /// `*.m4a` M4a, /// `*.webm` Webm, } impl FileFormat for AudioFormat { fn extension(&self) -> &str { match self { AudioFormat::M4a => ".m4a", AudioFormat::Webm => ".webm", } } } /// YouTube provides subtitles in different formats. /// /// srv1 (XML) is the default format, to request a different format you have /// to append `&fmt=` to the URL. /// /// # Subtitle formats /// /// ### `srv1` (default) /// /// ```xml /// /// /// - [Mr Beast] I built two massive circles /// and put 100 boys in one /// and 100 girls in the other /// /// ``` /// /// ### `srv2` /// /// ```xml /// /// /// - [Mr Beast] I built two massive circles /// and put 100 boys in one /// and 100 girls in the other /// /// ``` /// /// ### `srv3` /// /// ```xml /// /// /// ///

- [Mr Beast] I built two massive circles

///

and put 100 boys in one /// and 100 girls in the other

/// ///
/// ``` /// /// ### `json3` /// /// ```json /// { /// "wireMagic": "pb3", /// "pens": [{}], /// "wsWinStyles": [{}], /// "wpWinPositions": [{}], /// "events": [ /// { /// "tStartMs": 120, /// "dDurationMs": 1590, /// "segs": [ /// { /// "utf8": "- [Mr Beast] I built two massive circles" /// } /// ] /// }, /// { /// "tStartMs": 1710, /// "dDurationMs": 3390, /// "segs": [ /// { /// "utf8": "and put 100 boys in one\nand 100 girls in the other" /// } /// ] /// } /// ] /// } /// ``` /// /// ### Timed Text Markup Language (`ttml`) /// /// ```xml /// /// /// /// ///