diff --git a/Cargo.lock b/Cargo.lock index 88173b9..5e6b90b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -208,6 +208,12 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + [[package]] name = "futures-macro" version = "0.3.30" @@ -238,8 +244,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-core", + "futures-io", "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", "slab", @@ -659,10 +668,12 @@ dependencies = [ "system-configuration", "tokio", "tokio-native-tls", + "tokio-util", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", "winreg", ] @@ -1113,6 +1124,19 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +[[package]] +name = "wasm-streams" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.69" diff --git a/Cargo.toml b/Cargo.toml index 2e0d581..c8e9455 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,9 +6,12 @@ edition = "2021" [dependencies] serde = { version = "1.0.197", features = ["derive"] } serde_json = "1.0" -reqwest = { version = "0.11", features = ["json"] } +reqwest = { version = "0.11", features = ["json", "stream"] } tokio = { version = "1", features = ["full"] } strum = { version = "0.26", features = ["derive"] } async-stream = "0.3.5" futures-core = "0.3" futures-util = "0.3.30" + +[profile.release] +strip = true diff --git a/src/bilibili/analyzer.rs b/src/bilibili/analyzer.rs index 3e43e88..47f227f 100644 --- a/src/bilibili/analyzer.rs +++ b/src/bilibili/analyzer.rs @@ -1,6 +1,6 @@ use std::rc::Rc; -use crate::bilibili::Result; +use crate::Result; pub struct PageAnalyzer { info: serde_json::Value, @@ -38,17 +38,29 @@ pub enum VideoCodecs { AV1, } +pub struct FilterOption { + pub video_max_quality: VideoQuality, + pub video_min_quality: VideoQuality, + pub audio_max_quality: AudioQuality, + pub audio_min_quality: AudioQuality, + pub codecs: Rc>, + pub no_dolby_video: bool, + pub no_dolby_audio: bool, + pub no_hdr: bool, + pub no_hires: bool, +} + #[derive(Debug, PartialEq, PartialOrd)] pub enum Stream { - FlvStream(String), - Html5Mp4Stream(String), - EpositeTryMp4Stream(String), - DashVideoStream { + Flv(String), + Html5Mp4(String), + EpositeTryMp4(String), + DashVideo { url: String, quality: VideoQuality, codecs: VideoCodecs, }, - DashAudioStream { + DashAudio { url: String, quality: AudioQuality, }, @@ -57,19 +69,22 @@ pub enum Stream { impl Stream { pub fn url(&self) -> &str { match self { - Self::FlvStream(url) => url, - Self::Html5Mp4Stream(url) => url, - Self::EpositeTryMp4Stream(url) => url, - Self::DashVideoStream { url, .. } => url, - Self::DashAudioStream { url, .. } => url, + Self::Flv(url) => url, + Self::Html5Mp4(url) => url, + Self::EpositeTryMp4(url) => url, + Self::DashVideo { url, .. } => url, + Self::DashAudio { url, .. } => url, } } } #[derive(Debug)] pub enum BestStream { - VideoAudioStream { video: Stream, audio: Stream }, - MixedStream(Stream), + VideoAudio { + video: Stream, + audio: Option, + }, + Mixed(Stream), } impl PageAnalyzer { @@ -98,30 +113,19 @@ impl PageAnalyzer { && !(self.info["is_html5"].is_boolean() && self.info["is_html5"].as_bool().unwrap()) } - fn streams( - &mut self, - video_max_quality: VideoQuality, - video_min_quality: VideoQuality, - audio_max_quality: AudioQuality, - audio_min_quality: AudioQuality, - codecs: Rc>, - no_dolby_video: bool, - no_dolby_audio: bool, - no_hdr: bool, - no_hires: bool, - ) -> Result> { + fn streams(&mut self, filter_option: &FilterOption) -> Result> { if self.is_flv_stream() { - return Ok(vec![Stream::FlvStream( + return Ok(vec![Stream::Flv( self.info["durl"][0]["url"].as_str().unwrap().to_string(), )]); } if self.is_html5_mp4_stream() { - return Ok(vec![Stream::Html5Mp4Stream( + return Ok(vec![Stream::Html5Mp4( self.info["durl"][0]["url"].as_str().unwrap().to_string(), )]); } if self.is_episode_try_mp4_stream() { - return Ok(vec![Stream::EpositeTryMp4Stream( + return Ok(vec![Stream::EpositeTryMp4( self.info["durl"][0]["url"].as_str().unwrap().to_string(), )]); } @@ -134,13 +138,13 @@ impl PageAnalyzer { let video_stream_url = video_data["baseUrl"].as_str().unwrap().to_string(); let video_stream_quality = VideoQuality::from_repr(video_data["id"].as_u64().unwrap() as usize) - .ok_or_else(|| "invalid video stream quality")?; - if (video_stream_quality == VideoQuality::QualityHdr && no_hdr) // NO HDR - || (video_stream_quality == VideoQuality::QualityDolby && no_dolby_video) // NO DOLBY + .ok_or("invalid video stream quality")?; + if (video_stream_quality == VideoQuality::QualityHdr && filter_option.no_hdr) // NO HDR + || (video_stream_quality == VideoQuality::QualityDolby && filter_option.no_dolby_video) // NO DOLBY || (video_stream_quality != VideoQuality::QualityDolby && video_stream_quality != VideoQuality::QualityHdr - && (video_stream_quality < video_min_quality - || video_stream_quality > video_max_quality)) + && (video_stream_quality < filter_option.video_min_quality + || video_stream_quality > filter_option.video_max_quality)) // NOT IN RANGE { continue; @@ -149,17 +153,16 @@ impl PageAnalyzer { let video_codecs = vec![VideoCodecs::HEV, VideoCodecs::AVC, VideoCodecs::AV1] .into_iter() - .filter(|c| video_codecs.contains(c.to_string().as_str())) - .next(); + .find(|c| video_codecs.contains(c.to_string().as_str())); let Some(video_codecs) = video_codecs else { continue; }; - if !codecs.contains(&video_codecs) { + if !filter_option.codecs.contains(&video_codecs) { continue; } - streams.push(Stream::DashVideoStream { + streams.push(Stream::DashVideo { url: video_stream_url, quality: video_stream_quality, codecs: video_codecs, @@ -173,36 +176,36 @@ impl PageAnalyzer { let Some(audio_stream_quality) = audio_stream_quality else { continue; }; - if audio_stream_quality > audio_max_quality - || audio_stream_quality < audio_min_quality + if audio_stream_quality > filter_option.audio_max_quality + || audio_stream_quality < filter_option.audio_min_quality { continue; } - streams.push(Stream::DashAudioStream { + streams.push(Stream::DashAudio { url: audio_stream_url, quality: audio_stream_quality, }); } } - if !(no_hires || flac_data["audio"].is_null()) { + if !(filter_option.no_hires || flac_data["audio"].is_null()) { let flac_stream_url = flac_data["audio"]["baseUrl"].as_str().unwrap().to_string(); let flac_stream_quality = AudioQuality::from_repr(flac_data["audio"]["id"].as_u64().unwrap() as usize) .unwrap(); - streams.push(Stream::DashAudioStream { + streams.push(Stream::DashAudio { url: flac_stream_url, quality: flac_stream_quality, }); } - if !(no_dolby_audio || dolby_data["audio"].is_null()) { - let dolby_stream_data = dolby_data["audio"].as_array().and_then(|v| v.get(0)); + if !(filter_option.no_dolby_audio || dolby_data["audio"].is_null()) { + let dolby_stream_data = dolby_data["audio"].as_array().and_then(|v| v.first()); if dolby_stream_data.is_some() { let dolby_stream_data = dolby_stream_data.unwrap(); let dolby_stream_url = dolby_stream_data["baseUrl"].as_str().unwrap().to_string(); let dolby_stream_quality = AudioQuality::from_repr(dolby_stream_data["id"].as_u64().unwrap() as usize) .unwrap(); - streams.push(Stream::DashAudioStream { + streams.push(Stream::DashAudio { url: dolby_stream_url, quality: dolby_stream_quality, }); @@ -211,98 +214,78 @@ impl PageAnalyzer { Ok(streams) } - pub fn best_stream( - &mut self, - video_max_quality: VideoQuality, - video_min_quality: VideoQuality, - audio_max_quality: AudioQuality, - audio_min_quality: AudioQuality, - codecs: Vec, - no_dolby_video: bool, - no_dolby_audio: bool, - no_hdr: bool, - no_hires: bool, - ) -> Result { - let codecs = Rc::new(codecs); - let streams = dbg!(self.streams( - video_max_quality, - video_min_quality, - audio_max_quality, - audio_min_quality, - codecs.clone(), - no_dolby_video, - no_dolby_audio, - no_hdr, - no_hires - ))?; + pub fn best_stream(&mut self, filter_option: &FilterOption) -> Result { + let streams = self.streams(filter_option)?; if self.is_flv_stream() || self.is_html5_mp4_stream() || self.is_episode_try_mp4_stream() { - return Ok(BestStream::MixedStream( + return Ok(BestStream::Mixed( streams.into_iter().next().ok_or("no stream found")?, )); } let (mut video_streams, mut audio_streams): (Vec<_>, Vec<_>) = streams .into_iter() - .partition(|s| matches!(s, Stream::DashVideoStream { .. })); + .partition(|s| matches!(s, Stream::DashVideo { .. })); video_streams.sort_by(|a, b| match (a, b) { ( - Stream::DashVideoStream { + Stream::DashVideo { quality: a_quality, codecs: a_codecs, .. }, - Stream::DashVideoStream { + Stream::DashVideo { quality: b_quality, codecs: b_codecs, .. }, ) => { - if a_quality == &VideoQuality::QualityDolby && !no_dolby_video { + if a_quality == &VideoQuality::QualityDolby && !filter_option.no_dolby_video { return std::cmp::Ordering::Greater; } - if b_quality == &VideoQuality::QualityDolby && !no_dolby_video { + if b_quality == &VideoQuality::QualityDolby && !filter_option.no_dolby_video { return std::cmp::Ordering::Less; } - if a_quality == &VideoQuality::QualityHdr && !no_hdr { + if a_quality == &VideoQuality::QualityHdr && !filter_option.no_hdr { return std::cmp::Ordering::Greater; } - if b_quality == &VideoQuality::QualityHdr && !no_hdr { + if b_quality == &VideoQuality::QualityHdr && !filter_option.no_hdr { return std::cmp::Ordering::Less; } if a_quality != b_quality { return a_quality.partial_cmp(b_quality).unwrap(); } - codecs + filter_option + .codecs .iter() .position(|c| c == b_codecs) - .cmp(&codecs.iter().position(|c| c == a_codecs)) + .cmp(&filter_option.codecs.iter().position(|c| c == a_codecs)) } _ => std::cmp::Ordering::Equal, }); audio_streams.sort_by(|a, b| match (a, b) { ( - Stream::DashAudioStream { + Stream::DashAudio { quality: a_quality, .. }, - Stream::DashAudioStream { + Stream::DashAudio { quality: b_quality, .. }, ) => { - if a_quality == &AudioQuality::QualityDolby && !no_dolby_audio { + if a_quality == &AudioQuality::QualityDolby && !filter_option.no_dolby_audio { return std::cmp::Ordering::Greater; } - if b_quality == &AudioQuality::QualityDolby && !no_dolby_audio { + if b_quality == &AudioQuality::QualityDolby && !filter_option.no_dolby_audio { return std::cmp::Ordering::Less; } a_quality.partial_cmp(b_quality).unwrap() } _ => std::cmp::Ordering::Equal, }); - if video_streams.is_empty() || audio_streams.is_empty() { + if video_streams.is_empty() { return Err("no stream found".into()); } - Ok(BestStream::VideoAudioStream { + Ok(BestStream::VideoAudio { video: video_streams.remove(video_streams.len() - 1), - audio: audio_streams.remove(audio_streams.len() - 1), + // audio stream may be empty, representing no audio for the video + audio: audio_streams.pop(), }) } } diff --git a/src/bilibili/client.rs b/src/bilibili/client.rs index f283802..af71005 100644 --- a/src/bilibili/client.rs +++ b/src/bilibili/client.rs @@ -1,4 +1,4 @@ -use reqwest::Method; +use reqwest::{header, Method}; pub struct Credential { sessdata: String, @@ -26,6 +26,21 @@ impl Credential { } } +pub fn client_with_header() -> reqwest::Client { + let mut headers = header::HeaderMap::new(); + headers.insert( + header::USER_AGENT, + header::HeaderValue::from_static("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.54")); + headers.insert( + header::REFERER, + header::HeaderValue::from_static("https://www.bilibili.com"), + ); + reqwest::Client::builder() + .default_headers(headers) + .build() + .unwrap() +} + pub struct BiliClient { credential: Option, client: reqwest::Client, @@ -34,34 +49,28 @@ pub struct BiliClient { impl BiliClient { pub fn anonymous() -> Self { let credential = None; - let client = reqwest::Client::new(); + let client = client_with_header(); Self { credential, client } } pub fn authenticated(credential: Credential) -> Self { let credential = Some(credential); - let client = reqwest::Client::new(); + let client = client_with_header(); Self { credential, client } } fn set_header(&self, req: reqwest::RequestBuilder) -> reqwest::RequestBuilder { - let req =req.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.54") - .header("Referer", "https://www.bilibili.com"); - if let Some(credential) = &self.credential { - return req.header("cookie", format!("SESSDATA={}", credential.sessdata)) + let Some(credential) = &self.credential else { + return req; + }; + req.header("cookie", format!("SESSDATA={}", credential.sessdata)) .header("cookie", format!("bili_jct={}", credential.bili_jct)) .header("cookie", format!("buvid3={}", credential.buvid3)) - .header( - "cookie", - format!("DedeUserID={}", credential.dedeuserid), - ) + .header("cookie", format!("DedeUserID={}", credential.dedeuserid)) .header( "cookie", format!("ac_time_value={}", credential.ac_time_value), - ).header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.54") - .header("Referer", "https://www.bilibili.com"); - } - req + ) } pub fn request(&self, method: Method, url: &str) -> reqwest::RequestBuilder { diff --git a/src/bilibili/favorite_list.rs b/src/bilibili/favorite_list.rs index a3e83fd..e29542f 100644 --- a/src/bilibili/favorite_list.rs +++ b/src/bilibili/favorite_list.rs @@ -4,7 +4,8 @@ use async_stream::stream; use futures_core::stream::Stream; use serde_json::Value; -use crate::bilibili::{BiliClient, Result}; +use crate::bilibili::BiliClient; +use crate::Result; pub struct FavoriteList { client: Rc, fid: String, @@ -44,7 +45,7 @@ impl FavoriteList { .client .request( reqwest::Method::GET, - &"https://api.bilibili.com/x/v3/fav/folder/info", + "https://api.bilibili.com/x/v3/fav/folder/info", ) .query(&[("media_id", &self.fid)]) .send() @@ -59,15 +60,15 @@ impl FavoriteList { .client .request( reqwest::Method::GET, - &"https://api.bilibili.com/x/v3/fav/resource/list", + "https://api.bilibili.com/x/v3/fav/resource/list", ) .query(&[ - ("media_id", &self.fid), + ("media_id", self.fid.as_str()), ("pn", &page.to_string()), - ("ps", &"20".to_string()), - ("order", &"mtime".to_string()), - ("type", &"0".to_string()), - ("tid", &"0".to_string()), + ("ps", "20"), + ("order", "mtime"), + ("type", "0"), + ("tid", "0"), ]) .send() .await? diff --git a/src/bilibili/mod.rs b/src/bilibili/mod.rs index 8b5af79..53d6db5 100644 --- a/src/bilibili/mod.rs +++ b/src/bilibili/mod.rs @@ -3,11 +3,9 @@ mod client; mod favorite_list; mod video; -use std::error; - -type Result = std::result::Result>; - -pub use analyzer::{AudioQuality, PageAnalyzer, VideoCodecs, VideoQuality}; -pub use client::{BiliClient, Credential}; +pub use analyzer::{ + AudioQuality, BestStream, FilterOption, PageAnalyzer, VideoCodecs, VideoQuality, +}; +pub use client::{client_with_header, BiliClient, Credential}; pub use favorite_list::FavoriteList; pub use video::Video; diff --git a/src/bilibili/video.rs b/src/bilibili/video.rs index 5c37ce4..76d7945 100644 --- a/src/bilibili/video.rs +++ b/src/bilibili/video.rs @@ -4,7 +4,7 @@ use reqwest::Method; use crate::bilibili::analyzer::PageAnalyzer; use crate::bilibili::client::BiliClient; -use crate::bilibili::Result; +use crate::Result; static MASK_CODE: u64 = 2251799813685247; static XOR_CODE: u64 = 23442827791579; @@ -46,7 +46,7 @@ impl Video { pub async fn get_pages(&self) -> Result> { let mut res = self .client - .request(Method::GET, &"https://api.bilibili.com/x/player/pagelist") + .request(Method::GET, "https://api.bilibili.com/x/player/pagelist") .query(&[("aid", &self.aid), ("bvid", &self.bvid)]) .send() .await? @@ -60,7 +60,7 @@ impl Video { .client .request( Method::GET, - &"https://api.bilibili.com/x/web-interface/view/detail/tag", + "https://api.bilibili.com/x/web-interface/view/detail/tag", ) .query(&[("aid", &self.aid), ("bvid", &self.bvid)]) .send() @@ -73,10 +73,7 @@ impl Video { pub async fn get_page_analyzer(&self, page: &Page) -> Result { let mut res = self .client - .request( - Method::GET, - &"https://api.bilibili.com/x/player/wbi/playurl", - ) + .request(Method::GET, "https://api.bilibili.com/x/player/wbi/playurl") .query(&[ ("avid", self.aid.as_str()), ("cid", page.cid.to_string().as_str()), @@ -101,11 +98,11 @@ fn bvid_to_aid(bvid: &str) -> u64 { (bvid[3], bvid[9]) = (bvid[9], bvid[3]); (bvid[4], bvid[7]) = (bvid[7], bvid[4]); let mut tmp = 0u64; - for i in 3..bvid.len() { - let idx = DATA.iter().position(|&x| x == bvid[i]).unwrap(); + for char in bvid.into_iter().skip(3) { + let idx = DATA.iter().position(|&x| x == char).unwrap(); tmp = tmp * BASE + idx as u64; } - return (tmp & MASK_CODE) ^ XOR_CODE; + (tmp & MASK_CODE) ^ XOR_CODE } #[cfg(test)] diff --git a/src/downloader.rs b/src/downloader.rs new file mode 100644 index 0000000..801611b --- /dev/null +++ b/src/downloader.rs @@ -0,0 +1,65 @@ +use std::path::Path; + +use futures_util::StreamExt; +use tokio::fs::{self, File}; +use tokio::io; + +use crate::bilibili::client_with_header; +use crate::Result; +pub struct Downloader { + client: reqwest::Client, +} + +impl Downloader { + pub fn new(client: reqwest::Client) -> Self { + Self { client } + } + + pub async fn fetch(&self, url: &str, path: &Path) -> Result<()> { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).await?; + } + // must be a new file + let mut file = File::create(path).await?; + let mut res = self.client.get(url).send().await?.bytes_stream(); + while let Some(item) = res.next().await { + io::copy(&mut item?.as_ref(), &mut file).await?; + } + Ok(()) + } + + pub async fn merge( + &self, + video_path: &Path, + audio_path: &Path, + output_path: &Path, + ) -> Result<()> { + let output = tokio::process::Command::new("ffmpeg") + .args([ + "-i", + video_path.to_str().unwrap(), + "-i", + audio_path.to_str().unwrap(), + "-c", + "copy", + output_path.to_str().unwrap(), + ]) + .output() + .await?; + if !output.status.success() { + return match String::from_utf8(output.stderr) { + Ok(err) => Err(err.into()), + _ => Err("ffmpeg error".into()), + }; + } + let _ = fs::remove_file(video_path).await; + let _ = fs::remove_file(audio_path).await; + Ok(()) + } +} + +impl Default for Downloader { + fn default() -> Self { + Self::new(client_with_header()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 58c1058..9d8bf31 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1 +1,6 @@ +use std::error; + pub mod bilibili; +pub mod downloader; + +type Result = std::result::Result>; diff --git a/src/main.rs b/src/main.rs index 21a52ba..889d183 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,11 @@ +use std::path::Path; use std::rc::Rc; use bili_sync::bilibili::{ - AudioQuality, BiliClient, FavoriteList, Video, VideoCodecs, VideoQuality, + AudioQuality, BestStream, BiliClient, FavoriteList, FilterOption, Video, VideoCodecs, + VideoQuality, }; +use bili_sync::downloader::Downloader; use futures_util::{pin_mut, StreamExt}; #[tokio::main] @@ -10,36 +13,57 @@ async fn main() { let bili_client = Rc::new(BiliClient::anonymous()); let favorite_list = FavoriteList::new(bili_client.clone(), "52642258".to_string()); dbg!(favorite_list.get_info().await.unwrap()); + let video_stream = favorite_list.into_video_stream(); // from doc: https://docs.rs/async-stream/latest/async_stream/ pin_mut!(video_stream); - let mut count = 3; - let mut third_video = None; - while let Some(mut video) = video_stream.next().await { - count -= 1; - video = dbg!(video); - if count <= 0 { - third_video = Some(video); - break; - } - } - let third_video = Video::new(bili_client.clone(), third_video.unwrap().bvid); + + let third_video_info = dbg!(video_stream.skip(2).next().await.unwrap()); + let third_video = Video::new(bili_client.clone(), third_video_info.bvid); dbg!(third_video.get_tags().await.unwrap()); + let pages = dbg!(third_video.get_pages().await.unwrap()); - dbg!(third_video + let best_stream = dbg!(third_video .get_page_analyzer(&pages[0]) .await .unwrap() - .best_stream( - VideoQuality::QualityDolby, - VideoQuality::Quality360p, - AudioQuality::QualityDolby, - AudioQuality::Quality64k, - vec![VideoCodecs::HEV, VideoCodecs::AVC], - false, - false, - false, - false, - )) + .best_stream(&FilterOption { + video_max_quality: VideoQuality::QualityDolby, + video_min_quality: VideoQuality::Quality360p, + audio_max_quality: AudioQuality::QualityDolby, + audio_min_quality: AudioQuality::Quality64k, + codecs: Rc::new(vec![VideoCodecs::HEV, VideoCodecs::AVC]), + no_dolby_video: false, + no_dolby_audio: false, + no_hdr: false, + no_hires: false, + })) .unwrap(); + + let downloader = Downloader::default(); + let base = Path::new("./"); + let output_path = base.join(format!("{}.mp4", third_video_info.title)); + + match best_stream { + BestStream::Mixed(stream) => { + let url = dbg!(stream.url()); + downloader.fetch(url, &output_path).await.unwrap(); + } + BestStream::VideoAudio { video, audio } => { + let url = dbg!(video.url()); + let Some(audio) = audio else { + downloader.fetch(url, &output_path).await.unwrap(); + return; + }; + let video_path = base.join(format!("{}_video_tmp", third_video_info.title)); + downloader.fetch(url, &video_path).await.unwrap(); + let url = dbg!(audio.url()); + let audio_path = base.join(format!("{}_audio_tmp", third_video_info.title)); + downloader.fetch(url, &audio_path).await.unwrap(); + downloader + .merge(&video_path, &audio_path, &output_path) + .await + .unwrap(); + } + } }