From 54b46c150ee155d963c79496fb34e2342e8f0093 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=B4=80=E1=B4=8D=E1=B4=9B=E1=B4=8F=E1=B4=80=E1=B4=87?= =?UTF-8?q?=CA=80?= Date: Mon, 13 Jan 2025 18:57:08 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E4=B8=80=E4=BA=9B=E8=BE=B9?= =?UTF-8?q?=E8=BE=B9=E8=A7=92=E8=A7=92=E7=9A=84=E5=B0=8F=E9=87=8D=E6=9E=84?= =?UTF-8?q?=20(#213)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/bili_sync/src/bilibili/analyzer.rs | 64 +++++++++++++- crates/bili_sync/src/bilibili/collection.rs | 4 +- crates/bili_sync/src/bilibili/credential.rs | 93 ++++++++++++--------- crates/bili_sync/src/bilibili/mod.rs | 16 ++-- crates/bili_sync/src/bilibili/submission.rs | 2 +- crates/bili_sync/src/bilibili/video.rs | 2 +- crates/bili_sync/src/config/global.rs | 69 ++++++++++----- crates/bili_sync/src/config/mod.rs | 14 ++-- crates/bili_sync/src/main.rs | 2 +- 9 files changed, 187 insertions(+), 79 deletions(-) diff --git a/crates/bili_sync/src/bilibili/analyzer.rs b/crates/bili_sync/src/bilibili/analyzer.rs index ec8562f..4fae9a0 100644 --- a/crates/bili_sync/src/bilibili/analyzer.rs +++ b/crates/bili_sync/src/bilibili/analyzer.rs @@ -189,10 +189,18 @@ impl PageAnalyzer { }; let quality = VideoQuality::from_repr(quality as usize).ok_or(anyhow!("invalid video stream quality"))?; // 从视频流的 codecs 字段中获取编码格式,此处并非精确匹配而是判断包含,比如 codecs 是 av1.42c01e,需要匹配为 av1 - let codecs = [VideoCodecs::HEV, VideoCodecs::AVC, VideoCodecs::AV1] + let codecs = match [VideoCodecs::HEV, VideoCodecs::AVC, VideoCodecs::AV1] .into_iter() .find(|c| codecs.contains(c.as_ref())) - .ok_or(anyhow!("invalid video stream codecs"))?; + { + Some(codecs) => codecs, + None => { + // 极少数情况会走到此处,打印一条日志并跳过, + // 如 BV1Mm4y1P7JV 存在 codecs 为 dvh1.08.09 的视频流 + warn!("unknown video codecs: {}", codecs); + continue; + } + }; if !filter_option.codecs.contains(&codecs) || quality < filter_option.video_min_quality || quality > filter_option.video_max_quality @@ -302,6 +310,8 @@ impl PageAnalyzer { #[cfg(test)] mod tests { use super::*; + use crate::bilibili::{BiliClient, Video}; + use crate::config::CONFIG; #[test] fn test_quality_order() { @@ -327,4 +337,54 @@ mod tests { ] .is_sorted()); } + + #[ignore = "only for manual test"] + #[tokio::test] + async fn test_best_stream() { + let testcases = [ + // 随便一个 8k + hires 视频 + ( + "BV1xRChYUE2R", + VideoQuality::Quality8k, + Some(AudioQuality::QualityHiRES), + ), + // 一个没有声音的纯视频 + ("BV1J7411H7KQ", VideoQuality::Quality720p, None), + // 一个杜比全景声的演示片 + ( + "BV1Mm4y1P7JV", + VideoQuality::Quality4k, + Some(AudioQuality::QualityDolby), + ), + ]; + for (bvid, video_quality, audio_quality) in testcases.into_iter() { + let client = BiliClient::new(); + let video = Video::new(&client, bvid.to_owned()); + let pages = video.get_pages().await.expect("failed to get pages"); + let first_page = pages.into_iter().next().expect("no page found"); + let best_stream = video + .get_page_analyzer(&first_page) + .await + .expect("failed to get page analyzer") + .best_stream(&CONFIG.filter_option) + .expect("failed to get best stream"); + dbg!(bvid, &best_stream); + match best_stream { + BestStream::VideoAudio { + video: Stream::DashVideo { quality, .. }, + audio, + } => { + assert_eq!(quality, video_quality); + assert_eq!( + audio.map(|audio_stream| match audio_stream { + Stream::DashAudio { quality, .. } => quality, + _ => unreachable!(), + }), + audio_quality, + ); + } + _ => unreachable!(), + } + } + } } diff --git a/crates/bili_sync/src/bilibili/collection.rs b/crates/bili_sync/src/bilibili/collection.rs index c80bb15..3db9f23 100644 --- a/crates/bili_sync/src/bilibili/collection.rs +++ b/crates/bili_sync/src/bilibili/collection.rs @@ -133,7 +133,7 @@ impl<'a> Collection<'a> { ("pn", page.as_str()), ("ps", "30"), ], - MIXIN_KEY.load().as_ref().unwrap(), + MIXIN_KEY.load().as_deref().map(|x| x.as_str()), ), ), CollectionType::Season => ( @@ -146,7 +146,7 @@ impl<'a> Collection<'a> { ("page_num", page.as_str()), ("page_size", "30"), ], - MIXIN_KEY.load().as_ref().unwrap(), + MIXIN_KEY.load().as_deref().map(|x| x.as_str()), ), ), }; diff --git a/crates/bili_sync/src/bilibili/credential.rs b/crates/bili_sync/src/bilibili/credential.rs index c0f830a..6ee412e 100644 --- a/crates/bili_sync/src/bilibili/credential.rs +++ b/crates/bili_sync/src/bilibili/credential.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::collections::HashSet; use anyhow::{anyhow, bail, Result}; @@ -32,9 +33,18 @@ pub struct WbiImg { sub_url: String, } -impl WbiImg { - pub fn into_mixin_key(self) -> Option { - get_mixin_key(self) +impl From for Option { + /// 尝试将 WbiImg 转换成 mixin_key + fn from(value: WbiImg) -> Self { + let key = match ( + get_filename(value.img_url.as_str()), + get_filename(value.sub_url.as_str()), + ) { + (Some(img_key), Some(sub_key)) => img_key.to_string() + sub_key, + _ => return None, + }; + let key = key.as_bytes(); + Some(MIXIN_KEY_ENC_TAB.iter().take(32).map(|&x| key[x] as char).collect()) } } @@ -198,32 +208,40 @@ fn get_filename(url: &str) -> Option<&str> { .map(|(s, _)| s) } -fn get_mixin_key(wbi_img: WbiImg) -> Option { - let key = match ( - get_filename(wbi_img.img_url.as_str()), - get_filename(wbi_img.sub_url.as_str()), - ) { - (Some(img_key), Some(sub_key)) => img_key.to_string() + sub_key, - _ => return None, - }; - let key = key.as_bytes(); - Some(MIXIN_KEY_ENC_TAB.iter().take(32).map(|&x| key[x] as char).collect()) +pub fn encoded_query<'a>( + params: Vec<(&'a str, impl Into>)>, + mixin_key: Option<&str>, +) -> Vec<(&'a str, Cow<'a, str>)> { + match mixin_key { + Some(key) => _encoded_query(params, key, chrono::Local::now().timestamp().to_string()), + None => params.into_iter().map(|(k, v)| (k, v.into())).collect(), + } } -pub fn encoded_query<'a>(params: Vec<(&'a str, impl Into)>, mixin_key: &str) -> Vec<(&'a str, String)> { - let params = params.into_iter().map(|(k, v)| (k, v.into())).collect(); - _encoded_query(params, mixin_key, chrono::Local::now().timestamp().to_string()) -} - -fn _encoded_query<'a>(params: Vec<(&'a str, String)>, mixin_key: &str, timestamp: String) -> Vec<(&'a str, String)> { - let mut params: Vec<(&'a str, String)> = params +#[inline] +fn _encoded_query<'a>( + params: Vec<(&'a str, impl Into>)>, + mixin_key: &str, + timestamp: String, +) -> Vec<(&'a str, Cow<'a, str>)> { + let mut params: Vec<(&'a str, Cow<'a, str>)> = params .into_iter() - .map(|(k, v)| (k, v.chars().filter(|&x| !"!'()*".contains(x)).collect::())) + .map(|(k, v)| { + ( + k, + // FIXME: 总感觉这里不太好,即使 v 是 &str 也会被转换成 String + v.into() + .chars() + .filter(|&x| !"!'()*".contains(x)) + .collect::() + .into(), + ) + }) .collect(); - params.push(("wts", timestamp)); + params.push(("wts", timestamp.into())); params.sort_by(|a, b| a.0.cmp(b.0)); let query = serde_urlencoded::to_string(¶ms).unwrap().replace('+', "%20"); - params.push(("w_rid", format!("{:x}", md5::compute(query.clone() + mixin_key)))); + params.push(("w_rid", format!("{:x}", md5::compute(query.clone() + mixin_key)).into())); params } @@ -265,25 +283,22 @@ mod tests { img_url: "https://i0.hdslb.com/bfs/wbi/7cd084941338484aae1ad9425b84077c.png".to_string(), sub_url: "https://i0.hdslb.com/bfs/wbi/4932caff0ff746eab6f01bf08b70ac45.png".to_string(), }; - let mixin_key = get_mixin_key(key); - assert_eq!(mixin_key, Some("ea1db124af3c7062474693fa704f4ff8".to_string())); + let key = Option::::from(key).expect("fail to convert key"); + assert_eq!(key.as_str(), "ea1db124af3c7062474693fa704f4ff8"); assert_eq!( - _encoded_query( - vec![ - ("foo", "114".to_string()), - ("bar", "514".to_string()), - ("zab", "1919810".to_string()) - ], - &mixin_key.unwrap(), + dbg!(_encoded_query( + vec![("foo", "114"), ("bar", "514"), ("zab", "1919810")], + key.as_str(), "1702204169".to_string(), - ), + )), + // 上面产生的结果全是 Cow::Owned,但 eq 只会比较值,这样写比较方便 vec![ - ("bar", "514".to_string()), - ("foo", "114".to_string()), - ("wts", "1702204169".to_string()), - ("zab", "1919810".to_string()), - ("w_rid", "8f6f2b5b3d485fe1886cec6a0be8c5d4".to_string()), + ("bar", Cow::Borrowed("514")), + ("foo", Cow::Borrowed("114")), + ("wts", Cow::Borrowed("1702204169")), + ("zab", Cow::Borrowed("1919810")), + ("w_rid", Cow::Borrowed("8f6f2b5b3d485fe1886cec6a0be8c5d4")), ] - ) + ); } } diff --git a/crates/bili_sync/src/bilibili/mod.rs b/crates/bili_sync/src/bilibili/mod.rs index f92d490..c5bcdc8 100644 --- a/crates/bili_sync/src/bilibili/mod.rs +++ b/crates/bili_sync/src/bilibili/mod.rs @@ -30,6 +30,7 @@ mod watch_later; static MIXIN_KEY: Lazy> = Lazy::new(Default::default); +#[inline] pub(crate) fn set_global_mixin_key(key: String) { MIXIN_KEY.store(Some(Arc::new(key))); } @@ -140,19 +141,18 @@ mod tests { use futures::{pin_mut, StreamExt}; use super::*; + use crate::utils::init_logger; #[ignore = "only for manual test"] #[tokio::test] async fn test_video_info_type() { + init_logger("None,bili_sync=debug"); let bili_client = BiliClient::new(); - set_global_mixin_key( - bili_client - .wbi_img() - .await - .map(|x| x.into_mixin_key()) - .unwrap() - .unwrap(), - ); + // 请求 UP 主视频必须要获取 mixin key,使用 key 计算请求参数的签名,否则直接提示权限不足返回空 + let Ok(Some(mixin_key)) = bili_client.wbi_img().await.map(|wbi_img| wbi_img.into()) else { + panic!("获取 mixin key 失败"); + }; + set_global_mixin_key(mixin_key); let video = Video::new(&bili_client, "BV1Z54y1C7ZB".to_string()); assert!(matches!(video.get_view_info().await, Ok(VideoInfo::View { .. }))); let collection_item = CollectionItem { diff --git a/crates/bili_sync/src/bilibili/submission.rs b/crates/bili_sync/src/bilibili/submission.rs index 98db162..39aab4f 100644 --- a/crates/bili_sync/src/bilibili/submission.rs +++ b/crates/bili_sync/src/bilibili/submission.rs @@ -47,7 +47,7 @@ impl<'a> Submission<'a> { ("pn", page.to_string()), ("ps", "30".to_string()), ], - MIXIN_KEY.load().as_ref().unwrap(), + MIXIN_KEY.load().as_deref().map(|x| x.as_str()), )) .send() .await? diff --git a/crates/bili_sync/src/bilibili/video.rs b/crates/bili_sync/src/bilibili/video.rs index caa07a6..a85eac4 100644 --- a/crates/bili_sync/src/bilibili/video.rs +++ b/crates/bili_sync/src/bilibili/video.rs @@ -154,7 +154,7 @@ impl<'a> Video<'a> { ("fnval", "4048"), ("fourk", "1"), ], - MIXIN_KEY.load().as_ref().unwrap(), + MIXIN_KEY.load().as_deref().map(|x| x.as_str()), )) .send() .await? diff --git a/crates/bili_sync/src/config/global.rs b/crates/bili_sync/src/config/global.rs index 90d9fab..42e91c6 100644 --- a/crates/bili_sync/src/config/global.rs +++ b/crates/bili_sync/src/config/global.rs @@ -9,25 +9,7 @@ use crate::config::item::PathSafeTemplate; use crate::config::Config; /// 全局的 CONFIG,可以从中读取配置信息 -pub static CONFIG: Lazy = Lazy::new(|| { - let config = Config::load().unwrap_or_else(|err| { - if err - .downcast_ref::() - .is_none_or(|e| e.kind() != std::io::ErrorKind::NotFound) - { - panic!("加载配置文件失败,错误为: {err}"); - } - warn!("配置文件不存在,使用默认配置..."); - Config::default() - }); - // 放到外面,确保新的配置项被保存 - info!("配置加载完毕,覆盖刷新原有配置"); - config.save().unwrap(); - // 检查配置文件内容 - info!("校验配置文件内容..."); - config.check(); - config -}); +pub static CONFIG: Lazy = Lazy::new(load_config); /// 全局的 TEMPLATE,用来渲染 video_name 和 page_name 模板 pub static TEMPLATE: Lazy = Lazy::new(|| { @@ -51,3 +33,52 @@ pub static ARGS: Lazy = Lazy::new(Args::parse); /// 全局的 CONFIG_DIR,表示配置文件夹的路径 pub static CONFIG_DIR: Lazy = Lazy::new(|| dirs::config_dir().expect("No config path found").join("bili-sync")); + +#[cfg(not(test))] +#[inline] +fn load_config() -> Config { + let config = Config::load().unwrap_or_else(|err| { + if err + .downcast_ref::() + .is_none_or(|e| e.kind() != std::io::ErrorKind::NotFound) + { + panic!("加载配置文件失败,错误为: {err}"); + } + warn!("配置文件不存在,使用默认配置..."); + Config::default() + }); + // 放到外面,确保新的配置项被保存 + info!("配置加载完毕,覆盖刷新原有配置"); + config.save().unwrap(); + // 检查配置文件内容 + info!("校验配置文件内容..."); + config.check(); + config +} + +#[cfg(test)] +#[inline] +fn load_config() -> Config { + let credential = match ( + std::env::var("TEST_SESSDATA"), + std::env::var("TEST_BILI_JCT"), + std::env::var("TEST_BUVID3"), + std::env::var("TEST_DEDEUSERID"), + std::env::var("TEST_AC_TIME_VALUE"), + ) { + (Ok(sessdata), Ok(bili_jct), Ok(buvid3), Ok(dedeuserid), Ok(ac_time_value)) => { + Some(std::sync::Arc::new(crate::bilibili::Credential { + sessdata, + bili_jct, + buvid3, + dedeuserid, + ac_time_value, + })) + } + _ => None, + }; + Config { + credential: arc_swap::ArcSwapOption::from(credential), + ..Default::default() + } +} diff --git a/crates/bili_sync/src/config/mod.rs b/crates/bili_sync/src/config/mod.rs index 1da9222..129926a 100644 --- a/crates/bili_sync/src/config/mod.rs +++ b/crates/bili_sync/src/config/mod.rs @@ -71,12 +71,6 @@ impl Default for Config { } impl Config { - fn load() -> Result { - let config_path = CONFIG_DIR.join("config.toml"); - let config_content = std::fs::read_to_string(config_path)?; - Ok(toml::from_str(&config_content)?) - } - pub fn save(&self) -> Result<()> { let config_path = CONFIG_DIR.join("config.toml"); std::fs::create_dir_all(&*CONFIG_DIR)?; @@ -84,6 +78,14 @@ impl Config { Ok(()) } + #[cfg(not(test))] + fn load() -> Result { + let config_path = CONFIG_DIR.join("config.toml"); + let config_content = std::fs::read_to_string(config_path)?; + Ok(toml::from_str(&config_content)?) + } + + #[cfg(not(test))] pub fn check(&self) { let mut ok = true; if self.favorite_list.is_empty() && self.collection_list.is_empty() && !self.watch_later.enabled { diff --git a/crates/bili_sync/src/main.rs b/crates/bili_sync/src/main.rs index bbb7359..c26c9b1 100644 --- a/crates/bili_sync/src/main.rs +++ b/crates/bili_sync/src/main.rs @@ -31,7 +31,7 @@ async fn main() { let watch_later_config = &CONFIG.watch_later; loop { 'inner: { - match bili_client.wbi_img().await.map(|wbi_img| wbi_img.into_mixin_key()) { + match bili_client.wbi_img().await.map(|wbi_img| wbi_img.into()) { Ok(Some(mixin_key)) => bilibili::set_global_mixin_key(mixin_key), Ok(_) => { error!("获取 mixin key 失败,无法进行 wbi 签名,等待下一轮执行");