From dee93bf27fb1745124c773721cef21721d694c1d Mon Sep 17 00:00:00 2001 From: amtoaer Date: Mon, 13 Jan 2025 01:05:45 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E8=B0=83=E6=95=B4=E5=B9=B6?= =?UTF-8?q?=E9=87=8D=E6=9E=84=E8=A7=86=E9=A2=91=E9=9F=B3=E9=A2=91=E6=B5=81?= =?UTF-8?q?=E7=9A=84=E9=80=89=E6=8B=A9=E9=80=BB=E8=BE=91=EF=BC=8C=E5=BA=94?= =?UTF-8?q?=E8=AF=A5=E5=8F=AF=E4=BB=A5=E6=8F=90=E5=8D=87=E4=BA=9B=E8=AE=B8?= =?UTF-8?q?=E6=80=A7=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/bili_sync/src/bilibili/analyzer.rs | 245 ++++++++++------------ 1 file changed, 114 insertions(+), 131 deletions(-) diff --git a/crates/bili_sync/src/bilibili/analyzer.rs b/crates/bili_sync/src/bilibili/analyzer.rs index 3dbaa85..3c9c943 100644 --- a/crates/bili_sync/src/bilibili/analyzer.rs +++ b/crates/bili_sync/src/bilibili/analyzer.rs @@ -20,7 +20,8 @@ pub enum VideoQuality { QualityDolby = 126, Quality8k = 127, } -#[derive(Debug, strum::FromRepr, PartialEq, PartialOrd, Serialize, Deserialize)] + +#[derive(Debug, Clone, Copy, strum::FromRepr, PartialEq, Serialize, Deserialize)] pub enum AudioQuality { Quality64k = 30216, Quality132k = 30232, @@ -29,8 +30,25 @@ pub enum AudioQuality { Quality192k = 30280, } +impl AudioQuality { + #[inline] + pub fn as_sort_key(&self) -> isize { + match self { + // 这可以让 Dolby 和 Hi-RES 排在 192k 之后,且 Dolby 和 Hi-RES 之间的顺序不变 + Self::QualityHiRES | Self::QualityDolby => (*self as isize) + 40, + _ => *self as isize, + } + } +} + +impl PartialOrd for AudioQuality { + fn partial_cmp(&self, other: &AudioQuality) -> Option { + self.as_sort_key().partial_cmp(&other.as_sort_key()) + } +} + #[allow(clippy::upper_case_acronyms)] -#[derive(Debug, strum::EnumString, strum::Display, PartialEq, PartialOrd, Serialize, Deserialize)] +#[derive(Debug, strum::EnumString, strum::Display, strum::AsRefStr, PartialEq, PartialOrd, Serialize, Deserialize)] pub enum VideoCodecs { #[strum(serialize = "hev")] HEV, @@ -115,26 +133,22 @@ impl PageAnalyzer { } fn is_flv_stream(&self) -> bool { - self.info.get("durl").is_some() - && self.info["format"].is_string() - && self.info["format"].as_str().unwrap().starts_with("flv") + self.info.get("durl").is_some() && self.info["format"].as_str().is_some_and(|f| f.starts_with("flv")) } fn is_html5_mp4_stream(&self) -> bool { self.info.get("durl").is_some() - && self.info["format"].is_string() - && self.info["format"].as_str().unwrap().starts_with("mp4") - && self.info["is_html5"].is_boolean() - && self.info["is_html5"].as_bool().unwrap() + && self.info["format"].as_str().is_some_and(|f| f.starts_with("mp4")) + && self.info["is_html5"].as_bool().is_some_and(|b| b) } fn is_episode_try_mp4_stream(&self) -> bool { self.info.get("durl").is_some() - && self.info["format"].is_string() - && self.info["format"].as_str().unwrap().starts_with("mp4") - && !(self.info["is_html5"].is_boolean() && self.info["is_html5"].as_bool().unwrap()) + && self.info["format"].as_str().is_some_and(|f| f.starts_with("mp4")) + && self.info["is_html5"].as_bool().is_none_or(|b| !b) } + /// 获取所有的视频、音频流,并根据条件筛选 fn streams(&mut self, filter_option: &FilterOption) -> Result> { if self.is_flv_stream() { return Ok(vec![Stream::Flv( @@ -161,85 +175,78 @@ impl PageAnalyzer { )]); } let mut streams: Vec = Vec::new(); - let videos_data = self.info["dash"]["video"].take(); - let audios_data = self.info["dash"]["audio"].take(); - let flac_data = self.info["dash"]["flac"].take(); - let dolby_data = self.info["dash"]["dolby"].take(); - for video_data in videos_data.as_array().ok_or(BiliError::RiskControlOccurred)?.iter() { - 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(anyhow!("invalid video stream quality"))?; - if (video_stream_quality == VideoQuality::QualityHdr && filter_option.no_hdr) - || (video_stream_quality == VideoQuality::QualityDolby && filter_option.no_dolby_video) - || (video_stream_quality != VideoQuality::QualityDolby - && video_stream_quality != VideoQuality::QualityHdr - && (video_stream_quality < filter_option.video_min_quality - || video_stream_quality > filter_option.video_max_quality)) - // 此处过滤包含三种情况: - // 1. HDR 视频,但指定不需要 HDR - // 2. 杜比视界视频,但指定不需要杜比视界 - // 3. 视频质量不在指定范围内 - { + for video in self.info["dash"]["video"] + .as_array() + .ok_or(BiliError::RiskControlOccurred)? + .iter() + { + let (Some(url), Some(quality), Some(codecs)) = ( + video["baseUrl"].as_str(), + video["id"].as_u64(), + video["codecs"].as_str(), + ) else { continue; - } - - let video_codecs = video_data["codecs"].as_str().unwrap(); + }; + let quality = VideoQuality::from_repr(quality as usize).ok_or(anyhow!("invalid video stream quality"))?; // 从视频流的 codecs 字段中获取编码格式,此处并非精确匹配而是判断包含,比如 codecs 是 av1.42c01e,需要匹配为 av1 - let video_codecs = vec![VideoCodecs::HEV, VideoCodecs::AVC, VideoCodecs::AV1] + let codecs = [VideoCodecs::HEV, VideoCodecs::AVC, VideoCodecs::AV1] .into_iter() - .find(|c| video_codecs.contains(c.to_string().as_str())); - - let Some(video_codecs) = video_codecs else { - continue; - }; - if !filter_option.codecs.contains(&video_codecs) { + .find(|c| codecs.contains(c.as_ref())) + .ok_or(anyhow!("invalid video stream codecs"))?; + if !filter_option.codecs.contains(&codecs) + || quality < filter_option.video_min_quality + || quality > filter_option.video_max_quality + || (quality == VideoQuality::QualityHdr && filter_option.no_hdr) + || (quality == VideoQuality::QualityDolby && filter_option.no_dolby_video) + { continue; } streams.push(Stream::DashVideo { - url: video_stream_url, - quality: video_stream_quality, - codecs: video_codecs, + url: url.to_string(), + quality, + codecs, }); } - if audios_data.is_array() { - for audio_data in audios_data.as_array().unwrap().iter() { - let audio_stream_url = audio_data["baseUrl"].as_str().unwrap().to_string(); - let audio_stream_quality = AudioQuality::from_repr(audio_data["id"].as_u64().unwrap() as usize); - let Some(audio_stream_quality) = audio_stream_quality else { + if let Some(audios) = self.info["dash"]["audio"].as_array() { + for audio in audios.iter() { + let (Some(url), Some(quality)) = (audio["baseUrl"].as_str(), audio["id"].as_u64()) else { continue; }; - if audio_stream_quality > filter_option.audio_max_quality - || audio_stream_quality < filter_option.audio_min_quality - { + let quality = + AudioQuality::from_repr(quality as usize).ok_or(anyhow!("invalid audio stream quality"))?; + if quality < filter_option.audio_min_quality || quality > filter_option.audio_max_quality { continue; } streams.push(Stream::DashAudio { - url: audio_stream_url, - quality: audio_stream_quality, + url: url.to_string(), + quality, }); } } - if !(filter_option.no_hires || flac_data["audio"].is_null()) { - // 允许 hires 且存在 flac 音频流才会进来 - 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::DashAudio { - url: flac_stream_url, - quality: flac_stream_quality, - }); + let flac = &self.info["dash"]["flac"]["audio"]; + if !(filter_option.no_hires || flac.is_null()) { + let (Some(url), Some(quality)) = (flac["baseUrl"].as_str(), flac["id"].as_u64()) else { + bail!("invalid flac stream"); + }; + let quality = AudioQuality::from_repr(quality as usize).ok_or(anyhow!("invalid flac stream quality"))?; + if quality >= filter_option.audio_min_quality && quality <= filter_option.audio_max_quality { + streams.push(Stream::DashAudio { + url: url.to_string(), + quality, + }); + } } - 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(); + let dolby_audio = &self.info["dash"]["dolby"]["audio"][0]; + if !(filter_option.no_dolby_audio || dolby_audio.is_null()) { + let (Some(url), Some(quality)) = (dolby_audio["baseUrl"].as_str(), dolby_audio["id"].as_u64()) else { + bail!("invalid dolby audio stream"); + }; + let quality = + AudioQuality::from_repr(quality as usize).ok_or(anyhow!("invalid dolby audio stream quality"))?; + if quality >= filter_option.audio_min_quality && quality <= filter_option.audio_max_quality { streams.push(Stream::DashAudio { - url: dolby_stream_url, - quality: dolby_stream_quality, + url: url.to_string(), + quality, }); } } @@ -250,68 +257,44 @@ impl PageAnalyzer { let streams = self.streams(filter_option)?; if self.is_flv_stream() || self.is_html5_mp4_stream() || self.is_episode_try_mp4_stream() { // 按照 streams 中的假设,符合这三种情况的流只有一个,直接取 - return Ok(BestStream::Mixed(streams.into_iter().next().unwrap())); + return Ok(BestStream::Mixed( + streams.into_iter().next().ok_or(anyhow!("no stream found"))?, + )); } - // 将视频流和音频流拆分,分别做排序 - let (mut video_streams, mut audio_streams): (Vec<_>, Vec<_>) = + let (videos, audios): (Vec, Vec) = streams.into_iter().partition(|s| matches!(s, Stream::DashVideo { .. })); - // 因为该处的排序与筛选选项有关,因此不能在外面实现 PartialOrd trait,只能在这里写闭包 - video_streams.sort_by(|a, b| match (a, b) { - ( - Stream::DashVideo { - quality: a_quality, - codecs: a_codecs, - .. - }, - Stream::DashVideo { - quality: b_quality, - codecs: b_codecs, - .. - }, - ) => { - if a_quality == &VideoQuality::QualityDolby && !filter_option.no_dolby_video { - return std::cmp::Ordering::Greater; - } - if b_quality == &VideoQuality::QualityDolby && !filter_option.no_dolby_video { - return std::cmp::Ordering::Less; - } - if a_quality == &VideoQuality::QualityHdr && !filter_option.no_hdr { - return std::cmp::Ordering::Greater; - } - 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(); - } - // 如果视频质量相同,按照偏好的编码优先级排序 - filter_option - .codecs - .iter() - .position(|c| c == b_codecs) - .cmp(&filter_option.codecs.iter().position(|c| c == a_codecs)) - } - _ => unreachable!(), - }); - audio_streams.sort_by(|a, b| match (a, b) { - (Stream::DashAudio { quality: a_quality, .. }, Stream::DashAudio { quality: b_quality, .. }) => { - if a_quality == &AudioQuality::QualityDolby && !filter_option.no_dolby_audio { - return std::cmp::Ordering::Greater; + Ok(BestStream::VideoAudio { + video: Iterator::max_by(videos.into_iter(), |a, b| match (a, b) { + ( + Stream::DashVideo { + quality: a_quality, + codecs: a_codecs, + .. + }, + Stream::DashVideo { + quality: b_quality, + codecs: b_codecs, + .. + }, + ) => { + if a_quality != b_quality { + return a_quality.partial_cmp(b_quality).unwrap(); + }; + filter_option + .codecs + .iter() + .position(|c| c == b_codecs) + .cmp(&filter_option.codecs.iter().position(|c| c == a_codecs)) } - if b_quality == &AudioQuality::QualityDolby && !filter_option.no_dolby_audio { - return std::cmp::Ordering::Less; + _ => unreachable!(), + }) + .ok_or(anyhow!("no video stream found"))?, + audio: Iterator::max_by(audios.into_iter(), |a, b| match (a, b) { + (Stream::DashAudio { quality: a_quality, .. }, Stream::DashAudio { quality: b_quality, .. }) => { + a_quality.partial_cmp(b_quality).unwrap() } - a_quality.partial_cmp(b_quality).unwrap() - } - _ => unreachable!(), - }); - if video_streams.is_empty() { - bail!("no video stream found"); - } - Ok(BestStream::VideoAudio { - video: video_streams.remove(video_streams.len() - 1), - // 音频流可能为空,因此直接使用 pop 返回 Option - audio: audio_streams.pop(), + _ => unreachable!(), + }), }) } } From 263462a268d30ce503c8a555906a13155fed6cfd Mon Sep 17 00:00:00 2001 From: amtoaer Date: Mon, 13 Jan 2025 13:47:38 +0800 Subject: [PATCH 2/2] =?UTF-8?q?test:=20=E6=B7=BB=E5=8A=A0=E5=B0=91?= =?UTF-8?q?=E9=87=8F=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/bili_sync/src/bilibili/analyzer.rs | 30 +++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/crates/bili_sync/src/bilibili/analyzer.rs b/crates/bili_sync/src/bilibili/analyzer.rs index 3c9c943..ec8562f 100644 --- a/crates/bili_sync/src/bilibili/analyzer.rs +++ b/crates/bili_sync/src/bilibili/analyzer.rs @@ -298,3 +298,33 @@ impl PageAnalyzer { }) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_quality_order() { + assert!([ + VideoQuality::Quality360p, + VideoQuality::Quality480p, + VideoQuality::Quality720p, + VideoQuality::Quality1080p, + VideoQuality::Quality1080pPLUS, + VideoQuality::Quality1080p60, + VideoQuality::Quality4k, + VideoQuality::QualityHdr, + VideoQuality::QualityDolby, + VideoQuality::Quality8k + ] + .is_sorted()); + assert!([ + AudioQuality::Quality64k, + AudioQuality::Quality132k, + AudioQuality::Quality192k, + AudioQuality::QualityDolby, + AudioQuality::QualityHiRES, + ] + .is_sorted()); + } +}