Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 1 commit into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 62 additions & 2 deletions crates/bili_sync/src/bilibili/analyzer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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() {
Expand All @@ -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!(),
}
}
}
}
4 changes: 2 additions & 2 deletions crates/bili_sync/src/bilibili/collection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 => (
Expand All @@ -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()),
),
),
};
Expand Down
93 changes: 54 additions & 39 deletions crates/bili_sync/src/bilibili/credential.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::borrow::Cow;
use std::collections::HashSet;

use anyhow::{anyhow, bail, Result};
Expand Down Expand Up @@ -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())
}
}

Expand Down Expand Up @@ -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<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())
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(),
}
}

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
}

Expand Down Expand Up @@ -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")),
]
)
);
}
}
16 changes: 8 additions & 8 deletions crates/bili_sync/src/bilibili/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
}
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion crates/bili_sync/src/bilibili/submission.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
2 changes: 1 addition & 1 deletion crates/bili_sync/src/bilibili/video.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
69 changes: 50 additions & 19 deletions crates/bili_sync/src/config/global.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(|| {
Expand All @@ -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()
}
}
14 changes: 8 additions & 6 deletions crates/bili_sync/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,19 +71,21 @@ 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)?;
std::fs::write(config_path, toml::to_string_pretty(self)?)?;
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 {
Expand Down
2 changes: 1 addition & 1 deletion crates/bili_sync/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 签名,等待下一轮执行");
Expand Down
Loading