use std::fmt;
use std::hash::Hash;
use std::str::FromStr;
use ordered_float::OrderedFloat;
use reqwest::header::{self, HeaderMap, HeaderValue};
use serde::{Deserialize, Serialize};
use crate::model::DrmStatus;
use crate::model::utils::serde::json_none;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Format {
pub format: String,
pub format_id: String,
pub format_note: Option<String>,
#[serde(default)]
pub protocol: Protocol,
pub language: Option<String>,
pub has_drm: Option<DrmStatus>,
#[serde(default)]
pub container: Option<Container>,
pub available_at: Option<i64>,
pub language_preference: Option<i64>,
pub source_preference: Option<i64>,
#[serde(flatten)]
pub codec_info: CodecInfo,
#[serde(flatten)]
pub video_resolution: VideoResolution,
#[serde(flatten)]
pub download_info: DownloadInfo,
#[serde(flatten)]
pub quality_info: QualityInfo,
#[serde(flatten)]
pub file_info: FileInfo,
#[serde(flatten)]
pub storyboard_info: StoryboardInfo,
#[serde(flatten)]
pub rates_info: RatesInfo,
#[serde(skip)]
pub video_id: Option<String>,
}
impl Format {
pub fn is_video(&self) -> bool {
let format_type = self.format_type();
format_type.is_video()
}
pub fn is_audio(&self) -> bool {
let format_type = self.format_type();
format_type.is_audio()
}
pub fn format_type(&self) -> FormatType {
if self.download_info.manifest_url.is_some() {
return FormatType::Manifest;
}
if self.storyboard_info.fragments.is_some() {
return FormatType::Storyboard;
}
let audio = self.codec_info.audio_codec.is_some();
let video = self.codec_info.video_codec.is_some();
match (audio, video) {
(true, true) => FormatType::AudioVideo,
(true, false) => FormatType::Audio,
(false, true) => FormatType::Video,
_ => FormatType::Unknown,
}
}
pub fn url(&self) -> Result<&String, crate::error::Error> {
self.download_info
.url
.as_ref()
.ok_or_else(|| crate::error::Error::FormatNoUrl {
video_id: self.video_id.clone().unwrap_or_else(|| "unknown".to_string()),
format_id: self.format_id.clone(),
})
}
}
impl fmt::Display for Format {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Format(id={}, format={:?})", self.format_id, self.format)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct CodecInfo {
#[serde(default)]
#[serde(rename = "acodec")]
#[serde(deserialize_with = "json_none")]
pub audio_codec: Option<String>,
#[serde(default)]
#[serde(rename = "vcodec")]
#[serde(deserialize_with = "json_none")]
pub video_codec: Option<String>,
#[serde(default)]
pub audio_ext: Extension,
#[serde(default)]
pub video_ext: Extension,
pub audio_channels: Option<i64>,
pub asr: Option<i64>,
}
impl fmt::Display for CodecInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"CodecInfo(audio={}, video={})",
self.audio_codec.as_deref().unwrap_or("none"),
self.video_codec.as_deref().unwrap_or("none")
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct VideoResolution {
pub width: Option<u32>,
pub height: Option<u32>,
pub resolution: Option<String>,
pub fps: Option<OrderedFloat<f64>>,
pub aspect_ratio: Option<OrderedFloat<f64>>,
}
impl fmt::Display for VideoResolution {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match (self.width, self.height) {
(Some(w), Some(h)) => write!(f, "VideoResolution(width={}, height={})", w, h),
_ => write!(f, "VideoResolution(unknown)"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct DownloadInfo {
pub url: Option<String>,
#[serde(default)]
pub ext: Extension,
pub http_headers: HttpHeaders,
pub manifest_url: Option<String>,
pub downloader_options: Option<DownloaderOptions>,
}
impl fmt::Display for DownloadInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "DownloadInfo(url={})", self.url.as_deref().unwrap_or("none"))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct QualityInfo {
pub quality: Option<OrderedFloat<f64>>,
#[serde(default)]
pub dynamic_range: Option<DynamicRange>,
}
impl fmt::Display for QualityInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"QualityInfo(quality={})",
self.quality
.map(|q| q.to_string())
.unwrap_or_else(|| "unknown".to_string())
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct FileInfo {
pub filesize_approx: Option<i64>,
pub filesize: Option<i64>,
}
impl fmt::Display for FileInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(size) = self.filesize {
write!(f, "FileInfo(size={})", size)
} else if let Some(approx) = self.filesize_approx {
write!(f, "FileInfo(approx_size={})", approx)
} else {
write!(f, "FileInfo(size=unknown)")
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct RatesInfo {
#[serde(rename = "vbr")]
pub video_rate: Option<OrderedFloat<f64>>,
#[serde(rename = "abr")]
pub audio_rate: Option<OrderedFloat<f64>>,
#[serde(rename = "tbr")]
pub total_rate: Option<OrderedFloat<f64>>,
}
impl fmt::Display for RatesInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"RatesInfo(video={}, audio={}, total={})",
self.video_rate
.map(|r| r.to_string())
.unwrap_or_else(|| "none".to_string()),
self.audio_rate
.map(|r| r.to_string())
.unwrap_or_else(|| "none".to_string()),
self.total_rate
.map(|r| r.to_string())
.unwrap_or_else(|| "none".to_string())
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct StoryboardInfo {
pub rows: Option<i64>,
pub columns: Option<i64>,
pub fragments: Option<Vec<Fragment>>,
}
impl fmt::Display for StoryboardInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match (self.rows, self.columns) {
(Some(r), Some(c)) => write!(f, "StoryboardInfo(rows={}, columns={})", r, c),
_ => write!(f, "StoryboardInfo(unknown)"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Fragment {
pub url: String,
pub duration: OrderedFloat<f64>,
}
impl fmt::Display for Fragment {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Fragment(url={}, duration={})", self.url, self.duration)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct DownloaderOptions {
pub http_chunk_size: i64,
}
impl fmt::Display for DownloaderOptions {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "DownloaderOptions(chunk_size={})", self.http_chunk_size)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct HttpHeaders {
#[serde(rename = "User-Agent", default)]
pub user_agent: String,
#[serde(default)]
pub accept: String,
#[serde(rename = "Accept-Language", default)]
pub accept_language: String,
#[serde(rename = "Sec-Fetch-Mode", default)]
pub sec_fetch_mode: String,
}
impl HttpHeaders {
pub fn browser_defaults(user_agent: String) -> Self {
Self {
user_agent,
accept: "*/*".to_string(),
accept_language: "en-US,en".to_string(),
sec_fetch_mode: "navigate".to_string(),
}
}
pub fn to_header_map(&self) -> reqwest::header::HeaderMap {
let mut map = HeaderMap::new();
if let Ok(hv) = HeaderValue::from_str(&self.user_agent) {
map.insert(header::USER_AGENT, hv);
}
if let Ok(hv) = HeaderValue::from_str(&self.accept) {
map.insert(header::ACCEPT, hv);
}
if let Ok(hv) = HeaderValue::from_str(&self.accept_language) {
map.insert(header::ACCEPT_LANGUAGE, hv);
}
if let Ok(hv) = HeaderValue::from_bytes(self.sec_fetch_mode.as_bytes()) {
map.insert("Sec-Fetch-Mode", hv);
}
map
}
}
impl fmt::Display for HttpHeaders {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "HttpHeaders(user_agent={})", self.user_agent)
}
}
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Extension {
#[serde(rename = "m4a")]
M4A,
Mp3,
Mp4,
Webm,
Flac,
Ogg,
Wav,
Aac,
Aiff,
Avi,
Ts,
Flv,
Mhtml,
None,
#[default]
#[serde(other)]
Unknown,
}
impl Extension {
pub fn as_str(&self) -> &'static str {
match self {
Extension::M4A => "m4a",
Extension::Mp3 => "mp3",
Extension::Mp4 => "mp4",
Extension::Webm => "webm",
Extension::Flac => "flac",
Extension::Ogg => "ogg",
Extension::Wav => "wav",
Extension::Aac => "aac",
Extension::Aiff => "aiff",
Extension::Avi => "avi",
Extension::Ts => "ts",
Extension::Flv => "flv",
Extension::Mhtml => "mhtml",
Extension::None | Extension::Unknown => "bin",
}
}
}
impl fmt::Display for Extension {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Extension::M4A => f.write_str("M4A"),
Extension::Mp3 => f.write_str("Mp3"),
Extension::Mp4 => f.write_str("Mp4"),
Extension::Webm => f.write_str("Webm"),
Extension::Flac => f.write_str("Flac"),
Extension::Ogg => f.write_str("Ogg"),
Extension::Wav => f.write_str("Wav"),
Extension::Aac => f.write_str("Aac"),
Extension::Aiff => f.write_str("Aiff"),
Extension::Avi => f.write_str("Avi"),
Extension::Ts => f.write_str("Ts"),
Extension::Flv => f.write_str("Flv"),
Extension::Mhtml => f.write_str("Mhtml"),
Extension::None => f.write_str("None"),
Extension::Unknown => f.write_str("Unknown"),
}
}
}
impl FromStr for Extension {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"m4a" => Ok(Extension::M4A),
"mp3" => Ok(Extension::Mp3),
"mp4" => Ok(Extension::Mp4),
"webm" => Ok(Extension::Webm),
"flac" => Ok(Extension::Flac),
"ogg" | "oga" | "opus" => Ok(Extension::Ogg),
"wav" => Ok(Extension::Wav),
"aac" => Ok(Extension::Aac),
"aiff" | "aif" => Ok(Extension::Aiff),
"avi" => Ok(Extension::Avi),
"ts" | "m2ts" | "mts" => Ok(Extension::Ts),
"flv" => Ok(Extension::Flv),
"mhtml" => Ok(Extension::Mhtml),
"" | "none" => Ok(Extension::None),
_ => Ok(Extension::Unknown),
}
}
}
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Container {
#[serde(rename = "webm_dash")]
Webm,
#[serde(rename = "m4a_dash")]
M4A,
#[serde(rename = "mp4_dash")]
Mp4,
#[default]
#[serde(other)]
Unknown,
}
impl fmt::Display for Container {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Container::Mp4 => f.write_str("Mp4"),
Container::Webm => f.write_str("Webm"),
Container::M4A => f.write_str("M4A"),
Container::Unknown => f.write_str("Unknown"),
}
}
}
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Protocol {
Https,
#[serde(rename = "m3u8_native")]
M3U8Native,
Mhtml,
#[default]
#[serde(other)]
Unknown,
}
impl fmt::Display for Protocol {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Protocol::Https => f.write_str("Https"),
Protocol::M3U8Native => f.write_str("HLS"),
Protocol::Mhtml => f.write_str("Mhtml"),
Protocol::Unknown => f.write_str("Unknown"),
}
}
}
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum DynamicRange {
SDR,
HDR,
#[default]
#[serde(other)]
Unknown,
}
impl fmt::Display for DynamicRange {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DynamicRange::SDR => f.write_str("SDR"),
DynamicRange::HDR => f.write_str("HDR"),
DynamicRange::Unknown => f.write_str("Unknown"),
}
}
}
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum FormatType {
Audio,
Video,
AudioVideo,
Manifest,
Storyboard,
#[default]
#[serde(other)]
Unknown,
}
impl FormatType {
pub fn is_audio_and_video(&self) -> bool {
matches!(self, FormatType::AudioVideo)
}
pub fn is_video(&self) -> bool {
matches!(self, FormatType::Video)
}
pub fn is_audio(&self) -> bool {
matches!(self, FormatType::Audio)
}
pub fn is_storyboard(&self) -> bool {
matches!(self, FormatType::Storyboard)
}
pub fn is_manifest(&self) -> bool {
matches!(self, FormatType::Manifest)
}
}
impl fmt::Display for FormatType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FormatType::Audio => f.write_str("Audio"),
FormatType::Video => f.write_str("Video"),
FormatType::AudioVideo => f.write_str("AudioVideo"),
FormatType::Manifest => f.write_str("Manifest"),
FormatType::Storyboard => f.write_str("Storyboard"),
FormatType::Unknown => f.write_str("Unknown"),
}
}
}