bililive_core/builder/
mod.rs

1//! `bililive` config builder.
2
3use std::future::Future;
4use std::marker::PhantomData;
5use std::pin::Pin;
6
7/// `bililive` stream config builder.
8///
9/// Stream config can be built via given live room parameters (room id and user id) & danmaku server configs (server token and list).
10///
11/// # Helper methods
12///
13/// [`by_uid`](ConfigBuilder::by_uid) fetches room id by given user id.
14///
15/// [`fetch_conf`](ConfigBuilder::fetch_conf) fetches danmaku server token and list without any input parameter.
16///
17/// See docs of downstream crates for details.
18use serde::de::DeserializeOwned;
19
20use crate::builder::types::{ConfQueryInner, Resp, RoomQueryInner};
21use crate::config::StreamConfig;
22use crate::errors::{BoxedError, BuildError};
23
24#[cfg(test)]
25mod tests;
26mod types;
27
28/// An abstract HTTP client.
29///
30/// Used in [`ConfigBuilder`](ConfigBuilder) to help fetching bilibili config.
31#[cfg(feature = "not-send")]
32pub trait Requester {
33    /// Make a `GET` request to the url and try to deserialize the response body as JSON.
34    fn get_json<T: DeserializeOwned>(
35        &self,
36        url: &str,
37    ) -> Pin<Box<dyn Future<Output = Result<T, BoxedError>> + '_>>;
38}
39
40/// An abstract HTTP client.
41///
42/// Used in [`ConfigBuilder`](ConfigBuilder) to help fetching bilibili config.
43#[cfg(not(feature = "not-send"))]
44pub trait Requester: Send + Sync {
45    /// Make a `GET` request to the url and try to deserialize the response body as JSON.
46    fn get_json<T: DeserializeOwned>(
47        &self,
48        url: &str,
49    ) -> Pin<Box<dyn Future<Output = Result<T, BoxedError>> + Send + '_>>;
50}
51
52#[doc(hidden)]
53pub enum BF {}
54
55#[doc(hidden)]
56pub enum BN {}
57
58/// `bililive` stream config builder.
59///
60/// Stream config can be built via given live room parameters (room id and user id) & danmaku server configs (server token and list).
61///
62/// # Helper methods
63///
64/// [`by_uid`](ConfigBuilder::by_uid) fetches room id by given user id.
65///
66/// [`fetch_conf`](ConfigBuilder::fetch_conf) fetches danmaku server token and list without any input parameter.
67#[derive(Debug)]
68pub struct ConfigBuilder<H, R, U, T, S> {
69    http: H,
70    room_id: Option<u64>,
71    uid: Option<u64>,
72    token: Option<String>,
73    servers: Option<Vec<String>>,
74    __marker: PhantomData<(R, U, T, S)>,
75}
76
77impl<H: Default> ConfigBuilder<H, BN, BN, BN, BN> {
78    /// Construct a new builder with default requester client.
79    #[allow(clippy::new_without_default)]
80    #[must_use]
81    pub fn new() -> Self {
82        Self::new_with_client(H::default())
83    }
84}
85
86impl<H> ConfigBuilder<H, BN, BN, BN, BN> {
87    /// Construct a new builder with given requester client.
88    #[must_use]
89    pub const fn new_with_client(client: H) -> Self {
90        Self {
91            http: client,
92            room_id: None,
93            uid: None,
94            token: None,
95            servers: None,
96            __marker: PhantomData,
97        }
98    }
99}
100
101impl<H, R, U, T, S> ConfigBuilder<H, R, U, T, S> {
102    #[allow(clippy::missing_const_for_fn)] // misreport
103    fn cast<R2, U2, T2, S2>(self) -> ConfigBuilder<H, R2, U2, T2, S2> {
104        ConfigBuilder {
105            http: self.http,
106            room_id: self.room_id,
107            uid: self.uid,
108            token: self.token,
109            servers: self.servers,
110            __marker: PhantomData,
111        }
112    }
113}
114
115impl<H, R, U, T, S> ConfigBuilder<H, R, U, T, S> {
116    #[must_use]
117    pub fn room_id(mut self, room_id: u64) -> ConfigBuilder<H, BF, U, T, S> {
118        self.room_id = Some(room_id);
119        self.cast()
120    }
121    #[must_use]
122    pub fn uid(mut self, uid: u64) -> ConfigBuilder<H, R, BF, T, S> {
123        self.uid = Some(uid);
124        self.cast()
125    }
126    #[must_use]
127    pub fn token(mut self, token: &str) -> ConfigBuilder<H, R, U, BF, S> {
128        self.token = Some(token.to_string());
129        self.cast()
130    }
131
132    #[must_use]
133    pub fn servers(mut self, servers: &[String]) -> ConfigBuilder<H, R, U, T, BF> {
134        self.servers = Some(servers.to_vec());
135        self.cast()
136    }
137}
138
139impl<H, R, U, T, S> ConfigBuilder<H, R, U, T, S>
140where
141    H: Requester,
142    R: Send + Sync,
143    U: Send + Sync,
144    T: Send + Sync,
145    S: Send + Sync,
146{
147    /// Fills `room_id` and `uid` by given `uid`, fetching `room_id` automatically.
148    ///
149    /// # Errors
150    /// Returns an error when HTTP api request fails.
151    pub async fn by_uid(mut self, uid: u64) -> Result<ConfigBuilder<H, BF, BF, T, S>, BuildError> {
152        let resp: Resp<RoomQueryInner> = self
153            .http
154            .get_json(&*format!(
155                "https://api.live.bilibili.com/bili/living_v2/{}",
156                uid
157            ))
158            .await
159            .map_err(BuildError)?;
160        let room_id = resp.room_id();
161
162        self.room_id = Some(room_id);
163        self.uid = Some(uid);
164        Ok(self.cast())
165    }
166
167    /// Fetches danmaku server configs & uris
168    ///
169    /// # Errors
170    /// Returns an error when HTTP api request fails.
171    pub async fn fetch_conf(mut self) -> Result<ConfigBuilder<H, R, U, BF, BF>, BuildError> {
172        let resp: Resp<ConfQueryInner> = self
173            .http
174            .get_json("https://api.live.bilibili.com/room/v1/Danmu/getConf")
175            .await
176            .map_err(BuildError)?;
177
178        self.token = Some(resp.token().to_string());
179        self.servers = Some(resp.servers());
180        Ok(self.cast())
181    }
182}
183
184impl<H> ConfigBuilder<H, BF, BF, BF, BF> {
185    /// Consumes the builder and returns [`StreamConfig`](StreamConfig)
186    #[allow(clippy::missing_panics_doc)]
187    pub fn build(self) -> StreamConfig {
188        // SAFETY ensured by type state
189        StreamConfig::new(
190            self.room_id.unwrap(),
191            self.uid.unwrap(),
192            self.token.unwrap(),
193            self.servers.unwrap(),
194        )
195    }
196}