diff --git a/src/dl.rs b/src/dl.rs index 882bfb9..cce5ca3 100644 --- a/src/dl.rs +++ b/src/dl.rs @@ -71,15 +71,19 @@ pub async fn download(url: &str) -> Result { let av = match info.best_av_format() { Some(av) => av, None => { - event!(Level::WARN, "no best format found for {}, reverting to default", url); + event!( + Level::WARN, + "no best format found for {}, reverting to default", + url + ); match info.default_format() { Some(format) => format, None => { event!(Level::ERROR, "no formats found for {}", url); - return Err(DownloadError::NoFormatFound) + return Err(DownloadError::NoFormatFound); } } - }, + } }; let output_path = make_download_path(&info, &av)?; diff --git a/src/dl/ffmpeg.rs b/src/dl/ffmpeg.rs index 33290fd..2ff1349 100644 --- a/src/dl/ffmpeg.rs +++ b/src/dl/ffmpeg.rs @@ -37,6 +37,38 @@ impl FFMpeg { Ok(()) } + + pub async fn join_audio_video( + video_path: &str, + audio_path: &str, + abr: u16, + output_path: &str, + ) -> Result<(), SpawnError> { + let abr = format!("{}k", abr); + let output = spawn( + "ffmpeg", + &[ + "-i", + video_path, + "-i", + audio_path, + "-c", + "copy", + "-map", + "0:v:0", + "-map", + "1:a:0", + "-c:a", + "aac", + "-b:a", + &abr, + output_path, + ], + ) + .await?; + + Ok(()) + } } #[cfg(test)] diff --git a/src/dl/yt_dlp.rs b/src/dl/yt_dlp.rs index 83f69aa..f9b12ab 100644 --- a/src/dl/yt_dlp.rs +++ b/src/dl/yt_dlp.rs @@ -22,8 +22,20 @@ pub struct YtDlpFormat { struct VideoFormat<'a> { pub format: &'a YtDlpFormat, + pub format_note: &'a String, pub width: u16, pub height: u16, + pub vbr: f32, +} + +impl<'a> VideoFormat<'a> { + pub fn is_mp4(&self) -> bool { + self.format.ext == "mp4" + } + + pub fn is_premium(&self) -> bool { + self.format_note.contains("Premium") + } } struct AudioFormat<'a> { @@ -79,7 +91,7 @@ pub struct YtDlpInfo { } impl YtDlpInfo { - const H_LIMIT: u16 = 720; + const H_LIMIT: u16 = 1080; pub fn parse(json: &[u8]) -> Result { let mut info: YtDlpInfo = serde_json::from_slice(json)?; @@ -102,6 +114,10 @@ impl YtDlpInfo { } } + #[deprecated( + since = "0.1.1", + note = "for YouTube download audio and video separately" + )] pub fn best_av_format(&self) -> Option<&YtDlpFormat> { let format = self .formats @@ -110,8 +126,10 @@ impl YtDlpInfo { if f.vcodec.is_some() && f.acodec.is_some() { Some(VideoFormat { format: &f, + format_note: f.format_note.as_ref()?, width: f.width?, height: f.height?, + vbr: f.vbr?, }) } else { None @@ -148,6 +166,31 @@ impl YtDlpInfo { } } } + + pub fn best_video_format(&self) -> Option<&YtDlpFormat> { + let format = self + .formats + .iter() + .filter_map(|f| { + Some(VideoFormat { + format: f, + format_note: f.format_note.as_ref()?, + width: f.width?, + height: f.height?, + vbr: f.vbr?, + }) + }) + .filter(|f| f.height <= Self::H_LIMIT && f.is_mp4() && !f.is_premium()) + .max_by_key(|f| OrderedFloat(f.vbr)); + + match format { + Some(vf) => Some(vf.format), + None => { + event!(Level::ERROR, "no video format for {}", self.id); + None + } + } + } } #[derive(Debug)] @@ -250,4 +293,14 @@ mod tests { let video = info.best_audio_format().unwrap(); assert_eq!(video.format_id, "140"); } + + #[tokio::test] + async fn best_video_format() { + dotenv::from_filename(".env.test").unwrap(); + let info = YtDlp::load_info(env::var("TEST_URL").unwrap().as_str()) + .await + .unwrap(); + let video = info.best_video_format().unwrap(); + assert_eq!(video.format_id, "137"); + } }