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 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
37 result.push(byte as char);
38 }
39 b' ' => result.push_str("%20"),
41 _ => 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 for value in params.values_mut() {
55 *value = value
56 .chars()
57 .filter(|c| !"!'()*".contains(*c))
58 .collect();
59 }
60
61 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 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 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 {
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 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}