1use crate::BilibiliRequest;
4use crate::BpiResult;
5use crate::live::LiveClient;
6use reqwest::multipart::Form;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9
10#[derive(Debug, Clone, Deserialize, Serialize)]
13pub struct CreateRoomData {
14 #[serde(rename = "roomID")]
15 pub room_id: Option<String>,
16}
17
18#[derive(Debug, Clone, Deserialize, Serialize)]
20pub struct UpdateRoomData {
21 pub sub_session_key: String,
22 pub audit_info: Option<AuditInfo>,
23}
24
25#[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#[derive(Debug, Clone, Deserialize, Serialize)]
36pub struct RtmpInfo {
37 pub addr: String,
38 pub code: String,
39}
40
41#[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 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#[derive(Debug, Clone, Deserialize, Serialize)]
62pub struct StopLiveData {
63 pub change: u8,
64 pub status: String,
65}
66
67#[derive(Debug, Clone, Deserialize, Serialize)]
69pub struct UpdatePreLiveInfoData {
70 pub audit_info: Option<AuditInfo>,
71}
72
73#[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 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 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 #[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 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 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 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}