Skip to main content

bpi_rs/utils/
wbi.rs

1use serde::{ Deserialize, Serialize };
2use std::collections::{ BTreeMap, HashMap };
3use std::sync::{ RwLock, LazyLock };
4use chrono::Local;
5
6use crate::models::WbiData;
7use crate::{ BilibiliRequest, BpiClient, BpiError, BpiResponse };
8use std::time::{ SystemTime, UNIX_EPOCH };
9
10const MIXIN_KEY_TAB: [usize; 64] = [
11    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,
12    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,
13    56, 59, 6, 63, 57, 62, 11, 36, 20, 34, 44, 52,
14];
15
16pub static WBI_KEY_MAP: LazyLock<RwLock<HashMap<String, String>>> = LazyLock::new(||
17    RwLock::new(HashMap::new())
18);
19
20fn get_mixin_key(orig: &str) -> String {
21    let bytes = orig.as_bytes();
22    let mut s = Vec::new();
23    for &i in &MIXIN_KEY_TAB {
24        if i < bytes.len() {
25            s.push(bytes[i] as char);
26        }
27    }
28    s.into_iter().take(32).collect()
29}
30
31fn url_encode(s: &str) -> String {
32    let mut result = String::new();
33    for byte in s.bytes() {
34        match byte {
35            // 不编码的字符(字母数字和部分特殊字符)
36            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
37                result.push(byte as char);
38            }
39            // 空格编码为 %20
40            b' ' => result.push_str("%20"),
41            // 其他字符进行百分号编码,字母大写
42            _ => result.push_str(&format!("%{:02X}", byte)),
43        }
44    }
45    result
46}
47
48fn enc_wbi(params: &mut BTreeMap<String, String>, img_key: &str, sub_key: &str) {
49    let mixin_key = get_mixin_key(&(img_key.to_owned() + sub_key));
50    let wts = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
51    params.insert("wts".to_string(), wts.to_string());
52
53    // 过滤 value 中的 !'()* 字符
54    for value in params.values_mut() {
55        *value = value
56            .chars()
57            .filter(|c| !"!'()*".contains(*c))
58            .collect();
59    }
60
61    // 按 key 排序 (BTreeMap 默认排序)
62    let query = params
63        .iter()
64        .map(|(k, v)| format!("{}={}", url_encode(k), url_encode(v)))
65        .collect::<Vec<String>>()
66        .join("&");
67
68    let digest = md5::compute(format!("{}{}", query, mixin_key));
69    let w_rid = format!("{:x}", digest);
70    params.insert("w_rid".to_string(), w_rid);
71}
72
73#[derive(Deserialize, Serialize)]
74struct WbiImgData {
75    img_url: String,
76    sub_url: String,
77}
78
79#[derive(Deserialize, Serialize)]
80struct NavData {
81    wbi_img: WbiImgData,
82}
83
84impl BpiClient {
85    pub async fn get_wbi_sign(&self) -> Result<WbiData, BpiError> {
86        let mut params = BTreeMap::new();
87
88        let resp: BpiResponse<NavData> = self
89            .get("https://api.bilibili.com/x/web-interface/nav")
90            .with_bilibili_headers()
91            .send_bpi("获取 wbi 签名").await?;
92
93        let data = resp.data.ok_or_else(|| BpiError::parse("获取 wbi 签名失败"))?;
94
95        let img_key = data.wbi_img.img_url.rsplit('/').next().unwrap().split('.').next().unwrap();
96        let sub_key = data.wbi_img.sub_url.rsplit('/').next().unwrap().split('.').next().unwrap();
97
98        enc_wbi(&mut params, img_key, sub_key);
99
100        Ok(WbiData {
101            wts: params
102                .get("wts")
103                .ok_or_else(|| BpiError::parse("缺少 wts"))?
104                .parse::<u64>()
105                .map_err(|_| BpiError::parse("wts 转换失败"))?,
106            w_rid: params
107                .get("w_rid")
108                .ok_or_else(|| BpiError::parse("缺少 w_rid"))?
109                .to_string(),
110        })
111    }
112
113    pub async fn get_wbi_sign2<I, K, V>(&self, params: I) -> Result<Vec<(String, String)>, BpiError>
114        where I: IntoIterator<Item = (K, V)>, K: ToString, V: ToString
115    {
116        let now = Local::now();
117        let s = now.format("%Y-%m-%d %H").to_string();
118
119        let img_key_key = format!("{}img_key", s);
120        let sub_key_key = format!("{}sub_key", s);
121
122        // 独立作用域读缓存,guard 在 await 前彻底释放
123        let cached = {
124            let map = WBI_KEY_MAP.read().unwrap();
125            match (map.get(&img_key_key), map.get(&sub_key_key)) {
126                (Some(img), Some(sub)) => Some((img.clone(), sub.clone())),
127                _ => None,
128            }
129        };
130
131        let (img_key, sub_key) = if let Some(keys) = cached {
132            keys
133        } else {
134            // 缓存没有 -> 请求 API(此时无锁持有,可安全 await)
135            let resp: BpiResponse<NavData> = self
136                .get("https://api.bilibili.com/x/web-interface/nav")
137                .send_bpi("获取 wbi 签名").await?;
138
139            let data = resp.data.ok_or_else(|| BpiError::parse("获取 wbi 签名失败"))?;
140
141            let img = data.wbi_img.img_url
142                .rsplit('/')
143                .next()
144                .unwrap()
145                .split('.')
146                .next()
147                .unwrap()
148                .to_string();
149
150            let sub = data.wbi_img.sub_url
151                .rsplit('/')
152                .next()
153                .unwrap()
154                .split('.')
155                .next()
156                .unwrap()
157                .to_string();
158
159            // 独立作用域写缓存
160            {
161                let mut map = WBI_KEY_MAP.write().unwrap();
162                map.insert(img_key_key, img.clone());
163                map.insert(sub_key_key, sub.clone());
164            }
165
166            (img, sub)
167        };
168
169        // 构造参数
170        let mut params: BTreeMap<String, String> = params
171            .into_iter()
172            .map(|(k, v)| (k.to_string(), v.to_string()))
173            .collect();
174
175        enc_wbi(&mut params, &img_key, &sub_key);
176
177        Ok(params.into_iter().collect())
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    #[tokio::test]
186    async fn test_get_wts_and_rid2() {
187        let bpi = BpiClient::new();
188
189        let params = vec![
190            ("bvid", "BV18x411c74j".to_string()),
191            ("cid", "21448".to_string()),
192            ("up_mid", "46473".to_string()),
193            ("web_location", "0.0".to_string())
194        ];
195
196        let wbi = bpi.get_wbi_sign2(params.clone()).await.unwrap();
197        tracing::info!("{:?}", wbi);
198        tracing::info!("{:?}", WBI_KEY_MAP);
199
200        let wbi = bpi.get_wbi_sign2(params).await.unwrap();
201        tracing::info!("{:?}", wbi);
202    }
203}