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 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 let response = self.send().await.map_err(|e| {
49 tracing::error!("{} 请求失败: {}", operation_name, e);
50 BpiError::from(e) })?;
52
53 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 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 let start = Instant::now();
73 let bytes = self.log_url(operation_name).send_request(operation_name).await?;
75
76 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 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() .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) .deflate(true) .brotli(true) .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 #[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 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 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 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 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 fn clear_cookies(&self) {
236 tracing::info!("清空 cookies(需要重置整个客户端)");
239 }
240
241 pub fn set_account_from_cookie_str(&self, cookie_str: &str) {
242 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 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 pub fn get_account(&self) -> Option<Account> {
271 self.account.lock().unwrap().clone()
272 }
273
274 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 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}