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