use anyhow::{anyhow, bail, Result}; use serde::{Deserialize, Serialize}; use crate::bilibili::error::BiliError; pub struct PageAnalyzer { info: serde_json::Value, } #[derive(Debug, strum::FromRepr, PartialEq, PartialOrd, Serialize, Deserialize)] pub enum VideoQuality { Quality360p = 16, Quality480p = 32, Quality720p = 64, Quality1080p = 80, Quality1080pPLUS = 112, Quality1080p60 = 116, Quality4k = 120, QualityHdr = 125, QualityDolby = 126, Quality8k = 127, } #[derive(Debug, strum::FromRepr, PartialEq, PartialOrd, Serialize, Deserialize)] pub enum AudioQuality { Quality64k = 30216, Quality132k = 30232, QualityDolby = 30250, QualityHiRES = 30251, Quality192k = 30280, } #[allow(clippy::upper_case_acronyms)] #[derive(Debug, strum::EnumString, strum::Display, PartialEq, PartialOrd, Serialize, Deserialize)] pub enum VideoCodecs { #[strum(serialize = "hev")] HEV, #[strum(serialize = "avc")] AVC, #[strum(serialize = "av01")] AV1, } // 视频流的筛选偏好 #[derive(Serialize, Deserialize)] 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: Vec, pub no_dolby_video: bool, pub no_dolby_audio: bool, pub no_hdr: bool, pub no_hires: bool, } impl Default for FilterOption { fn default() -> Self { Self { video_max_quality: VideoQuality::Quality8k, video_min_quality: VideoQuality::Quality360p, audio_max_quality: AudioQuality::QualityHiRES, audio_min_quality: AudioQuality::Quality64k, codecs: vec![VideoCodecs::AV1, VideoCodecs::HEV, VideoCodecs::AVC], no_dolby_video: false, no_dolby_audio: false, no_hdr: false, no_hires: false, } } } // 上游项目中的五种流类型,不过目测应该只有 Flv、DashVideo、DashAudio 三种会被用到 #[derive(Debug, PartialEq, PartialOrd)] pub enum Stream { Flv(String), Html5Mp4(String), EpositeTryMp4(String), DashVideo { url: String, quality: VideoQuality, codecs: VideoCodecs, }, DashAudio { url: String, quality: AudioQuality, }, } // 通用的获取流链接的方法,交由 Downloader 使用 impl Stream { pub fn url(&self) -> &str { match self { Self::Flv(url) => url, Self::Html5Mp4(url) => url, Self::EpositeTryMp4(url) => url, Self::DashVideo { url, .. } => url, Self::DashAudio { url, .. } => url, } } } /// 用于获取视频流的最佳筛选结果,有两种可能: /// 1. 单个混合流,作为 Mixed 返回 /// 2. 视频、音频分离,作为 VideoAudio 返回,其中音频流可能不存在(对于无声视频,如 BV1J7411H7KQ) #[derive(Debug)] pub enum BestStream { VideoAudio { video: Stream, audio: Option }, Mixed(Stream), } impl PageAnalyzer { pub fn new(info: serde_json::Value) -> Self { Self { info } } 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") } 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() } 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()) } fn streams(&mut self, filter_option: &FilterOption) -> Result> { if self.is_flv_stream() { return Ok(vec![Stream::Flv( self.info["durl"][0]["url"] .as_str() .ok_or(anyhow!("invalid flv stream"))? .to_string(), )]); } if self.is_html5_mp4_stream() { return Ok(vec![Stream::Html5Mp4( self.info["durl"][0]["url"] .as_str() .ok_or(anyhow!("invalid html5 mp4 stream"))? .to_string(), )]); } if self.is_episode_try_mp4_stream() { return Ok(vec![Stream::EpositeTryMp4( self.info["durl"][0]["url"] .as_str() .ok_or(anyhow!("invalid episode try mp4 stream"))? .to_string(), )]); } 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. 视频质量不在指定范围内 { continue; } let video_codecs = video_data["codecs"].as_str().unwrap(); // 从视频流的 codecs 字段中获取编码格式,此处并非精确匹配而是判断包含,比如 codecs 是 av1.42c01e,需要匹配为 av1 let video_codecs = vec![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) { continue; } streams.push(Stream::DashVideo { url: video_stream_url, quality: video_stream_quality, codecs: video_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 { continue; }; if audio_stream_quality > filter_option.audio_max_quality || audio_stream_quality < filter_option.audio_min_quality { continue; } streams.push(Stream::DashAudio { url: audio_stream_url, quality: audio_stream_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, }); } 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::DashAudio { url: dolby_stream_url, quality: dolby_stream_quality, }); } } Ok(streams) } 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() { // 按照 streams 中的假设,符合这三种情况的流只有一个,直接取 return Ok(BestStream::Mixed(streams.into_iter().next().unwrap())); } // 将视频流和音频流拆分,分别做排序 let (mut video_streams, mut audio_streams): (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; } if b_quality == &AudioQuality::QualityDolby && !filter_option.no_dolby_audio { return std::cmp::Ordering::Less; } 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(), }) } }