mirror of
https://github.com/lanyeeee/bilibili-video-downloader.git
synced 2026-09-05 07:27:42 +08:00
124 lines
4.2 KiB
Rust
124 lines
4.2 KiB
Rust
use std::time::{SystemTime, UNIX_EPOCH};
|
|
|
|
use eyre::{OptionExt, WrapErr, eyre};
|
|
use md5::{Digest, Md5};
|
|
use serde::Deserialize;
|
|
use tracing::instrument;
|
|
|
|
use crate::bili_client::{BiliClient, BiliResp};
|
|
|
|
const MIXIN_KEY_ENC_TAB: [usize; 64] = [
|
|
46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49, 33, 9, 42, 19, 29,
|
|
28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40, 61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25,
|
|
54, 21, 56, 59, 6, 63, 57, 62, 11, 36, 20, 34, 44, 52,
|
|
];
|
|
|
|
#[derive(Deserialize)]
|
|
struct WbiImgRespData {
|
|
img_url: String,
|
|
sub_url: String,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct WeiRespData {
|
|
wbi_img: WbiImgRespData,
|
|
}
|
|
|
|
impl BiliClient {
|
|
// 为请求参数进行 wbi 签名
|
|
#[instrument(level = "error", skip_all)]
|
|
pub async fn wbi(&self, params: &mut Vec<(&str, String)>) -> eyre::Result<()> {
|
|
let (img_key, sub_key) = self.get_wbi_keys().await.wrap_err("获取wbi keys失败")?;
|
|
let mixin_key = get_mixin_key((img_key + &sub_key).as_bytes());
|
|
|
|
let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
|
|
// 添加当前时间戳
|
|
params.push(("wts", timestamp.to_string()));
|
|
// 重新排序
|
|
params.sort_by(|a, b| a.0.cmp(b.0));
|
|
// 拼接参数
|
|
let query = params
|
|
.iter()
|
|
.map(|(k, v)| format!("{}={}", get_url_encoded(k), get_url_encoded(v)))
|
|
.collect::<Vec<_>>()
|
|
.join("&");
|
|
// 计算签名
|
|
let web_sign = format!("{:x}", Md5::digest(query.clone() + &mixin_key));
|
|
params.push(("w_rid", web_sign));
|
|
Ok(())
|
|
}
|
|
|
|
#[instrument(level = "error", skip_all)]
|
|
async fn get_wbi_keys(&self) -> eyre::Result<(String, String)> {
|
|
let request = self
|
|
.api_client
|
|
.read()
|
|
.get("https://api.bilibili.com/x/web-interface/nav")
|
|
.header("Cookie", &self.get_cookie());
|
|
|
|
let http_resp = request.send().await?;
|
|
// 检查http响应状态码
|
|
let status = http_resp.status();
|
|
let body = http_resp.text().await?;
|
|
if status != reqwest::StatusCode::OK {
|
|
return Err(eyre!("预料之外的状态码({status}): {body}"));
|
|
}
|
|
// 尝试将body解析为BiliResp
|
|
let bili_resp: BiliResp =
|
|
serde_json::from_str(&body).wrap_err(format!("将body解析为BiliResp失败: {body}"))?;
|
|
// 检查BiliResp的data是否存在
|
|
let Some(data) = bili_resp.data else {
|
|
return Err(eyre!("BiliResp中不存在data字段: {bili_resp:?}"));
|
|
};
|
|
// 尝试将data解析为Data
|
|
let data_str = data.to_string();
|
|
let wei_resp_data: WeiRespData = serde_json::from_str(&data_str)
|
|
.wrap_err(format!("将data解析为Data失败: {data_str}"))?;
|
|
|
|
let img_url = wei_resp_data.wbi_img.img_url;
|
|
let sub_url = wei_resp_data.wbi_img.sub_url;
|
|
|
|
let img_filename =
|
|
take_filename(&img_url).ok_or_eyre(format!("从img_url中提取文件名失败: {img_url}"))?;
|
|
let sub_filename =
|
|
take_filename(&sub_url).ok_or_eyre(format!("从sub_url中提取文件名失败: {sub_url}"))?;
|
|
|
|
Ok((img_filename, sub_filename))
|
|
}
|
|
}
|
|
|
|
// 对 imgKey 和 subKey 进行字符顺序打乱编码
|
|
fn get_mixin_key(orig: &[u8]) -> String {
|
|
MIXIN_KEY_ENC_TAB
|
|
.iter()
|
|
.take(32)
|
|
.map(|&i| orig[i] as char)
|
|
.collect::<String>()
|
|
}
|
|
|
|
fn get_url_encoded(s: &str) -> String {
|
|
s.chars()
|
|
.filter_map(|c| {
|
|
if c.is_ascii_alphanumeric() || "-_.~".contains(c) {
|
|
Some(c.to_string())
|
|
} else {
|
|
// 过滤 value 中的 "!'()*" 字符
|
|
if "!'()*".contains(c) {
|
|
return None;
|
|
}
|
|
let encoded = c
|
|
.encode_utf8(&mut [0; 4])
|
|
.bytes()
|
|
.fold(String::new(), |acc, b| acc + &format!("%{b:02X}"));
|
|
Some(encoded)
|
|
}
|
|
})
|
|
.collect::<String>()
|
|
}
|
|
|
|
fn take_filename(url: &str) -> Option<String> {
|
|
url.rsplit_once('/')
|
|
.and_then(|(_, s)| s.rsplit_once('.'))
|
|
.map(|(s, _)| s.to_string())
|
|
}
|