refactor: 一些边边角角的小重构 (#213)

This commit is contained in:
ᴀᴍᴛᴏᴀᴇʀ
2025-01-13 18:57:08 +08:00
committed by GitHub
parent 7d9999d6aa
commit 54b46c150e
9 changed files with 187 additions and 79 deletions

View File

@@ -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!(),
}
}
}
}

View File

@@ -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()),
),
),
};

View File

@@ -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<String> {
get_mixin_key(self)
impl From<WbiImg> for Option<String> {
/// 尝试将 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<String> {
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<Cow<'a, str>>)>,
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<String>)>, 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<Cow<'a, str>>)>,
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::<String>()))
.map(|(k, v)| {
(
k,
// FIXME: 总感觉这里不太好,即使 v 是 &str 也会被转换成 String
v.into()
.chars()
.filter(|&x| !"!'()*".contains(x))
.collect::<String>()
.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(&params).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::<String>::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")),
]
)
);
}
}

View File

@@ -30,6 +30,7 @@ mod watch_later;
static MIXIN_KEY: Lazy<ArcSwapOption<String>> = 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 {

View File

@@ -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?

View File

@@ -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?

View File

@@ -9,25 +9,7 @@ use crate::config::item::PathSafeTemplate;
use crate::config::Config;
/// 全局的 CONFIG可以从中读取配置信息
pub static CONFIG: Lazy<Config> = Lazy::new(|| {
let config = Config::load().unwrap_or_else(|err| {
if err
.downcast_ref::<std::io::Error>()
.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<Config> = Lazy::new(load_config);
/// 全局的 TEMPLATE用来渲染 video_name 和 page_name 模板
pub static TEMPLATE: Lazy<handlebars::Handlebars> = Lazy::new(|| {
@@ -51,3 +33,52 @@ pub static ARGS: Lazy<Args> = Lazy::new(Args::parse);
/// 全局的 CONFIG_DIR表示配置文件夹的路径
pub static CONFIG_DIR: Lazy<PathBuf> =
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::<std::io::Error>()
.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()
}
}

View File

@@ -71,12 +71,6 @@ impl Default for Config {
}
impl Config {
fn load() -> Result<Self> {
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<Self> {
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 {

View File

@@ -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 签名,等待下一轮执行");