refactor: 一些边边角角的小重构 (#213)
This commit is contained in:
@@ -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!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()),
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
@@ -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(¶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::<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")),
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 签名,等待下一轮执行");
|
||||
|
||||
Reference in New Issue
Block a user