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