bilibili_api_rs/
lib.rs

1#![warn(missing_docs)]
2//! bilibili-api-rs is a rust library project got inspiration from [bilibili-api](https://github.com/Passkou/bilibili-api).
3//!
4//! Currently "GET" apis only. Api interface `User`, `Xlive` derive from
5//! [Client]
6//!
7//! Api result is part of response, alike [bilibili-api](https://github.com/Passkou/bilibili-api),
8//! is `response["data"]`. Invalid response treated as error then bail. *Note that init retries and token
9//! refresh also be treated as error.*
10//!
11//! *High overhead*: to anti-detect, client open one whole new connection in every request; must
12//! recreate `User/Xlive/..` helper if failed, better recreate every times but with higher cost.
13//!
14//! ## Example
15//! ```
16//! use bilibili_api_rs::Client;
17//! use anyhow::Result;
18//! async fn test_xlive() -> Result<()> {
19//!     let mut cli = Client::new();
20//!     let area_virtual = 9;
21//!     let type_all = 0;
22//!     cli.xlive(area_virtual, type_all).list(1).await.ok(); // usually retry once for init
23//!     let lives = cli.xlive(area_virtual, type_all).list(2).await?;
24//!     Ok(())
25//! }
26//! ```
27use log::debug;
28use std::sync::Arc;
29use tokio::sync::mpsc;
30
31mod cred_utils;
32
33pub mod wbi;
34pub use wbi::Client;
35
36type StateData = im::HashMap<String, String>;
37type Json = serde_json::Value;
38
39#[derive(Clone, Debug)]
40struct Bench {
41    data: Arc<Json>,
42    state: StateData,
43    tx: mpsc::Sender<StateData>,
44}
45
46impl Bench {
47    pub fn new() -> (Self, mpsc::Receiver<StateData>) {
48        let unstable: Json = serde_json::from_str(include_str!("api_info/unstable.json"))
49            .expect("api_info/unstable.json invalid");
50        let user: Json = serde_json::from_str(include_str!("api_info/user.json"))
51            .expect("api_info/user.json invalid");
52        let live: Json = serde_json::from_str(include_str!("api_info/live.json"))
53            .expect("api_info/live.json invalid");
54        let video: Json = serde_json::from_str(include_str!("api_info/video.json"))
55            .expect("api_info/video.json invalid");
56        let api_xlive: Json = serde_json::from_str(include_str!("api_info/xlive.json"))
57            .expect("api_info/xlive.json invalid");
58        let credential: Json = serde_json::from_str(include_str!("api_info/credential.json"))
59            .expect("api_info/credential.json invalid");
60        let wbi_oe: Json =
61            serde_json::from_str(include_str!("wbi_oe.json")).expect("wbi_oe.json invalid"); // this file has to be
62                                                                                             // manually maintain now,
63                                                                                             // perl tool TODO
64        let mut state = StateData::new();
65        state.insert("SESSDATA".into(), "None".into());
66        state.insert("bili_jct".into(), "None".into());
67        state.insert("ac_time_value".into(), "None".into());
68        let (tx, rx) = mpsc::channel(1);
69        (
70            Self {
71                data: Arc::new(serde_json::json!({
72                    "api": {
73                        "user": user,
74                        "live": live,
75                        "video": video,
76                        "xlive": api_xlive,
77                        "credential": credential,
78                        "unstable": unstable,
79                    },
80                    "cookie_state": [
81                        "buvid3",
82                        "buvid4",
83                        "buvid_fp",
84                        "_uuid",
85                        "SESSDATA",
86                        "ac_time_value",
87                        "bili_jct",
88                        "DedeUserID",
89                    ],
90                    "wbi_oe": wbi_oe,
91                    "headers": {
92                        "REFERER":  "https://www.bilibili.com",
93                        "USER_AGENT": concat!(
94                            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ",
95                            "AppleWebKit/537.36 (KHTML, like Gecko) ",
96                            "Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.54"
97                        ),
98                    }
99                })),
100                state,
101                tx,
102            },
103            rx,
104        )
105    }
106
107    #[allow(dead_code)]
108    pub fn commit_state(&self, change: impl FnOnce(&mut StateData)) {
109        let mut s = self.state.clone();
110        change(&mut s);
111        self.update_state(s);
112    }
113
114    pub fn update_state(&self, state: StateData) {
115        if let Err(e) = self.tx.try_send(state) {
116            debug!("bench try_send {e:?}");
117        }
118    }
119
120    pub fn get_room_id<T: std::fmt::Display>(&self, uid: T) -> Option<&String> {
121        self.state.get(&format!("room_id:{uid}"))
122    }
123
124    pub fn set_room_id<U, V>(&self, uid: U, room: &V)
125    where
126        U: std::fmt::Display,
127        V: ToString,
128    {
129        let k = format!("room_id:{uid}");
130        let v = room.to_string();
131        self.commit_state(|sto| {
132            sto.insert(k, v);
133        });
134    }
135}
136
137/// Lodash-like get helper, implemented for `serde_json`
138///
139/// ```
140/// use bilibili_api_rs::Lodash;
141/// # use serde_json::json;
142/// let v = json!({
143///     "following": [ {
144///         "mid": 1472906636,
145///         "name": "ywwuyi",
146///     }, {
147///         "mid": 15810,
148///         "name": "Mr.Quin",
149///     }],
150/// });
151/// assert_eq!(v.at(json!(["following", 0, "mid"])), json!(1472906636));
152/// assert_eq!(v["following"].at(json!([
153///         [0, "name"],
154///         [1, "name"],
155///     ])),
156///     json!(["ywwuyi", "Mr.Quin"]));
157/// ```
158pub trait Lodash {
159    /// Input a matrix, output a vector; input a vector, output single one value
160    #[must_use]
161    fn at(&self, paths: Json) -> Self;
162}
163
164fn lodash_step<'a>(v: &'a Json, p: &Json) -> &'a Json {
165    match p {
166        Json::Number(n) => &v[n.as_u64().map(usize::try_from).unwrap().unwrap()],
167        Json::String(s) => &v[s],
168        _ => &Json::Null,
169    }
170}
171
172fn lodash_get<'a>(v: &'a Json, path: &Json) -> &'a Json {
173    let Json::Array(path) = path else {
174        return &Json::Null;
175    };
176    if path.is_empty() {
177        return v;
178    }
179    let mut it = path.iter();
180    let mut v = lodash_step(v, it.next().unwrap());
181    for p in it {
182        v = lodash_step(v, p);
183    }
184    v
185}
186
187impl Lodash for Json {
188    fn at(&self, paths: Json) -> Self {
189        let Some(a) = paths.as_array() else {
190            return Self::Null;
191        };
192        if a[0].is_array() {
193            let mut v: Vec<Self> = Vec::new();
194            for path in a {
195                v.push(lodash_get(self, path).clone());
196            }
197            Self::Array(v)
198        } else {
199            lodash_get(self, &paths).clone()
200        }
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207    use serde_json::json;
208    use std::thread;
209
210    #[test]
211    fn test_lodash_at() {
212        let bench = Bench::new().0;
213        let v = &bench.data;
214        assert_eq!(
215            v.at(json!([
216                ["headers", "REFERER"],
217                ["headers", "__must_null__"],
218            ])),
219            json!(["https://www.bilibili.com", ()])
220        );
221        assert_eq!(
222            v.at(json!(["headers", "REFERER"])),
223            json!("https://www.bilibili.com")
224        );
225    }
226
227    #[test]
228    fn validate_wbi_user_info() {
229        let bench = Bench::new().0;
230        assert_eq!(
231            bench.data["api"]["user"]["info"]["info"]["wbi"].as_bool(),
232            Some(true)
233        );
234    }
235
236    #[test]
237    fn validate_method_xlive_get_list() {
238        let bench = Bench::new().0;
239        assert_eq!(
240            bench.data["api"]["xlive"]["info"]["get_list"]["method"].as_str(),
241            Some("GET")
242        );
243    }
244
245    fn json_state(rx: &mut mpsc::Receiver<StateData>) -> Json {
246        rx.try_recv()
247            .ok()
248            .map(serde_json::to_value)
249            .and_then(Result::ok)
250            .unwrap_or(Json::Null)
251    }
252
253    #[test]
254    fn commit_state() {
255        let (bench, mut rx) = Bench::new();
256        bench.commit_state(|s| {
257            s.clear();
258            s.insert("test".into(), "value".into());
259        });
260        assert_eq!(json_state(&mut rx), json!({"test":"value"}));
261        bench.commit_state(|s| {
262            s.clear();
263            s.insert("test".into(), "modified".into());
264        });
265        assert_eq!(json_state(&mut rx), json!({"test":"modified"}));
266    }
267
268    #[test]
269    fn multithread_commit_state() {
270        let (bench0, mut rx) = Bench::new();
271
272        let bench = bench0.clone();
273        let hdl = thread::spawn(move || {
274            bench.commit_state(|s| {
275                s.clear();
276                s.insert("test".into(), "value".into());
277            });
278        });
279        assert!(hdl.join().is_ok());
280        assert_eq!(json_state(&mut rx), json!({"test":"value"}));
281
282        let bench = bench0;
283        let hdl = thread::spawn(move || {
284            bench.commit_state(|s| {
285                s.clear();
286                s.insert("test".into(), "modified".into());
287            });
288        });
289        assert!(hdl.join().is_ok());
290        assert_eq!(json_state(&mut rx), json!({"test":"modified"}));
291    }
292
293    #[test]
294    fn insure_get_nav_api_no_encwbi() {
295        let bench = Bench::new().0;
296        assert!(matches!(
297            bench.data["api"]["credential"]["valid"]["wbi"],
298            Json::Null | Json::Bool(false)
299        ));
300    }
301}