Skip to main content

bpi_rs/
client.rs

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
11/// 使用示例:
12///
13///
14/// ```rust
15/// use bpi_rs::{ Account, BpiClient };
16///
17/// #[tokio::main]
18/// async fn main() {
19///     let bpi = BpiClient::new();
20///     bpi.set_account(Account {
21///         dede_user_id: "".to_string(),
22///         dede_user_id_ckmd5: "".to_string(),
23///         sessdata: "".to_string(),
24///         bili_jct: "".to_string(),
25///         buvid3: "".to_string(),
26///     });
27///
28///     // bpi.set_account_from_cookie_str("dede_user_id=123;bili_jct=456...");
29///
30///     let result = bpi.bangumi_info(28220978).await;
31///     match result {
32///         Ok(result) => {
33///             tracing::info!("{:#?}", result.data);
34///         }
35///         Err(e) => {
36///             tracing::error!("{:#?}", e);
37///         }
38///     }
39/// }
40
41/// ```
42pub struct BpiClient {
43    client: Client,
44    jar: Arc<Jar>,
45    account: Mutex<Option<Account>>,
46}
47
48impl BpiClient {
49    /// 创建client
50    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) // 启用gzip自动解压缩
57                .deflate(true) // 启用deflate解压缩
58                .brotli(true) // 启用brotli解压缩
59                .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            // 在 debug 模式下自动从account.toml加载测试账号
72            #[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    /// 设置账号信息
90    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    /// 从账号信息设置登录 cookies
102    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    /// 清除账号信息
117    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    /// 批量添加 cookies
132    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    /// 清空所有 cookies
141    /// todo
142    fn clear_cookies(&self) {
143        // 注意:reqwest 的 Jar 没有直接的 clear 方法
144        // 这里需要重新创建 jar,但由于 Arc 的限制,需要在上层重置整个 Bpi
145        tracing::info!("清空 cookies(需要重置整个客户端)");
146    }
147
148    pub fn set_account_from_cookie_str(&self, cookie_str: &str) {
149        // 先解析成 map
150        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    /// 检查是否有登录 cookies
171    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    /// 获取当前账号信息
177    pub fn get_account(&self) -> Option<Account> {
178        self.account.lock().unwrap().clone()
179    }
180
181    /// 从账号信息获取 CSRF token
182    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    /// reqwest的get请求包装, 自带user_agent
192    pub fn get(&self, url: &str) -> RequestBuilder {
193        self.client.get(url).with_user_agent()
194    }
195    /// reqwest的post请求包装, 自带user_agent
196    pub fn post(&self, url: &str) -> RequestBuilder {
197        self.client.post(url).with_user_agent()
198    }
199}
200
201impl BpiClient {
202    /// 从配置创建Client
203    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}