Skip to main content

bpi_rs/
request.rs

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