Skip to main content

bpi_rs/
request.rs

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    /// UserAgent + Referer + Origin
42    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}