bpi_rs/
client.rs

1use crate::{ BpiError, response::BpiResponse };
2use reqwest::RequestBuilder;
3use reqwest::cookie::CookieStore;
4use reqwest::{ Client, Url, cookie::Jar };
5use serde::de::DeserializeOwned;
6use std::sync::{ Arc, Mutex };
7use tokio::time::Instant;
8use tracing;
9
10use super::auth::Account;
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<T>(
22        self,
23        operation_name: &str
24    )
25        -> impl std::future::Future<Output = Result<BpiResponse<T>, BpiError>> + Send
26        where Self: Sized + Send, T: DeserializeOwned;
27
28    fn log_url(self, operation_name: &str) -> Self;
29}
30
31impl BilibiliRequest for RequestBuilder {
32    /// UserAgent + Referer + Origin
33    fn with_bilibili_headers(self) -> Self {
34        self.with_user_agent()
35            .header("Referer", "https://www.bilibili.com/")
36            .header("Origin", "https://www.bilibili.com")
37    }
38
39    fn with_user_agent(self) -> Self {
40        self.header(
41            "User-Agent",
42            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
43        )
44    }
45
46    async fn send_request(self, operation_name: &str) -> Result<bytes::Bytes, BpiError> {
47        // 发送请求
48        let response = self.send().await.map_err(|e| {
49            tracing::error!("{} 请求失败: {}", operation_name, e);
50            BpiError::from(e) // 使用 From trait 自动转换
51        })?;
52
53        // 检查响应状态
54        let status = response.status();
55        if !status.is_success() {
56            let err = BpiError::http(status.as_u16());
57            tracing::error!("{} HTTP错误: {}", operation_name, err);
58            return Err(err);
59        }
60
61        // 获取响应体
62        response.bytes().await.map_err(|e| {
63            tracing::error!("{} 获取响应体失败: {}", operation_name, e);
64            BpiError::network(format!("获取响应体失败: {}", e))
65        })
66    }
67
68    async fn send_bpi<T>(self, operation_name: &str) -> Result<BpiResponse<T>, BpiError>
69        where T: DeserializeOwned
70    {
71        // 开始计时
72        let start = Instant::now();
73        // 请求拿到响应 bytes
74        let bytes = self.log_url(operation_name).send_request(operation_name).await?;
75
76        // 解析JSON响应
77        let result: BpiResponse<T> = serde_json::from_slice(&bytes).map_err(|e| {
78            #[cfg(any(test, debug_assertions))]
79            {
80                let json_str = String::from_utf8_lossy(&bytes);
81                let error_pos = e.column().saturating_sub(1);
82                let start = error_pos.saturating_sub(25);
83                let end = (error_pos + 25).min(json_str.len());
84                let context = &json_str[start..end];
85
86                tracing::error!(
87                    "{} JSON解析失败 (行:{} 列:{}): {}",
88                    operation_name,
89                    e.line(),
90                    e.column(),
91                    e
92                );
93                tracing::error!(
94                    "错误位置: ...{}... ({}^)",
95                    context,
96                    " ".repeat(error_pos.saturating_sub(start))
97                );
98            }
99            #[cfg(not(any(test, debug_assertions)))]
100            {
101                tracing::error!("{} JSON解析失败: {}", operation_name, e);
102            }
103            BpiError::from(e)
104        })?;
105
106        // 处理API业务错误
107        if result.code != 0 {
108            let err = if result.message.is_empty() || result.message == "0" {
109                BpiError::from_code(result.code)
110            } else {
111                BpiError::from_code_message(result.code, result.message.clone())
112            };
113
114            tracing::error!("{} API错误: {}", operation_name, err);
115            return Err(err);
116        }
117
118        let duration = start.elapsed();
119        tracing::info!("{} 请求成功,耗时: {:.2?}", operation_name, duration);
120        Ok(result)
121    }
122
123    fn log_url(self, operation_name: &str) -> Self {
124        let url = self
125            .try_clone() // 注意:这里用不到也行,直接 build 也可以
126            .and_then(|rb| rb.build().ok())
127            .map(|req| req.url().to_string())
128            .unwrap_or_else(|| "未知URL".to_string());
129
130        tracing::info!("开始请求 {}: {}", operation_name, url);
131
132        self
133    }
134}
135
136pub struct BpiClient {
137    client: Client,
138    jar: Arc<Jar>,
139    account: Mutex<Option<Account>>,
140}
141
142impl BpiClient {
143    pub fn new() -> &'static Self {
144        static INSTANCE: std::sync::OnceLock<BpiClient> = std::sync::OnceLock::new();
145        INSTANCE.get_or_init(|| {
146            let jar = Arc::new(Jar::default());
147            let client = Client::builder()
148                .timeout(std::time::Duration::from_secs(10))
149                .gzip(true) // 启用gzip自动解压缩
150                .deflate(true) // 启用deflate解压缩
151                .brotli(true) // 启用brotli解压缩
152                .no_proxy()
153                .cookie_provider(jar.clone())
154                .pool_max_idle_per_host(0)
155                .build()
156                .unwrap();
157
158            let instance = Self {
159                client,
160                jar,
161                account: Mutex::new(None),
162            };
163
164            // 在 debug 模式下自动加载测试账号
165            #[cfg(any(test, debug_assertions))]
166            {
167                use super::log::init_log;
168
169                init_log();
170                if let Ok(test_account) = Account::load_test_account() {
171                    instance.set_account(test_account);
172                    tracing::info!("已自动加载测试账号");
173                } else {
174                    tracing::warn!("无法加载测试账号,使用默认配置");
175                }
176            }
177
178            instance
179        })
180    }
181
182    /// 设置账号信息
183    pub fn set_account(&self, account: Account) {
184        if account.is_complete() {
185            self.load_cookies_from_account(&account);
186            let mut acc = self.account.lock().unwrap();
187            *acc = Some(account);
188            tracing::info!("设置账号信息完成,使用[登录]模式");
189        } else {
190            tracing::warn!("账号信息不完整,使用[游客]模式");
191        }
192    }
193
194    /// 从账号信息设置登录 cookies
195    fn load_cookies_from_account(&self, account: &Account) {
196        tracing::info!("开始从账号信息加载cookies...");
197
198        let cookies = vec![
199            ("DedeUserID", account.dede_user_id.clone()),
200            ("DedeUserID__ckMd5", account.dede_user_id_ckmd5.clone()),
201            ("SESSDATA", account.sessdata.clone()),
202            ("bili_jct", account.bili_jct.clone()),
203            ("buvid3", account.buvid3.clone())
204        ];
205        self.add_cookies(cookies);
206        tracing::info!("从账号信息加载登录 cookies 完成");
207    }
208
209    /// 清除账号信息
210    pub fn clear_account(&self) {
211        let mut acc = self.account.lock().unwrap();
212        *acc = None;
213        self.clear_cookies();
214        tracing::info!("清除账号信息完成");
215    }
216
217    fn add_cookie_pair(&self, key: &str, value: &str) {
218        let url = Url::parse("https://www.bilibili.com").unwrap();
219        let cookie = format!("{}={}; Domain=.bilibili.com; Path=/", key, value);
220        self.jar.add_cookie_str(&cookie, &url);
221        tracing::debug!("添加 cookie: {} = {}", key, value);
222    }
223
224    /// 批量添加 cookies
225    fn add_cookies<I, K, V>(&self, cookies: I)
226        where I: IntoIterator<Item = (K, V)>, K: ToString, V: ToString
227    {
228        for (key, value) in cookies {
229            self.add_cookie_pair(&key.to_string(), &value.to_string());
230        }
231    }
232
233    /// 清空所有 cookies
234    /// todo
235    fn clear_cookies(&self) {
236        // 注意:reqwest 的 Jar 没有直接的 clear 方法
237        // 这里需要重新创建 jar,但由于 Arc 的限制,需要在上层重置整个 Bpi
238        tracing::info!("清空 cookies(需要重置整个客户端)");
239    }
240
241    pub fn set_account_from_cookie_str(&self, cookie_str: &str) {
242        // 先解析成 map
243        let mut map = std::collections::HashMap::new();
244        for kv in cookie_str.split(';') {
245            let kv = kv.trim();
246            if let Some(pos) = kv.find('=') {
247                let (key, value) = kv.split_at(pos);
248                map.insert(key.trim().to_string(), value[1..].trim().to_string());
249            }
250        }
251
252        let account = Account {
253            dede_user_id: map.get("DedeUserID").cloned().unwrap_or_default(),
254            dede_user_id_ckmd5: map.get("DedeUserID__ckMd5").cloned().unwrap_or_default(),
255            sessdata: map.get("SESSDATA").cloned().unwrap_or_default(),
256            bili_jct: map.get("bili_jct").cloned().unwrap_or_default(),
257            buvid3: map.get("buvid3").cloned().unwrap_or_default(),
258        };
259
260        self.set_account(account);
261    }
262
263    /// 检查是否有登录 cookies
264    pub fn has_login_cookies(&self) -> bool {
265        let url = Url::parse("https://api.bilibili.com").unwrap();
266        self.jar.cookies(&url).is_some()
267    }
268
269    /// 获取当前账号信息
270    pub fn get_account(&self) -> Option<Account> {
271        self.account.lock().unwrap().clone()
272    }
273
274    /// 获取 CSRF token
275    pub fn csrf(&self) -> Result<String, BpiError> {
276        let account = self.account.lock().unwrap();
277        account
278            .as_ref()
279            .filter(|acc| !acc.bili_jct.is_empty())
280            .map(|acc| acc.bili_jct.clone())
281            .ok_or_else(BpiError::missing_csrf)
282    }
283
284    pub fn get(&self, url: &str) -> RequestBuilder {
285        self.client.get(url).with_user_agent()
286    }
287
288    pub fn post(&self, url: &str) -> RequestBuilder {
289        self.client.post(url).with_user_agent()
290    }
291}
292
293impl BpiClient {
294    /// 从配置创建(如果你仍然需要从外部配置加载)
295    pub fn from_config(config: &Account) -> &Self {
296        let bpi = Self::new();
297
298        if
299            !config.dede_user_id.is_empty() &&
300            !config.sessdata.is_empty() &&
301            !config.bili_jct.is_empty() &&
302            !config.buvid3.is_empty()
303        {
304            let account = Account::new(
305                config.dede_user_id.clone(),
306                config.dede_user_id_ckmd5.clone(),
307                config.sessdata.clone(),
308                config.bili_jct.clone(),
309                config.buvid3.clone()
310            );
311            bpi.set_account(account);
312        }
313
314        bpi
315    }
316}