1use md5;
5use reqwest::header::HeaderMap;
6use reqwest::StatusCode;
7use serde::Deserialize;
8use std::time::{SystemTime, UNIX_EPOCH};
9
10use crate::browser_cookies;
12
13pub fn get_cookies_or_browser(provided_cookie: Option<&str>) -> Option<String> {
15 log::info!("Searching for Bilibili cookies in browser (newest)...");
17 if let Some(browser_cookie) = browser_cookies::find_bilibili_cookies_as_string() {
18 log::info!("Found Bilibili cookies in browser (using newest)");
19 return Some(browser_cookie);
20 }
21
22 log::info!("No Bilibili cookies found in browser, checking provided cookie...");
23 if let Some(cookie) = provided_cookie {
24 if !cookie.is_empty() && cookie != "dummy_sessdata" && cookie.len() > 20 {
25 log::info!("Using provided cookie as fallback");
26 return Some(cookie.to_string());
27 }
28 }
29
30 log::warn!("No valid Bilibili cookies found in browser or provided input");
31 None
32}
33
34pub fn init_uid(headers: HeaderMap) -> (StatusCode, String) {
35 let client = reqwest::blocking::Client::builder()
36 .https_only(true)
37 .build()
38 .unwrap();
39
40 let mut request_headers = headers;
41 request_headers.insert("user-agent", USER_AGENT.parse().unwrap());
42
43 let response = client.get(UID_INIT_URL).headers(request_headers).send();
44 log::debug!("init uid response: {:?}", response);
45 let stat: StatusCode;
46 let body: String;
47 match response {
48 Ok(resp) => {
49 stat = resp.status();
50 body = resp.text().unwrap();
51 log::info!("init uid response: {:?}", body);
52 }
53 Err(_) => {
54 panic!("init uid failed");
55 }
56 }
57 (stat, body)
58}
59
60pub fn init_buvid(headers: HeaderMap) -> (StatusCode, String) {
68 let client = reqwest::blocking::Client::builder()
70 .https_only(true)
71 .build()
72 .unwrap();
73
74 let mut request_headers = headers;
75 request_headers.insert("user-agent", USER_AGENT.parse().unwrap());
76
77 let response = client.get(BUVID_INIT_URL).headers(request_headers).send();
78 let stat: StatusCode;
79 let mut buvid: String = "".to_string();
80 match response {
81 Ok(resp) => {
82 stat = resp.status();
83 let cookies = resp.cookies();
84 for i in cookies {
85 log::debug!("init buvid response cookie : {:?}", i);
86 if "buvid3".eq(i.name()) {
87 buvid = i.value().to_string();
88 log::info!("init buvid response: {:?}", buvid);
89 }
90 }
91 }
92 Err(_) => {
93 panic!("init buvid failed");
94 }
95 }
96 (stat, buvid)
97}
98
99pub fn init_room(headers: HeaderMap, temp_room_id: &str) -> (StatusCode, String) {
107 let client = reqwest::blocking::Client::builder()
108 .https_only(true)
109 .build()
110 .unwrap();
111
112 let mut request_headers = headers;
113 request_headers.insert("user-agent", USER_AGENT.parse().unwrap());
114
115 let url = format!("{}?room_id={}", ROOM_INIT_URL, temp_room_id);
116 let response = client.get(url).headers(request_headers).send();
117 let stat: StatusCode;
118 let body: String;
119 match response {
120 Ok(resp) => {
121 stat = resp.status();
122 body = resp.text().unwrap();
123 log::info!("init room response: {:?}", body);
124 }
125 Err(_) => {
126 panic!("init buvid failed");
127 }
128 }
129 (stat, body)
130}
131
132pub fn init_host_server(headers: HeaderMap, room_id: u64) -> (StatusCode, String) {
133 let client = reqwest::blocking::Client::builder()
134 .https_only(true)
135 .build()
136 .unwrap();
137
138 let mut request_headers = headers.clone();
139 request_headers.insert("user-agent", USER_AGENT.parse().unwrap());
140
141 let wbi_keys = match get_wbi_keys(request_headers.clone()) {
143 Ok(keys) => keys,
144 Err(e) => {
145 log::error!("Failed to get WBI keys: {:?}", e);
146 panic!("Failed to get WBI keys");
147 }
148 };
149
150 let params = vec![
152 ("id", room_id.to_string()),
153 ("type", "0".to_string()),
154 ("web_location", "444.8".to_string()),
155 ];
156
157 let signed_query = encode_wbi(params, wbi_keys);
159
160 let url = format!("{}?{}", DANMAKU_SERVER_CONF_URL, signed_query);
162
163 let response = client.get(url).headers(request_headers).send();
165 log::debug!("init host server response: {:?}", response);
166 let stat: StatusCode;
167 let body: String;
168 match response {
169 Ok(resp) => {
170 stat = resp.status();
171 body = resp.text().unwrap();
172 log::info!("init host server response body: {:?}", body);
173 }
174 Err(_) => {
175 panic!("init host server failed");
176 }
177 }
178 (stat, body)
179}
180
181const MIXIN_KEY_ENC_TAB: [usize; 64] = [
183 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,
184 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,
185 54, 21, 56, 59, 6, 63, 57, 62, 11, 36, 20, 34, 44, 52,
186];
187
188#[derive(Deserialize)]
189struct WbiImg {
190 img_url: String,
191 sub_url: String,
192}
193
194#[derive(Deserialize)]
195struct Data {
196 wbi_img: WbiImg,
197}
198
199#[derive(Deserialize)]
200struct ResWbi {
201 data: Data,
202}
203
204fn get_mixin_key(orig: &[u8]) -> String {
206 MIXIN_KEY_ENC_TAB
207 .iter()
208 .take(32)
209 .map(|&i| orig[i] as char)
210 .collect::<String>()
211}
212
213fn get_url_encoded(s: &str) -> String {
214 s.chars()
215 .filter_map(|c| match c.is_ascii_alphanumeric() || "-_.~".contains(c) {
216 true => Some(c.to_string()),
217 false => {
218 if "!'()*".contains(c) {
220 return None;
221 }
222 let encoded = c
223 .encode_utf8(&mut [0; 4])
224 .bytes()
225 .fold("".to_string(), |acc, b| acc + &format!("%{:02X}", b));
226 Some(encoded)
227 }
228 })
229 .collect::<String>()
230}
231
232fn encode_wbi(params: Vec<(&str, String)>, (img_key, sub_key): (String, String)) -> String {
234 let cur_time = match SystemTime::now().duration_since(UNIX_EPOCH) {
235 Ok(t) => t.as_secs(),
236 Err(_) => panic!("SystemTime before UNIX EPOCH!"),
237 };
238 _encode_wbi(params, (img_key, sub_key), cur_time)
239}
240
241fn _encode_wbi(
242 mut params: Vec<(&str, String)>,
243 (img_key, sub_key): (String, String),
244 timestamp: u64,
245) -> String {
246 let mixin_key = get_mixin_key((img_key + &sub_key).as_bytes());
247 params.push(("wts", timestamp.to_string()));
249 params.sort_by(|a, b| a.0.cmp(b.0));
251 let query = params
253 .iter()
254 .map(|(k, v)| format!("{}={}", get_url_encoded(k), get_url_encoded(v)))
255 .collect::<Vec<_>>()
256 .join("&");
257 let web_sign = format!("{:x}", md5::compute(query.clone() + &mixin_key));
259 query + &format!("&w_rid={}", web_sign)
261}
262
263fn get_wbi_keys(headers: HeaderMap) -> Result<(String, String), reqwest::Error> {
264 let client = reqwest::blocking::Client::builder()
265 .https_only(true)
266 .build()
267 .unwrap();
268
269 let mut request_headers = headers;
270 request_headers.insert("user-agent", USER_AGENT.parse().unwrap());
271
272 let response = client
273 .get("https://api.bilibili.com/x/web-interface/nav")
274 .headers(request_headers)
275 .send()?;
276
277 let res_wbi: ResWbi = response.json()?;
278 Ok((
279 take_filename(res_wbi.data.wbi_img.img_url).unwrap(),
280 take_filename(res_wbi.data.wbi_img.sub_url).unwrap(),
281 ))
282}
283
284fn take_filename(url: String) -> Option<String> {
285 url.rsplit_once('/')
286 .and_then(|(_, s)| s.rsplit_once('.'))
287 .map(|(s, _)| s.to_string())
288}
289
290pub const UID_INIT_URL: &str = "https://api.bilibili.com/x/web-interface/nav";
291pub const BUVID_INIT_URL: &str = "https://data.bilibili.com/v/";
292pub const ROOM_INIT_URL: &str =
293 "https://api.live.bilibili.com/xlive/web-room/v1/index/getInfoByRoom";
294pub const DANMAKU_SERVER_CONF_URL: &str =
295 "https://api.live.bilibili.com/xlive/web-room/v1/index/getDanmuInfo";
296pub const USER_AGENT: &str =
297 "Mozilla/5.0 (X11; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0";
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302
303 #[test]
304 fn test_uid_url_constant() {
305 assert!(UID_INIT_URL.contains("bilibili.com"));
306 }
307
308 #[test]
309 fn test_take_filename() {
310 assert_eq!(
311 take_filename(
312 "https://i0.hdslb.com/bfs/wbi/7cd084941338484aae1ad9425b84077c.png".to_string()
313 ),
314 Some("7cd084941338484aae1ad9425b84077c".to_string())
315 );
316
317 assert_eq!(
318 take_filename(
319 "https://i0.hdslb.com/bfs/wbi/4932caff0ff746eab6f01bf08b70ac45.png".to_string()
320 ),
321 Some("4932caff0ff746eab6f01bf08b70ac45".to_string())
322 );
323
324 assert_eq!(
326 take_filename("https://example.com/path/file".to_string()),
327 None
328 );
329 }
330
331 #[test]
332 fn test_encode_wbi_with_known_values() {
333 let params = vec![
334 ("foo", String::from("114")),
335 ("bar", String::from("514")),
336 ("zab", String::from("1919810")),
337 ];
338
339 let result = _encode_wbi(
340 params,
341 (
342 "7cd084941338484aae1ad9425b84077c".to_string(),
343 "4932caff0ff746eab6f01bf08b70ac45".to_string(),
344 ),
345 1702204169,
346 );
347
348 assert_eq!(
349 result,
350 "bar=514&foo=114&wts=1702204169&zab=1919810&w_rid=8f6f2b5b3d485fe1886cec6a0be8c5d4"
351 );
352 }
353
354 #[test]
355 fn test_encode_wbi_bilibili_danmu_params() {
356 let params = vec![
358 ("id", String::from("24779526")),
359 ("type", String::from("0")),
360 ("web_location", String::from("444.8")),
361 ];
362
363 let result = _encode_wbi(
365 params,
366 (
367 "7cd084941338484aae1ad9425b84077c".to_string(),
368 "4932caff0ff746eab6f01bf08b70ac45".to_string(),
369 ),
370 1748308267,
371 );
372
373 assert!(result.contains("id=24779526"));
375 assert!(result.contains("type=0"));
376 assert!(result.contains("web_location=444.8"));
377 assert!(result.contains("wts=1748308267"));
378 assert!(result.contains("w_rid="));
379
380 let expected_order = "id=24779526&type=0&web_location=444.8&wts=1748308267&w_rid=";
382 assert!(result.starts_with(expected_order));
383 }
384
385 #[test]
386 fn test_wbi_signature_consistency() {
387 let params1 = vec![
389 ("id", String::from("24779526")),
390 ("type", String::from("0")),
391 ("web_location", String::from("444.8")),
392 ];
393
394 let params2 = vec![
395 ("id", String::from("24779526")),
396 ("type", String::from("0")),
397 ("web_location", String::from("444.8")),
398 ];
399
400 let keys = (
401 "7cd084941338484aae1ad9425b84077c".to_string(),
402 "4932caff0ff746eab6f01bf08b70ac45".to_string(),
403 );
404
405 let timestamp = 1748308267;
406
407 let result1 = _encode_wbi(params1, keys.clone(), timestamp);
408 let result2 = _encode_wbi(params2, keys, timestamp);
409
410 assert_eq!(result1, result2);
411 }
412
413 #[test]
414 fn test_wbi_parameter_sorting() {
415 let params = vec![
417 ("z_param", String::from("last")),
418 ("a_param", String::from("first")),
419 ("m_param", String::from("middle")),
420 ];
421
422 let result = _encode_wbi(
423 params,
424 (
425 "7cd084941338484aae1ad9425b84077c".to_string(),
426 "4932caff0ff746eab6f01bf08b70ac45".to_string(),
427 ),
428 1748308267,
429 );
430
431 let parts: Vec<&str> = result.split('&').collect();
433 assert!(parts[0].starts_with("a_param="));
434 assert!(parts[1].starts_with("m_param="));
435 assert!(parts[2].starts_with("wts="));
436 assert!(parts[3].starts_with("z_param="));
437 assert!(parts[4].starts_with("w_rid="));
438 }
439
440 #[test]
441 fn test_correct_bilibili_url_signature() {
442 let params = vec![
447 ("id", String::from("24779526")),
448 ("type", String::from("0")),
449 ("web_location", String::from("444.8")),
450 ];
451
452 let result = _encode_wbi(
453 params,
454 (
455 "7cd084941338484aae1ad9425b84077c".to_string(),
456 "4932caff0ff746eab6f01bf08b70ac45".to_string(),
457 ),
458 1751072897,
459 );
460
461 let expected = "id=24779526&type=0&web_location=444.8&wts=1751072897&w_rid=d1e619744b4977f88ed67524a1f567cc";
463 assert_eq!(result, expected);
464
465 let w_rid = result.split("w_rid=").nth(1).unwrap();
467 assert_eq!(w_rid, "d1e619744b4977f88ed67524a1f567cc");
468 }
469
470 #[test]
471 fn test_second_bilibili_url_signature() {
472 let params = vec![
476 ("id", String::from("24779526")),
477 ("type", String::from("0")),
478 ("web_location", String::from("444.8")),
479 ];
480
481 let result = _encode_wbi(
482 params,
483 (
484 "7cd084941338484aae1ad9425b84077c".to_string(),
485 "4932caff0ff746eab6f01bf08b70ac45".to_string(),
486 ),
487 1748311635,
488 );
489
490 let expected = "id=24779526&type=0&web_location=444.8&wts=1748311635&w_rid=fa20533eb27334ba6f2ec7263721319a";
492 assert_eq!(result, expected);
493
494 let w_rid = result.split("w_rid=").nth(1).unwrap();
496 assert_eq!(w_rid, "fa20533eb27334ba6f2ec7263721319a");
497 }
498
499 #[test]
500 fn test_third_bilibili_url_signature() {
501 let params = vec![
505 ("id", String::from("24779526")),
506 ("type", String::from("0")),
507 ("web_location", String::from("444.8")),
508 ];
509
510 let result = _encode_wbi(
511 params,
512 (
513 "7cd084941338484aae1ad9425b84077c".to_string(),
514 "4932caff0ff746eab6f01bf08b70ac45".to_string(),
515 ),
516 1748312554,
517 );
518
519 let expected = "id=24779526&type=0&web_location=444.8&wts=1748312554&w_rid=30f250e8abd9effea1bcb88aab416507";
521 assert_eq!(result, expected);
522
523 let w_rid = result.split("w_rid=").nth(1).unwrap();
525 assert_eq!(w_rid, "30f250e8abd9effea1bcb88aab416507");
526 }
527}