Skip to main content

bpi_rs/live/
manage.rs

1// --- 直播间管理 API 结构体 ---
2
3use crate::BilibiliRequest;
4use crate::BpiResult;
5use crate::live::LiveClient;
6use reqwest::multipart::Form;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9
10/// 开通直播间响应数据
11
12#[derive(Debug, Clone, Deserialize, Serialize)]
13pub struct CreateRoomData {
14    #[serde(rename = "roomID")]
15    pub room_id: Option<String>,
16}
17
18/// 直播间信息更新响应数据
19#[derive(Debug, Clone, Deserialize, Serialize)]
20pub struct UpdateRoomData {
21    pub sub_session_key: String,
22    pub audit_info: Option<AuditInfo>,
23}
24
25/// 审核信息
26#[derive(Debug, Clone, Deserialize, Serialize)]
27pub struct AuditInfo {
28    pub audit_title_reason: String,
29    pub audit_title_status: u8,
30    pub audit_title: Option<String>,
31    pub update_title: Option<String>,
32}
33
34/// RTMP 推流地址信息
35#[derive(Debug, Clone, Deserialize, Serialize)]
36pub struct RtmpInfo {
37    pub addr: String,
38    pub code: String,
39}
40
41/// 开始直播响应数据
42#[derive(Debug, Clone, Deserialize, Serialize)]
43pub struct StartLiveData {
44    pub change: u8,
45    pub status: String,
46    pub rtmp: RtmpInfo,
47    pub live_key: String,
48    pub sub_session_key: String,
49    pub need_face_auth: bool,
50    // 其他不明确的字段都使用 Value
51    pub room_type: Value,
52    pub protocols: Value,
53    pub notice: Value,
54    pub qr: Value,
55    pub service_source: String,
56    pub rtmp_backup: Value,
57    pub up_stream_extra: Value,
58}
59
60/// 关闭直播响应数据
61#[derive(Debug, Clone, Deserialize, Serialize)]
62pub struct StopLiveData {
63    pub change: u8,
64    pub status: String,
65}
66
67/// 预更新直播间信息响应数据
68#[derive(Debug, Clone, Deserialize, Serialize)]
69pub struct UpdatePreLiveInfoData {
70    pub audit_info: Option<AuditInfo>,
71}
72
73/// PC直播姬版本号响应数据
74#[derive(Debug, Clone, Deserialize, Serialize)]
75pub struct PcLiveVersionData {
76    pub curr_version: String,
77    pub build: u64,
78    pub instruction: String,
79    pub file_size: String,
80    pub file_md5: String,
81    pub content: String,
82    pub download_url: String,
83    pub hdiffpatch_switch: u8,
84}
85
86impl<'a> LiveClient<'a> {
87    /// 开通直播间
88    pub async fn live_create_room(&self) -> BpiResult<CreateRoomData> {
89        let csrf = self.client.csrf()?;
90        let form = Form::new()
91            .text("platform", "web")
92            .text("visit_id", "")
93            .text("csrf", csrf.clone())
94            .text("csrf_token", csrf);
95
96        self.client
97            .post("https://api.live.bilibili.com/xlive/app-blink/v1/preLive/CreateRoom")
98            .multipart(form)
99            .send_bpi_payload("live.room.create")
100            .await
101    }
102
103    /// 更新直播间信息
104    ///
105    /// # 参数
106    /// * `room_id` - 直播间 ID
107    /// * `title` - 标题,可选
108    /// * `area_id` - 分区 ID,可选
109    /// * `add_tag` - 要添加的标签,可选
110    /// * `del_tag` - 要删除的标签,可选
111    pub async fn live_update_room_info(
112        &self,
113        room_id: u64,
114        title: Option<&str>,
115        area_id: Option<u64>,
116        add_tag: Option<&str>,
117        del_tag: Option<&str>,
118    ) -> BpiResult<UpdateRoomData> {
119        let csrf = self.client.csrf()?;
120        let mut form = Form::new()
121            .text("room_id", room_id.to_string())
122            .text("csrf", csrf.clone())
123            .text("csrf_token", csrf);
124
125        if let Some(t) = title {
126            form = form.text("title", t.to_string());
127        }
128        if let Some(a) = area_id {
129            form = form.text("area_id", a.to_string());
130        }
131        if let Some(a_tag) = add_tag {
132            form = form.text("add_tag", a_tag.to_string());
133        }
134        if let Some(d_tag) = del_tag {
135            form = form.text("del_tag", d_tag.to_string());
136        }
137
138        self.client
139            .post("https://api.live.bilibili.com/room/v1/Room/update")
140            .multipart(form)
141            .send_bpi_payload("live.room.update")
142            .await
143    }
144
145    /// 开始直播 (目前仅支持直播姬开播)
146    ///
147    /// # 参数
148    /// * `room_id` - 直播间 ID
149    /// * `area_v2` - 直播分区 ID
150    /// * `platform` - 直播平台,如 "pc"
151    #[allow(dead_code)]
152    async fn live_start(
153        &self,
154        room_id: u64,
155        area_v2: u64,
156        platform: &str,
157    ) -> BpiResult<StartLiveData> {
158        let csrf = self.client.csrf()?;
159        let form = Form::new()
160            .text("room_id", room_id.to_string())
161            .text("area_v2", area_v2.to_string())
162            .text("platform", platform.to_string())
163            .text("csrf", csrf.clone())
164            .text("csrf_token", csrf);
165
166        self.client
167            .post("https://api.live.bilibili.com/room/v1/Room/startLive")
168            .multipart(form)
169            .send_bpi_payload("live.start")
170            .await
171    }
172
173    /// 关闭直播
174    ///
175    /// # 参数
176    /// * `room_id` - 直播间 ID
177    /// * `platform` - 直播平台,如 "pc_link"
178    pub async fn live_stop(&self, room_id: u64, platform: &str) -> BpiResult<StopLiveData> {
179        let csrf = self.client.csrf()?;
180        let form = Form::new()
181            .text("platform", platform.to_string())
182            .text("room_id", room_id.to_string())
183            .text("csrf", csrf.clone())
184            .text("csrf_token", csrf);
185
186        self.client
187            .post("https://api.live.bilibili.com/room/v1/Room/stopLive")
188            .multipart(form)
189            .send_bpi_payload("live.stop")
190            .await
191    }
192
193    /// 预更新直播间信息
194    ///
195    /// # 参数
196    /// * `title` - 标题,可选
197    /// * `cover` - 封面 URL,可选
198    pub async fn live_update_pre_live_info(
199        &self,
200        title: Option<&str>,
201        cover: Option<&str>,
202    ) -> BpiResult<UpdatePreLiveInfoData> {
203        let csrf = self.client.csrf()?;
204        let mut form = Form::new()
205            .text("platform", "web")
206            .text("mobi_app", "web")
207            .text("build", "1")
208            .text("csrf", csrf.clone())
209            .text("csrf_token", csrf);
210
211        if let Some(t) = title {
212            form = form.text("title", t.to_string());
213        }
214        if let Some(c) = cover {
215            form = form.text("cover", c.to_string());
216        }
217
218        self.client
219            .post("https://api.live.bilibili.com/xlive/app-blink/v1/preLive/UpdatePreLiveInfo")
220            .multipart(form)
221            .send_bpi_payload("live.pre_live_info.update")
222            .await
223    }
224
225    /// 更新直播间公告
226    ///
227    /// # 参数
228    /// * `room_id` - 直播间 ID
229    /// * `uid` - 用户ID
230    /// * `content` - 公告内容
231    pub async fn live_update_room_news(
232        &self,
233        room_id: u64,
234        uid: u64,
235        content: &str,
236    ) -> BpiResult<Value> {
237        let csrf = self.client.csrf()?;
238        let form = Form::new()
239            .text("room_id", room_id.to_string())
240            .text("uid", uid.to_string())
241            .text("content", content.to_string())
242            .text("csrf", csrf.clone())
243            .text("csrf_token", csrf);
244
245        self.client
246            .post("https://api.live.bilibili.com/xlive/app-blink/v1/index/updateRoomNews")
247            .multipart(form)
248            .send_bpi_payload("live.room_news.update")
249            .await
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256    use crate::probe::contract::HttpMethod;
257    use crate::probe::endpoint_contract::EndpointContract;
258    use crate::{ApiEnvelope, BpiResult};
259
260    fn version_contract() -> BpiResult<EndpointContract> {
261        EndpointContract::from_slice(include_bytes!(
262            "../../tests/contracts/live/public-core/version/contract.json"
263        ))
264    }
265
266    #[test]
267    fn live_version_contract_matches_endpoint_request() -> BpiResult<()> {
268        let contract = version_contract()?;
269
270        assert_eq!(contract.name, "live.version");
271        assert_eq!(contract.request.method, HttpMethod::Get);
272        assert_eq!(
273            contract.request.url.as_str(),
274            "https://api.live.bilibili.com/xlive/app-blink/v1/liveVersionInfo/getHomePageLiveVersion"
275        );
276        assert_eq!(
277            contract
278                .request
279                .query
280                .get("system_version")
281                .map(String::as_str),
282            Some("2")
283        );
284        assert_eq!(contract.cases.len(), 3);
285        assert_eq!(
286            contract.cases[0].response.rust_model.as_deref(),
287            Some("PcLiveVersionData")
288        );
289        Ok(())
290    }
291
292    #[test]
293    fn live_version_response_fixture_parses_declared_model() -> BpiResult<()> {
294        let payload = ApiEnvelope::<PcLiveVersionData>::from_slice(include_bytes!(
295            "../../tests/contracts/live/public-core/version/responses/success.json"
296        ))?
297        .into_payload()?;
298
299        assert_eq!(payload.curr_version, "7.61.0.10694");
300        Ok(())
301    }
302
303    fn local_probe_body(profile: &str) -> Option<serde_json::Value> {
304        let path =
305            format!("target/bpi-probe-runs/live/public-core/version/{profile}.response.json");
306        let bytes = std::fs::read(path).ok()?;
307        let value: serde_json::Value = serde_json::from_slice(&bytes).ok()?;
308        value
309            .get("response")
310            .and_then(|response| response.get("body"))
311            .cloned()
312    }
313
314    #[test]
315    fn live_version_model_matches_local_probe_outputs_when_available() -> BpiResult<()> {
316        for profile in ["anonymous", "normal", "vip"] {
317            if let Some(body) = local_probe_body(profile) {
318                let payload = serde_json::from_value::<ApiEnvelope<PcLiveVersionData>>(body)?
319                    .into_payload()?;
320                assert!(!payload.curr_version.is_empty());
321            }
322        }
323        Ok(())
324    }
325}