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        // 先尝试从缓存读取
123        let (img_key, sub_key) = {
124            let map = WBI_KEY_MAP.read().unwrap();
125            if let (Some(img), Some(sub)) = (map.get(&img_key_key), map.get(&sub_key_key)) {
126                (img.clone(), sub.clone())
127            } else {
128                drop(map); // 释放读锁再去写
129
130                // 缓存没有 -> 请求 API
131                let resp: BpiResponse<NavData> = self
132                    .get("https://api.bilibili.com/x/web-interface/nav")
133                    .send_bpi("获取 wbi 签名").await?;
134
135                let data = resp.data.ok_or_else(|| BpiError::parse("获取 wbi 签名失败"))?;
136
137                let img = data.wbi_img.img_url
138                    .rsplit('/')
139                    .next()
140                    .unwrap()
141                    .split('.')
142                    .next()
143                    .unwrap()
144                    .to_string();
145
146                let sub = data.wbi_img.sub_url
147                    .rsplit('/')
148                    .next()
149                    .unwrap()
150                    .split('.')
151                    .next()
152                    .unwrap()
153                    .to_string();
154
155                // 插入缓存
156                let mut map = WBI_KEY_MAP.write().unwrap();
157                map.insert(img_key_key.clone(), img.clone());
158                map.insert(sub_key_key.clone(), sub.clone());
159
160                (img, sub)
161            }
162        };
163
164        // 构造参数
165        let mut params: BTreeMap<String, String> = params
166            .into_iter()
167            .map(|(k, v)| (k.to_string(), v.to_string()))
168            .collect();
169
170        enc_wbi(&mut params, &img_key, &sub_key);
171
172        Ok(params.into_iter().collect())
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[tokio::test]
181    async fn test_get_wts_and_rid2() {
182        let bpi = BpiClient::new();
183
184        let params = vec![
185            ("bvid", "BV18x411c74j".to_string()),
186            ("cid", "21448".to_string()),
187            ("up_mid", "46473".to_string()),
188            ("web_location", "0.0".to_string())
189        ];
190
191        let wbi = bpi.get_wbi_sign2(params.clone()).await.unwrap();
192        tracing::info!("{:?}", wbi);
193        tracing::info!("{:?}", WBI_KEY_MAP);
194
195        let wbi = bpi.get_wbi_sign2(params).await.unwrap();
196        tracing::info!("{:?}", wbi);
197    }
198}