1use std::fmt;
4use std::hash::Hash;
5use std::str::FromStr;
6
7use ordered_float::OrderedFloat;
8use reqwest::header::{self, HeaderMap, HeaderValue};
9use serde::{Deserialize, Serialize};
10
11use crate::model::DrmStatus;
12use crate::model::utils::serde::json_none;
13
14#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
22pub struct Format {
23 pub format: String,
25 pub format_id: String,
27 pub format_note: Option<String>,
29
30 #[serde(default)]
32 pub protocol: Protocol,
33 pub language: Option<String>,
35
36 pub has_drm: Option<DrmStatus>,
38 #[serde(default)]
40 pub container: Option<Container>,
41
42 pub available_at: Option<i64>,
45 pub language_preference: Option<i64>,
47 pub source_preference: Option<i64>,
49
50 #[serde(flatten)]
52 pub codec_info: CodecInfo,
53 #[serde(flatten)]
55 pub video_resolution: VideoResolution,
56 #[serde(flatten)]
58 pub download_info: DownloadInfo,
59 #[serde(flatten)]
61 pub quality_info: QualityInfo,
62 #[serde(flatten)]
64 pub file_info: FileInfo,
65 #[serde(flatten)]
67 pub storyboard_info: StoryboardInfo,
68 #[serde(flatten)]
70 pub rates_info: RatesInfo,
71
72 #[serde(skip)]
76 pub video_id: Option<String>,
77}
78
79impl Format {
80 pub fn is_video(&self) -> bool {
82 let format_type = self.format_type();
83
84 format_type.is_video()
85 }
86
87 pub fn is_audio(&self) -> bool {
89 let format_type = self.format_type();
90
91 format_type.is_audio()
92 }
93
94 pub fn format_type(&self) -> FormatType {
101 if self.download_info.manifest_url.is_some() {
102 return FormatType::Manifest;
103 }
104
105 if self.storyboard_info.fragments.is_some() {
106 return FormatType::Storyboard;
107 }
108
109 let audio = self.codec_info.audio_codec.is_some();
110 let video = self.codec_info.video_codec.is_some();
111
112 match (audio, video) {
113 (true, true) => FormatType::AudioVideo,
114 (true, false) => FormatType::Audio,
115 (false, true) => FormatType::Video,
116 _ => FormatType::Unknown,
117 }
118 }
119
120 pub fn url(&self) -> Result<&String, crate::error::Error> {
130 self.download_info
131 .url
132 .as_ref()
133 .ok_or_else(|| crate::error::Error::FormatNoUrl {
134 video_id: self.video_id.clone().unwrap_or_else(|| "unknown".to_string()),
135 format_id: self.format_id.clone(),
136 })
137 }
138}
139
140impl fmt::Display for Format {
141 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142 write!(f, "Format(id={}, format={:?})", self.format_id, self.format)
143 }
144}
145
146#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
148pub struct CodecInfo {
149 #[serde(default)]
151 #[serde(rename = "acodec")]
152 #[serde(deserialize_with = "json_none")]
153 pub audio_codec: Option<String>,
154 #[serde(default)]
156 #[serde(rename = "vcodec")]
157 #[serde(deserialize_with = "json_none")]
158 pub video_codec: Option<String>,
159 #[serde(default)]
161 pub audio_ext: Extension,
162 #[serde(default)]
164 pub video_ext: Extension,
165 pub audio_channels: Option<i64>,
167 pub asr: Option<i64>,
169}
170
171impl fmt::Display for CodecInfo {
172 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173 write!(
174 f,
175 "CodecInfo(audio={}, video={})",
176 self.audio_codec.as_deref().unwrap_or("none"),
177 self.video_codec.as_deref().unwrap_or("none")
178 )
179 }
180}
181
182#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
184pub struct VideoResolution {
185 pub width: Option<u32>,
187 pub height: Option<u32>,
189 pub resolution: Option<String>,
191 pub fps: Option<OrderedFloat<f64>>,
193 pub aspect_ratio: Option<OrderedFloat<f64>>,
195}
196
197impl fmt::Display for VideoResolution {
198 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199 match (self.width, self.height) {
200 (Some(w), Some(h)) => write!(f, "VideoResolution(width={}, height={})", w, h),
201 _ => write!(f, "VideoResolution(unknown)"),
202 }
203 }
204}
205
206#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
208pub struct DownloadInfo {
209 pub url: Option<String>,
211 #[serde(default)]
213 pub ext: Extension,
214 pub http_headers: HttpHeaders,
216 pub manifest_url: Option<String>,
218 pub downloader_options: Option<DownloaderOptions>,
220}
221
222impl fmt::Display for DownloadInfo {
223 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224 write!(f, "DownloadInfo(url={})", self.url.as_deref().unwrap_or("none"))
225 }
226}
227
228#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
230pub struct QualityInfo {
231 pub quality: Option<OrderedFloat<f64>>,
233 #[serde(default)]
235 pub dynamic_range: Option<DynamicRange>,
236}
237
238impl fmt::Display for QualityInfo {
239 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
240 write!(
241 f,
242 "QualityInfo(quality={})",
243 self.quality
244 .map(|q| q.to_string())
245 .unwrap_or_else(|| "unknown".to_string())
246 )
247 }
248}
249
250#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
252pub struct FileInfo {
253 pub filesize_approx: Option<i64>,
255 pub filesize: Option<i64>,
257}
258
259impl fmt::Display for FileInfo {
260 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261 if let Some(size) = self.filesize {
262 write!(f, "FileInfo(size={})", size)
263 } else if let Some(approx) = self.filesize_approx {
264 write!(f, "FileInfo(approx_size={})", approx)
265 } else {
266 write!(f, "FileInfo(size=unknown)")
267 }
268 }
269}
270
271#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
273pub struct RatesInfo {
274 #[serde(rename = "vbr")]
276 pub video_rate: Option<OrderedFloat<f64>>,
277 #[serde(rename = "abr")]
279 pub audio_rate: Option<OrderedFloat<f64>>,
280 #[serde(rename = "tbr")]
282 pub total_rate: Option<OrderedFloat<f64>>,
283}
284
285impl fmt::Display for RatesInfo {
286 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
287 write!(
288 f,
289 "RatesInfo(video={}, audio={}, total={})",
290 self.video_rate
291 .map(|r| r.to_string())
292 .unwrap_or_else(|| "none".to_string()),
293 self.audio_rate
294 .map(|r| r.to_string())
295 .unwrap_or_else(|| "none".to_string()),
296 self.total_rate
297 .map(|r| r.to_string())
298 .unwrap_or_else(|| "none".to_string())
299 )
300 }
301}
302
303#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
305pub struct StoryboardInfo {
306 pub rows: Option<i64>,
308 pub columns: Option<i64>,
310 pub fragments: Option<Vec<Fragment>>,
312}
313
314impl fmt::Display for StoryboardInfo {
315 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
316 match (self.rows, self.columns) {
317 (Some(r), Some(c)) => write!(f, "StoryboardInfo(rows={}, columns={})", r, c),
318 _ => write!(f, "StoryboardInfo(unknown)"),
319 }
320 }
321}
322
323#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
325pub struct Fragment {
326 pub url: String,
328 pub duration: OrderedFloat<f64>,
330}
331
332impl fmt::Display for Fragment {
333 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
334 write!(f, "Fragment(url={}, duration={})", self.url, self.duration)
335 }
336}
337
338#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
340pub struct DownloaderOptions {
341 pub http_chunk_size: i64,
343}
344
345impl fmt::Display for DownloaderOptions {
346 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
347 write!(f, "DownloaderOptions(chunk_size={})", self.http_chunk_size)
348 }
349}
350
351#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
353#[serde(rename_all = "PascalCase")]
354pub struct HttpHeaders {
355 #[serde(rename = "User-Agent", default)]
357 pub user_agent: String,
358 #[serde(default)]
360 pub accept: String,
361 #[serde(rename = "Accept-Language", default)]
363 pub accept_language: String,
364 #[serde(rename = "Sec-Fetch-Mode", default)]
366 pub sec_fetch_mode: String,
367}
368
369impl HttpHeaders {
370 pub fn browser_defaults(user_agent: String) -> Self {
380 Self {
381 user_agent,
382 accept: "*/*".to_string(),
383 accept_language: "en-US,en".to_string(),
384 sec_fetch_mode: "navigate".to_string(),
385 }
386 }
387
388 pub fn to_header_map(&self) -> reqwest::header::HeaderMap {
394 let mut map = HeaderMap::new();
395 if let Ok(hv) = HeaderValue::from_str(&self.user_agent) {
396 map.insert(header::USER_AGENT, hv);
397 }
398 if let Ok(hv) = HeaderValue::from_str(&self.accept) {
399 map.insert(header::ACCEPT, hv);
400 }
401 if let Ok(hv) = HeaderValue::from_str(&self.accept_language) {
402 map.insert(header::ACCEPT_LANGUAGE, hv);
403 }
404 if let Ok(hv) = HeaderValue::from_bytes(self.sec_fetch_mode.as_bytes()) {
405 map.insert("Sec-Fetch-Mode", hv);
406 }
407 map
408 }
409}
410
411impl fmt::Display for HttpHeaders {
412 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
413 write!(f, "HttpHeaders(user_agent={})", self.user_agent)
414 }
415}
416
417#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
419#[serde(rename_all = "snake_case")]
420pub enum Extension {
421 #[serde(rename = "m4a")]
423 M4A,
425 Mp3,
427 Mp4,
429 Webm,
431 Flac,
433 Ogg,
435 Wav,
437 Aac,
439 Aiff,
441 Avi,
443 Ts,
445 Flv,
447
448 Mhtml,
450
451 None,
453 #[default]
455 #[serde(other)]
456 Unknown,
457}
458
459impl Extension {
460 pub fn as_str(&self) -> &'static str {
467 match self {
468 Extension::M4A => "m4a",
469 Extension::Mp3 => "mp3",
470 Extension::Mp4 => "mp4",
471 Extension::Webm => "webm",
472 Extension::Flac => "flac",
473 Extension::Ogg => "ogg",
474 Extension::Wav => "wav",
475 Extension::Aac => "aac",
476 Extension::Aiff => "aiff",
477 Extension::Avi => "avi",
478 Extension::Ts => "ts",
479 Extension::Flv => "flv",
480 Extension::Mhtml => "mhtml",
481 Extension::None | Extension::Unknown => "bin",
482 }
483 }
484}
485
486impl fmt::Display for Extension {
487 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
488 match self {
489 Extension::M4A => f.write_str("M4A"),
490 Extension::Mp3 => f.write_str("Mp3"),
491 Extension::Mp4 => f.write_str("Mp4"),
492 Extension::Webm => f.write_str("Webm"),
493 Extension::Flac => f.write_str("Flac"),
494 Extension::Ogg => f.write_str("Ogg"),
495 Extension::Wav => f.write_str("Wav"),
496 Extension::Aac => f.write_str("Aac"),
497 Extension::Aiff => f.write_str("Aiff"),
498 Extension::Avi => f.write_str("Avi"),
499 Extension::Ts => f.write_str("Ts"),
500 Extension::Flv => f.write_str("Flv"),
501 Extension::Mhtml => f.write_str("Mhtml"),
502 Extension::None => f.write_str("None"),
503 Extension::Unknown => f.write_str("Unknown"),
504 }
505 }
506}
507
508impl FromStr for Extension {
509 type Err = ();
510
511 fn from_str(s: &str) -> Result<Self, Self::Err> {
512 match s.to_lowercase().as_str() {
513 "m4a" => Ok(Extension::M4A),
514 "mp3" => Ok(Extension::Mp3),
515 "mp4" => Ok(Extension::Mp4),
516 "webm" => Ok(Extension::Webm),
517 "flac" => Ok(Extension::Flac),
518 "ogg" | "oga" | "opus" => Ok(Extension::Ogg),
519 "wav" => Ok(Extension::Wav),
520 "aac" => Ok(Extension::Aac),
521 "aiff" | "aif" => Ok(Extension::Aiff),
522 "avi" => Ok(Extension::Avi),
523 "ts" | "m2ts" | "mts" => Ok(Extension::Ts),
524 "flv" => Ok(Extension::Flv),
525 "mhtml" => Ok(Extension::Mhtml),
526 "" | "none" => Ok(Extension::None),
527 _ => Ok(Extension::Unknown),
528 }
529 }
530}
531
532#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
534#[serde(rename_all = "snake_case")]
535pub enum Container {
536 #[serde(rename = "webm_dash")]
538 Webm,
539 #[serde(rename = "m4a_dash")]
541 M4A,
542 #[serde(rename = "mp4_dash")]
544 Mp4,
545
546 #[default]
548 #[serde(other)]
549 Unknown,
550}
551
552impl fmt::Display for Container {
553 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
554 match self {
555 Container::Mp4 => f.write_str("Mp4"),
556 Container::Webm => f.write_str("Webm"),
557 Container::M4A => f.write_str("M4A"),
558 Container::Unknown => f.write_str("Unknown"),
559 }
560 }
561}
562
563#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
565#[serde(rename_all = "snake_case")]
566pub enum Protocol {
567 Https,
569 #[serde(rename = "m3u8_native")]
571 M3U8Native,
572 Mhtml,
574
575 #[default]
577 #[serde(other)]
578 Unknown,
579}
580
581impl fmt::Display for Protocol {
582 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
583 match self {
584 Protocol::Https => f.write_str("Https"),
585 Protocol::M3U8Native => f.write_str("HLS"),
586 Protocol::Mhtml => f.write_str("Mhtml"),
587 Protocol::Unknown => f.write_str("Unknown"),
588 }
589 }
590}
591
592#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
594pub enum DynamicRange {
595 SDR,
597 HDR,
599
600 #[default]
602 #[serde(other)]
603 Unknown,
604}
605
606impl fmt::Display for DynamicRange {
607 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
608 match self {
609 DynamicRange::SDR => f.write_str("SDR"),
610 DynamicRange::HDR => f.write_str("HDR"),
611 DynamicRange::Unknown => f.write_str("Unknown"),
612 }
613 }
614}
615
616#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
619pub enum FormatType {
620 Audio,
622 Video,
624 AudioVideo,
626 Manifest,
628 Storyboard,
630
631 #[default]
633 #[serde(other)]
634 Unknown,
635}
636
637impl FormatType {
638 pub fn is_audio_and_video(&self) -> bool {
644 matches!(self, FormatType::AudioVideo)
645 }
646
647 pub fn is_video(&self) -> bool {
653 matches!(self, FormatType::Video)
654 }
655
656 pub fn is_audio(&self) -> bool {
662 matches!(self, FormatType::Audio)
663 }
664
665 pub fn is_storyboard(&self) -> bool {
671 matches!(self, FormatType::Storyboard)
672 }
673
674 pub fn is_manifest(&self) -> bool {
676 matches!(self, FormatType::Manifest)
677 }
678}
679
680impl fmt::Display for FormatType {
681 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
682 match self {
683 FormatType::Audio => f.write_str("Audio"),
684 FormatType::Video => f.write_str("Video"),
685 FormatType::AudioVideo => f.write_str("AudioVideo"),
686 FormatType::Manifest => f.write_str("Manifest"),
687 FormatType::Storyboard => f.write_str("Storyboard"),
688 FormatType::Unknown => f.write_str("Unknown"),
689 }
690 }
691}