1use crate::{ BpiError };
2use reqwest::RequestBuilder;
3use reqwest::cookie::CookieStore;
4use reqwest::{ Client, Url, cookie::Jar };
5use std::sync::{ Arc, Mutex };
6use tracing;
7
8use super::auth::Account;
9use super::request::BilibiliRequest;
10
11pub struct BpiClient {
43 client: Client,
44 jar: Arc<Jar>,
45 account: Mutex<Option<Account>>,
46}
47
48impl BpiClient {
49 pub fn new() -> &'static Self {
51 static INSTANCE: std::sync::OnceLock<BpiClient> = std::sync::OnceLock::new();
52 INSTANCE.get_or_init(|| {
53 let jar = Arc::new(Jar::default());
54 let client = Client::builder()
55 .timeout(std::time::Duration::from_secs(10))
56 .gzip(true) .deflate(true) .brotli(true) .no_proxy()
60 .cookie_provider(jar.clone())
61 .pool_max_idle_per_host(0)
62 .build()
63 .unwrap();
64
65 let instance = Self {
66 client,
67 jar,
68 account: Mutex::new(None),
69 };
70
71 #[cfg(any(test, debug_assertions))]
73 {
74 use super::log::init_log;
75
76 init_log();
77 if let Ok(test_account) = Account::load_test_account() {
78 instance.set_account(test_account);
79 tracing::info!("已自动加载测试账号");
80 } else {
81 tracing::warn!("无法加载测试账号,使用默认配置");
82 }
83 }
84
85 instance
86 })
87 }
88
89 pub fn set_account(&self, account: Account) {
91 if account.is_complete() {
92 self.load_cookies_from_account(&account);
93 let mut acc = self.account.lock().unwrap();
94 *acc = Some(account);
95 tracing::info!("设置账号信息完成,使用[登录]模式");
96 } else {
97 tracing::warn!("账号信息不完整,使用[游客]模式");
98 }
99 }
100
101 fn load_cookies_from_account(&self, account: &Account) {
103 tracing::info!("开始从账号信息加载cookies...");
104
105 let cookies = vec![
106 ("DedeUserID", account.dede_user_id.clone()),
107 ("DedeUserID__ckMd5", account.dede_user_id_ckmd5.clone()),
108 ("SESSDATA", account.sessdata.clone()),
109 ("bili_jct", account.bili_jct.clone()),
110 ("buvid3", account.buvid3.clone())
111 ];
112 self.add_cookies(cookies);
113 tracing::info!("从账号信息加载登录 cookies 完成");
114 }
115
116 pub fn clear_account(&self) {
118 let mut acc = self.account.lock().unwrap();
119 *acc = None;
120 self.clear_cookies();
121 tracing::info!("清除账号信息完成");
122 }
123
124 fn add_cookie_pair(&self, key: &str, value: &str) {
125 let url = Url::parse("https://www.bilibili.com").unwrap();
126 let cookie = format!("{}={}; Domain=.bilibili.com; Path=/", key, value);
127 self.jar.add_cookie_str(&cookie, &url);
128 tracing::debug!("添加 cookie: {} = {}", key, value);
129 }
130
131 fn add_cookies<I, K, V>(&self, cookies: I)
133 where I: IntoIterator<Item = (K, V)>, K: ToString, V: ToString
134 {
135 for (key, value) in cookies {
136 self.add_cookie_pair(&key.to_string(), &value.to_string());
137 }
138 }
139
140 fn clear_cookies(&self) {
143 tracing::info!("清空 cookies(需要重置整个客户端)");
146 }
147
148 pub fn set_account_from_cookie_str(&self, cookie_str: &str) {
149 let mut map = std::collections::HashMap::new();
151 for kv in cookie_str.split(';') {
152 let kv = kv.trim();
153 if let Some(pos) = kv.find('=') {
154 let (key, value) = kv.split_at(pos);
155 map.insert(key.trim().to_string(), value[1..].trim().to_string());
156 }
157 }
158
159 let account = Account {
160 dede_user_id: map.get("DedeUserID").cloned().unwrap_or_default(),
161 dede_user_id_ckmd5: map.get("DedeUserID__ckMd5").cloned().unwrap_or_default(),
162 sessdata: map.get("SESSDATA").cloned().unwrap_or_default(),
163 bili_jct: map.get("bili_jct").cloned().unwrap_or_default(),
164 buvid3: map.get("buvid3").cloned().unwrap_or_default(),
165 };
166
167 self.set_account(account);
168 }
169
170 pub fn has_login_cookies(&self) -> bool {
172 let url = Url::parse("https://api.bilibili.com").unwrap();
173 self.jar.cookies(&url).is_some()
174 }
175
176 pub fn get_account(&self) -> Option<Account> {
178 self.account.lock().unwrap().clone()
179 }
180
181 pub fn csrf(&self) -> Result<String, BpiError> {
183 let account = self.account.lock().unwrap();
184 account
185 .as_ref()
186 .filter(|acc| !acc.bili_jct.is_empty())
187 .map(|acc| acc.bili_jct.clone())
188 .ok_or_else(BpiError::missing_csrf)
189 }
190
191 pub fn get(&self, url: &str) -> RequestBuilder {
193 self.client.get(url).with_user_agent()
194 }
195 pub fn post(&self, url: &str) -> RequestBuilder {
197 self.client.post(url).with_user_agent()
198 }
199}
200
201impl BpiClient {
202 pub fn from_config(config: &Account) -> &Self {
204 let bpi = Self::new();
205
206 if
207 !config.dede_user_id.is_empty() &&
208 !config.sessdata.is_empty() &&
209 !config.bili_jct.is_empty() &&
210 !config.buvid3.is_empty()
211 {
212 let account = Account::new(
213 config.dede_user_id.clone(),
214 config.dede_user_id_ckmd5.clone(),
215 config.sessdata.clone(),
216 config.bili_jct.clone(),
217 config.buvid3.clone()
218 );
219 bpi.set_account(account);
220 }
221
222 bpi
223 }
224}