1use crate::{
2 BpiError,
3 response::ApiEnvelope,
4 transport::{ReqwestTransport, TransportEnvelope, TransportResponse},
5};
6use reqwest::RequestBuilder;
7use serde::de::DeserializeOwned;
8use tokio::time::Instant;
9use tracing;
10
11#[cfg(any(test, debug_assertions))]
13fn floor_char_boundary(s: &str, index: usize) -> usize {
14 if index >= s.len() {
15 return s.len();
16 }
17 let mut i = index;
18 while i > 0 && !s.is_char_boundary(i) {
19 i -= 1;
20 }
21 i
22}
23
24pub trait BilibiliRequest {
25 fn with_bilibili_headers(self) -> Self;
26 fn with_user_agent(self) -> Self;
27
28 fn send_request(
29 self,
30 operation_name: &str,
31 ) -> impl std::future::Future<Output = Result<bytes::Bytes, BpiError>> + Send;
32
33 fn send_bpi_payload<T>(
34 self,
35 operation_name: &str,
36 ) -> impl std::future::Future<Output = Result<T, BpiError>> + Send
37 where
38 Self: Sized + Send,
39 T: DeserializeOwned;
40
41 fn send_bpi_optional_payload<T>(
42 self,
43 operation_name: &str,
44 ) -> impl std::future::Future<Output = Result<Option<T>, BpiError>> + Send
45 where
46 Self: Sized + Send,
47 T: DeserializeOwned;
48
49 fn log_url(self, operation_name: &str) -> Self;
50}
51
52impl BilibiliRequest for RequestBuilder {
53 fn with_bilibili_headers(self) -> Self {
55 self.with_user_agent()
56 .header("Referer", "https://www.bilibili.com/")
57 .header("Origin", "https://www.bilibili.com")
58 }
59
60 fn with_user_agent(self) -> Self {
61 self.header(
62 "User-Agent",
63 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
64 )
65 }
66
67 async fn send_request(self, operation_name: &str) -> Result<bytes::Bytes, BpiError> {
68 ReqwestTransport::send_request_builder(self, operation_name)
69 .await
70 .map(|response| response.body)
71 }
72
73 async fn send_bpi_payload<T>(self, operation_name: &str) -> Result<T, BpiError>
74 where
75 T: DeserializeOwned,
76 {
77 let start = Instant::now();
78 let response =
79 ReqwestTransport::send_request_builder(self.log_url(operation_name), operation_name)
80 .await?;
81 let result = decode_bpi_payload_response(operation_name, &response)?;
82
83 log_success(operation_name, start);
84 Ok(result)
85 }
86
87 async fn send_bpi_optional_payload<T>(self, operation_name: &str) -> Result<Option<T>, BpiError>
88 where
89 T: DeserializeOwned,
90 {
91 let start = Instant::now();
92 let response =
93 ReqwestTransport::send_request_builder(self.log_url(operation_name), operation_name)
94 .await?;
95 let result = decode_bpi_optional_payload_response(operation_name, &response)?;
96
97 log_success(operation_name, start);
98 Ok(result)
99 }
100
101 fn log_url(self, operation_name: &str) -> Self {
102 tracing::info!("开始请求 {}", operation_name);
103
104 self
105 }
106}
107
108pub(crate) async fn send_bpi_envelope<T>(
109 request: RequestBuilder,
110 operation_name: &str,
111) -> Result<ApiEnvelope<T>, BpiError>
112where
113 T: DeserializeOwned,
114{
115 let start = Instant::now();
116 let response =
117 ReqwestTransport::send_request_builder(request.log_url(operation_name), operation_name)
118 .await?;
119 let result = decode_bpi_envelope_response(operation_name, &response)?;
120
121 log_success(operation_name, start);
122 Ok(result)
123}
124
125fn decode_bpi_envelope_response<T>(
126 operation_name: &str,
127 response: &TransportResponse,
128) -> Result<ApiEnvelope<T>, BpiError>
129where
130 T: DeserializeOwned,
131{
132 decode_bpi_transport_response(operation_name, response, |decoded| {
133 decoded.into_api_envelope()
134 })
135}
136
137fn decode_bpi_payload_response<T>(
138 operation_name: &str,
139 response: &TransportResponse,
140) -> Result<T, BpiError>
141where
142 T: DeserializeOwned,
143{
144 decode_bpi_transport_response(operation_name, response, |decoded| {
145 decoded.into_payload().map(|payload| payload.payload)
146 })
147}
148
149fn decode_bpi_optional_payload_response<T>(
150 operation_name: &str,
151 response: &TransportResponse,
152) -> Result<Option<T>, BpiError>
153where
154 T: DeserializeOwned,
155{
156 decode_bpi_transport_response(operation_name, response, |decoded| {
157 decoded
158 .into_optional_payload()
159 .map(|payload| payload.payload)
160 })
161}
162
163fn decode_bpi_transport_response<T, R>(
164 operation_name: &str,
165 response: &TransportResponse,
166 extract: impl FnOnce(TransportEnvelope<T>) -> Result<R, BpiError>,
167) -> Result<R, BpiError>
168where
169 T: DeserializeOwned,
170{
171 match response.decode_api_envelope::<T>().and_then(extract) {
172 Ok(result) => Ok(result),
173 Err(err) => {
174 if let BpiError::Decode { source } = &err {
175 log_decode_error(operation_name, &response.body, source);
176 } else {
177 tracing::error!("{} API错误: {}", operation_name, err);
178 }
179 Err(err)
180 }
181 }
182}
183
184fn log_success(operation_name: &str, start: Instant) {
185 let duration = start.elapsed();
186 tracing::info!("{} 请求成功,耗时: {:.2?}", operation_name, duration);
187}
188
189fn log_decode_error(operation_name: &str, bytes: &[u8], error: &serde_json::Error) {
190 #[cfg(any(test, debug_assertions))]
191 {
192 let json_str = String::from_utf8_lossy(bytes);
193 let error_pos = error.column().saturating_sub(1);
194 let start = floor_char_boundary(&json_str, error_pos.saturating_sub(25));
195 let end = floor_char_boundary(&json_str, (error_pos + 25).min(json_str.len()));
196 let context = &json_str[start..end];
197 tracing::error!(
198 "{} JSON解析失败 (行:{} 列:{}): {}",
199 operation_name,
200 error.line(),
201 error.column(),
202 error
203 );
204 tracing::error!(
205 "错误位置: ...{}... ({}^)",
206 context,
207 " ".repeat(error_pos.saturating_sub(start))
208 );
209 }
210 #[cfg(not(any(test, debug_assertions)))]
211 {
212 tracing::error!("{} JSON解析失败: {}", operation_name, error);
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use std::time::Duration;
219
220 use bytes::Bytes;
221 use serde::Deserialize;
222
223 use super::*;
224 use crate::transport::{ResponseMetadata, TransportResponse};
225 use crate::{BpiError, BpiResult};
226
227 #[derive(Debug, Deserialize, PartialEq, Eq)]
228 struct Payload {
229 value: u64,
230 }
231
232 #[test]
233 fn decode_bpi_payload_response_returns_required_payload() -> BpiResult<()> {
234 let payload = decode_bpi_payload_response::<Payload>(
235 "unit",
236 &response(br#"{ "code": 0, "data": { "value": 42 } }"#),
237 )?;
238
239 assert_eq!(payload.value, 42);
240 Ok(())
241 }
242
243 #[test]
244 fn decode_bpi_payload_response_rejects_missing_required_payload() {
245 let err = decode_bpi_payload_response::<Payload>(
246 "unit",
247 &response(br#"{ "code": 0, "message": "0" }"#),
248 )
249 .unwrap_err();
250
251 assert!(matches!(err, BpiError::MissingData));
252 }
253
254 #[test]
255 fn decode_bpi_optional_payload_response_allows_missing_payload() -> BpiResult<()> {
256 let payload = decode_bpi_optional_payload_response::<Payload>(
257 "unit",
258 &response(br#"{ "code": 0, "message": "0" }"#),
259 )?;
260
261 assert!(payload.is_none());
262 Ok(())
263 }
264
265 fn response(body: &'static [u8]) -> TransportResponse {
266 TransportResponse {
267 metadata: ResponseMetadata {
268 status: 200,
269 duration: Duration::from_millis(1),
270 api_code: None,
271 },
272 body: Bytes::from_static(body),
273 }
274 }
275}