1use crate::{ BpiError, response::BpiResponse };
2use reqwest::RequestBuilder;
3use serde::de::DeserializeOwned;
4use tokio::time::Instant;
5use tracing;
6
7#[cfg(any(test, debug_assertions))]
9fn floor_char_boundary(s: &str, index: usize) -> usize {
10 if index >= s.len() {
11 return s.len();
12 }
13 let mut i = index;
14 while i > 0 && !s.is_char_boundary(i) {
15 i -= 1;
16 }
17 i
18}
19
20pub trait BilibiliRequest {
21 fn with_bilibili_headers(self) -> Self;
22 fn with_user_agent(self) -> Self;
23
24 fn send_request(
25 self,
26 operation_name: &str
27 ) -> impl std::future::Future<Output = Result<bytes::Bytes, BpiError>> + Send;
28
29 fn send_bpi<T>(
30 self,
31 operation_name: &str
32 )
33 -> impl std::future::Future<Output = Result<BpiResponse<T>, BpiError>> + Send
34 where Self: Sized + Send, T: DeserializeOwned;
35
36 fn log_url(self, operation_name: &str) -> Self;
37}
38
39impl BilibiliRequest for RequestBuilder {
40 fn with_bilibili_headers(self) -> Self {
42 self.with_user_agent()
43 .header("Referer", "https://www.bilibili.com/")
44 .header("Origin", "https://www.bilibili.com")
45 }
46
47 fn with_user_agent(self) -> Self {
48 self.header(
49 "User-Agent",
50 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
51 )
52 }
53
54 async fn send_request(self, operation_name: &str) -> Result<bytes::Bytes, BpiError> {
55 let response = self.send().await.map_err(|e| {
57 tracing::error!("{} 请求失败: {}", operation_name, e);
58 BpiError::from(e) })?;
60
61 let status = response.status();
63 if !status.is_success() {
64 let err = BpiError::http(status.as_u16());
65 tracing::error!("{} HTTP错误: {}", operation_name, err);
66 return Err(err);
67 }
68
69 response.bytes().await.map_err(|e| {
71 tracing::error!("{} 获取响应体失败: {}", operation_name, e);
72 BpiError::network(format!("获取响应体失败: {}", e))
73 })
74 }
75
76 async fn send_bpi<T>(self, operation_name: &str) -> Result<BpiResponse<T>, BpiError>
77 where T: DeserializeOwned
78 {
79 let start = Instant::now();
81 let bytes = self.log_url(operation_name).send_request(operation_name).await?;
83
84 let result: BpiResponse<T> = serde_json::from_slice(&bytes).map_err(|e| {
86 #[cfg(any(test, debug_assertions))]
87 {
88 let json_str = String::from_utf8_lossy(&bytes);
89 let error_pos = e.column().saturating_sub(1);
90 let start = floor_char_boundary(&json_str, error_pos.saturating_sub(25));
91 let end = floor_char_boundary(&json_str, (error_pos + 25).min(json_str.len()));
92 let context = &json_str[start..end];
93 tracing::error!(
94 "{} JSON解析失败 (行:{} 列:{}): {}",
95 operation_name,
96 e.line(),
97 e.column(),
98 e
99 );
100 tracing::error!(
101 "错误位置: ...{}... ({}^)",
102 context,
103 " ".repeat(error_pos.saturating_sub(start))
104 );
105 }
106 #[cfg(not(any(test, debug_assertions)))]
107 {
108 tracing::error!("{} JSON解析失败: {}", operation_name, e);
109 }
110 BpiError::from(e)
111 })?;
112
113 if result.code != 0 {
115 let err = if result.message.is_empty() || result.message == "0" {
116 BpiError::from_code(result.code)
117 } else {
118 BpiError::from_code_message(result.code, result.message.clone())
119 };
120
121 tracing::error!("{} API错误: {}", operation_name, err);
122 return Err(err);
123 }
124
125 let duration = start.elapsed();
126 tracing::info!("{} 请求成功,耗时: {:.2?}", operation_name, duration);
127 Ok(result)
128 }
129
130 fn log_url(self, operation_name: &str) -> Self {
131 let url = self
132 .try_clone() .and_then(|rb| rb.build().ok())
134 .map(|req| req.url().to_string())
135 .unwrap_or_else(|| "未知URL".to_string());
136
137 tracing::info!("开始请求 {}: {}", operation_name, url);
138
139 self
140 }
141}