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