1use crate::live::danmaku::LiveDanmuInfoData;
2use crate::live::emoticons::EmoticonData;
3use crate::live::follow_up_live::{FollowUpLiveData, LiveWebListData};
4use crate::live::gift::{BlindGiftData, RoomGiftData};
5use crate::live::guard::GuardListData;
6use crate::live::info::RoomInfoData;
7use crate::live::live_area::LiveParentArea;
8use crate::live::live_bill::GiftTypeItem;
9use crate::live::live_replay::ReplayListData;
10use crate::live::live_stream::LiveStreamData;
11use crate::live::manage::PcLiveVersionData;
12use crate::live::recommend::RecommendData;
13use crate::live::redpocket::LotteryInfoData;
14use crate::live::report::{HeartBeatData, LiveWebHeartBeatParams};
15use crate::live::silent_user_manage::{
16 BannedUserListData, LiveBannedUserListParams, LiveShieldKeywordListParams,
17 LiveSilentUserListParams, ShieldKeywordListData, SilentUserListData,
18};
19use crate::live::user::MyMedalsData;
20use crate::{BilibiliRequest, BpiClient, BpiResult};
21
22const AREA_LIST_ENDPOINT: &str = "https://api.live.bilibili.com/room/v1/Area/getList";
23const ROOM_INFO_ENDPOINT: &str = "https://api.live.bilibili.com/room/v1/Room/get_info";
24const STREAM_ENDPOINT: &str = "https://api.live.bilibili.com/room/v1/Room/playUrl";
25const RECOMMEND_ENDPOINT: &str =
26 "https://api.live.bilibili.com/xlive/web-interface/v1/webMain/getMoreRecList";
27const VERSION_ENDPOINT: &str =
28 "https://api.live.bilibili.com/xlive/app-blink/v1/liveVersionInfo/getHomePageLiveVersion";
29const GIFT_TYPES_ENDPOINT: &str = "https://api.live.bilibili.com/gift/v1/master/getGiftTypes";
30const ROOM_GIFT_LIST_ENDPOINT: &str =
31 "https://api.live.bilibili.com/xlive/web-room/v1/giftPanel/roomGiftList";
32const BLIND_GIFT_INFO_ENDPOINT: &str =
33 "https://api.live.bilibili.com/xlive/general-interface/v1/blindFirstWin/getInfo";
34const DANMU_INFO_ENDPOINT: &str =
35 "https://api.live.bilibili.com/xlive/web-room/v1/index/getDanmuInfo";
36const EMOTICONS_ENDPOINT: &str =
37 "https://api.live.bilibili.com/xlive/web-ucenter/v2/emoticon/GetEmoticons";
38const LOTTERY_INFO_ENDPOINT: &str =
39 "https://api.live.bilibili.com/xlive/lottery-interface/v1/lottery/getLotteryInfoWeb";
40const MY_MEDALS_ENDPOINT: &str =
41 "https://api.live.bilibili.com/xlive/app-ucenter/v1/user/GetMyMedals";
42const FOLLOW_UP_LIST_ENDPOINT: &str =
43 "https://api.live.bilibili.com/xlive/web-ucenter/user/following";
44const FOLLOW_UP_WEB_LIST_ENDPOINT: &str =
45 "https://api.live.bilibili.com/xlive/web-ucenter/v1/xfetter/GetWebList";
46const REPLAY_LIST_ENDPOINT: &str =
47 "https://api.live.bilibili.com/xlive/app-blink/v1/anchorVideo/AnchorGetReplayList";
48const GUARD_LIST_ENDPOINT: &str =
49 "https://api.live.bilibili.com/xlive/app-room/v2/guardTab/topListNew";
50const SILENT_USERS_ENDPOINT: &str =
51 "https://api.live.bilibili.com/xlive/web-ucenter/v1/banned/GetSilentUserList";
52const BANNED_USERS_ENDPOINT: &str =
53 "https://api.live.bilibili.com/xlive/app-ucenter/v2/xbanned/banned/GetBlackList";
54const SHIELD_KEYWORDS_ENDPOINT: &str =
55 "https://api.live.bilibili.com/xlive/app-ucenter/v1/banned/GetShieldKeywordList";
56const WEB_HEART_BEAT_ENDPOINT: &str =
57 "https://live-trace.bilibili.com/xlive/rdata-interface/v1/heartbeat/webHeartBeat";
58
59#[derive(Clone, Copy)]
61pub struct LiveClient<'a> {
62 pub(crate) client: &'a BpiClient,
63}
64
65impl<'a> LiveClient<'a> {
66 pub(crate) fn new(client: &'a BpiClient) -> Self {
67 Self { client }
68 }
69
70 #[cfg(test)]
71 pub(crate) fn area_list_endpoint(&self) -> &'static str {
72 AREA_LIST_ENDPOINT
73 }
74
75 #[cfg(test)]
76 pub(crate) fn room_info_endpoint(&self) -> &'static str {
77 ROOM_INFO_ENDPOINT
78 }
79
80 #[cfg(test)]
81 pub(crate) fn stream_endpoint(&self) -> &'static str {
82 STREAM_ENDPOINT
83 }
84
85 #[cfg(test)]
86 pub(crate) fn recommend_endpoint(&self) -> &'static str {
87 RECOMMEND_ENDPOINT
88 }
89
90 #[cfg(test)]
91 pub(crate) fn version_endpoint(&self) -> &'static str {
92 VERSION_ENDPOINT
93 }
94
95 pub async fn area_list(&self) -> BpiResult<Vec<LiveParentArea>> {
97 self.client
98 .get(AREA_LIST_ENDPOINT)
99 .with_bilibili_headers()
100 .send_bpi_payload("live.area_list")
101 .await
102 }
103
104 pub async fn room_info(&self, room_id: i64) -> BpiResult<RoomInfoData> {
106 self.client
107 .get(ROOM_INFO_ENDPOINT)
108 .with_bilibili_headers()
109 .query(&[("room_id", room_id.to_string())])
110 .send_bpi_payload("live.room_info")
111 .await
112 }
113
114 pub async fn stream(
116 &self,
117 cid: i64,
118 platform: Option<&str>,
119 quality: Option<i32>,
120 qn: Option<i32>,
121 ) -> BpiResult<LiveStreamData> {
122 let mut query = vec![("cid", cid.to_string())];
123
124 if let Some(platform) = platform {
125 query.push(("platform", platform.to_string()));
126 }
127 if let Some(quality) = quality {
128 query.push(("quality", quality.to_string()));
129 }
130 if let Some(qn) = qn {
131 query.push(("qn", qn.to_string()));
132 }
133
134 self.client
135 .get(STREAM_ENDPOINT)
136 .with_bilibili_headers()
137 .query(&query)
138 .send_bpi_payload("live.stream")
139 .await
140 }
141
142 pub async fn recommend(&self) -> BpiResult<RecommendData> {
144 self.client
145 .get(RECOMMEND_ENDPOINT)
146 .with_bilibili_headers()
147 .query(&[("platform", "web"), ("web_location", "333.1007")])
148 .send_bpi_payload("live.recommend")
149 .await
150 }
151
152 pub async fn version(&self) -> BpiResult<PcLiveVersionData> {
154 self.client
155 .get(VERSION_ENDPOINT)
156 .with_bilibili_headers()
157 .query(&[("system_version", "2")])
158 .send_bpi_payload("live.version")
159 .await
160 }
161
162 pub async fn gift_types(&self) -> BpiResult<Vec<GiftTypeItem>> {
164 self.client
165 .get(GIFT_TYPES_ENDPOINT)
166 .with_bilibili_headers()
167 .send_bpi_payload("live.gift_types")
168 .await
169 }
170
171 pub async fn room_gift_list(
173 &self,
174 room_id: i64,
175 area_parent_id: Option<i32>,
176 area_id: Option<i32>,
177 ) -> BpiResult<RoomGiftData> {
178 let mut query = vec![
179 ("room_id", room_id.to_string()),
180 ("platform", "web".to_string()),
181 ];
182
183 if let Some(area_parent_id) = area_parent_id {
184 query.push(("area_parent_id", area_parent_id.to_string()));
185 }
186
187 if let Some(area_id) = area_id {
188 query.push(("area_id", area_id.to_string()));
189 }
190
191 self.client
192 .get(ROOM_GIFT_LIST_ENDPOINT)
193 .with_bilibili_headers()
194 .query(&query)
195 .send_bpi_payload("live.room_gift_list")
196 .await
197 }
198
199 pub async fn blind_gift_info(&self, gift_id: i64) -> BpiResult<BlindGiftData> {
201 self.client
202 .get(BLIND_GIFT_INFO_ENDPOINT)
203 .with_bilibili_headers()
204 .query(&[("gift_id", gift_id.to_string())])
205 .send_bpi_payload("live.blind_gift_info")
206 .await
207 }
208
209 pub async fn danmu_info(&self, room_id: u64, info_type: u8) -> BpiResult<LiveDanmuInfoData> {
211 let query = self
212 .client
213 .get_wbi_sign2(vec![
214 ("id", room_id.to_string()),
215 ("type", info_type.to_string()),
216 ])
217 .await?;
218
219 self.client
220 .get(DANMU_INFO_ENDPOINT)
221 .with_bilibili_headers()
222 .query(&query)
223 .send_bpi_payload("live.danmu_info")
224 .await
225 }
226
227 pub async fn emoticons(&self, room_id: i64, platform: &str) -> BpiResult<EmoticonData> {
229 self.client
230 .get(EMOTICONS_ENDPOINT)
231 .with_bilibili_headers()
232 .query(&[
233 ("room_id", room_id.to_string()),
234 ("platform", platform.to_string()),
235 ])
236 .send_bpi_payload("live.emoticons")
237 .await
238 }
239
240 pub async fn lottery_info(&self, room_id: i64) -> BpiResult<LotteryInfoData> {
242 let query = self
243 .client
244 .get_wbi_sign2(vec![("roomid", room_id.to_string())])
245 .await?;
246
247 self.client
248 .get(LOTTERY_INFO_ENDPOINT)
249 .with_bilibili_headers()
250 .query(&query)
251 .send_bpi_payload("live.lottery_info")
252 .await
253 }
254
255 pub async fn my_medals(&self, page: i32, page_size: i32) -> BpiResult<MyMedalsData> {
257 self.client
258 .get(MY_MEDALS_ENDPOINT)
259 .with_bilibili_headers()
260 .query(&[
261 ("page", page.to_string()),
262 ("page_size", page_size.to_string()),
263 ])
264 .send_bpi_payload("live.my_medals")
265 .await
266 }
267
268 pub async fn follow_up_list(
270 &self,
271 page: Option<i32>,
272 page_size: Option<i32>,
273 ignore_record: Option<i32>,
274 hit_ab: Option<bool>,
275 ) -> BpiResult<FollowUpLiveData> {
276 let mut query = Vec::new();
277
278 if let Some(page) = page {
279 query.push(("page", page.to_string()));
280 }
281 if let Some(page_size) = page_size {
282 query.push(("page_size", page_size.to_string()));
283 }
284 if let Some(ignore_record) = ignore_record {
285 query.push(("ignoreRecord", ignore_record.to_string()));
286 }
287 if let Some(hit_ab) = hit_ab {
288 query.push(("hit_ab", hit_ab.to_string()));
289 }
290
291 self.client
292 .get(FOLLOW_UP_LIST_ENDPOINT)
293 .with_bilibili_headers()
294 .query(&query)
295 .send_bpi_payload("live.follow_up_list")
296 .await
297 }
298
299 pub async fn follow_up_web_list(&self, hit_ab: Option<bool>) -> BpiResult<LiveWebListData> {
301 let mut query = Vec::new();
302
303 if let Some(hit_ab) = hit_ab {
304 query.push(("hit_ab", hit_ab.to_string()));
305 }
306
307 self.client
308 .get(FOLLOW_UP_WEB_LIST_ENDPOINT)
309 .with_bilibili_headers()
310 .query(&query)
311 .send_bpi_payload("live.follow_up_web_list")
312 .await
313 }
314
315 pub async fn replay_list(
317 &self,
318 page: Option<i32>,
319 page_size: Option<i32>,
320 ) -> BpiResult<ReplayListData> {
321 let mut query = Vec::new();
322
323 if let Some(page) = page {
324 query.push(("page", page.to_string()));
325 }
326 if let Some(page_size) = page_size {
327 query.push(("page_size", page_size.to_string()));
328 }
329
330 self.client
331 .get(REPLAY_LIST_ENDPOINT)
332 .with_bilibili_headers()
333 .query(&query)
334 .send_bpi_payload("live.replay_list")
335 .await
336 }
337
338 pub async fn guard_list(
340 &self,
341 room_id: i64,
342 ruid: i64,
343 page: Option<i32>,
344 page_size: Option<i32>,
345 typ: Option<i32>,
346 ) -> BpiResult<GuardListData> {
347 let query = [
348 ("roomid", room_id.to_string()),
349 ("ruid", ruid.to_string()),
350 ("page", page.unwrap_or(1).to_string()),
351 ("page_size", page_size.unwrap_or(20).to_string()),
352 ("typ", typ.unwrap_or(5).to_string()),
353 ];
354
355 self.client
356 .get(GUARD_LIST_ENDPOINT)
357 .with_bilibili_headers()
358 .query(&query)
359 .send_bpi_payload("live.guard_list")
360 .await
361 }
362
363 pub async fn silent_users(
365 &self,
366 params: LiveSilentUserListParams,
367 ) -> BpiResult<SilentUserListData> {
368 let csrf = self.client.csrf().unwrap_or_default();
369 let form = params.form_pairs(&csrf);
370
371 self.client
372 .post(SILENT_USERS_ENDPOINT)
373 .with_bilibili_headers()
374 .form(&form)
375 .send_bpi_payload("live.silent_users")
376 .await
377 }
378
379 pub async fn banned_users(
381 &self,
382 params: LiveBannedUserListParams,
383 ) -> BpiResult<BannedUserListData> {
384 let csrf = self.client.csrf().unwrap_or_default();
385 let query = params.query_pairs(&csrf);
386
387 self.client
388 .get(BANNED_USERS_ENDPOINT)
389 .with_bilibili_headers()
390 .query(&query)
391 .send_bpi_payload("live.banned_users")
392 .await
393 }
394
395 pub async fn shield_keywords(
397 &self,
398 params: LiveShieldKeywordListParams,
399 ) -> BpiResult<ShieldKeywordListData> {
400 let csrf = self.client.csrf().unwrap_or_default();
401 let form = params.form_pairs(&csrf);
402
403 self.client
404 .post(SHIELD_KEYWORDS_ENDPOINT)
405 .with_bilibili_headers()
406 .form(&form)
407 .send_bpi_payload("live.shield_keywords")
408 .await
409 }
410
411 pub async fn web_heart_beat(&self, params: LiveWebHeartBeatParams) -> BpiResult<HeartBeatData> {
413 self.client
414 .get(WEB_HEART_BEAT_ENDPOINT)
415 .with_bilibili_headers()
416 .query(¶ms.query_pairs())
417 .send_bpi_payload("live.web_heart_beat")
418 .await
419 }
420}
421
422#[cfg(test)]
423mod tests {
424 use std::future::Future;
425
426 use crate::ids::{Mid, RoomId};
427 use crate::live::danmaku::LiveDanmuInfoData;
428 use crate::live::emoticons::EmoticonData;
429 use crate::live::follow_up_live::{FollowUpLiveData, LiveWebListData};
430 use crate::live::gift::{BlindGiftData, RoomGiftData};
431 use crate::live::guard::GuardListData;
432 use crate::live::info::RoomInfoData;
433 use crate::live::live_area::LiveParentArea;
434 use crate::live::live_bill::GiftTypeItem;
435 use crate::live::live_replay::ReplayListData;
436 use crate::live::live_stream::LiveStreamData;
437 use crate::live::manage::PcLiveVersionData;
438 use crate::live::recommend::RecommendData;
439 use crate::live::redpocket::LotteryInfoData;
440 use crate::live::report::{HeartBeatData, LiveWebHeartBeatParams};
441 use crate::live::silent_user_manage::{
442 BannedUserListData, LiveBannedUserListParams, LiveShieldKeywordListParams,
443 LiveSilentUserListParams, ShieldKeywordListData, SilentUserListData,
444 };
445 use crate::live::user::MyMedalsData;
446 use crate::probe::contract::HttpMethod;
447 use crate::probe::endpoint_contract::EndpointContract;
448 use crate::{BpiClient, BpiError, BpiResult};
449
450 fn assert_area_list_future<F>(_future: F)
451 where
452 F: Future<Output = BpiResult<Vec<LiveParentArea>>>,
453 {
454 }
455
456 fn assert_room_info_future<F>(_future: F)
457 where
458 F: Future<Output = BpiResult<RoomInfoData>>,
459 {
460 }
461
462 fn assert_stream_future<F>(_future: F)
463 where
464 F: Future<Output = BpiResult<LiveStreamData>>,
465 {
466 }
467
468 fn assert_recommend_future<F>(_future: F)
469 where
470 F: Future<Output = BpiResult<RecommendData>>,
471 {
472 }
473
474 fn assert_version_future<F>(_future: F)
475 where
476 F: Future<Output = BpiResult<PcLiveVersionData>>,
477 {
478 }
479
480 fn assert_gift_types_future<F>(_future: F)
481 where
482 F: Future<Output = BpiResult<Vec<GiftTypeItem>>>,
483 {
484 }
485
486 fn assert_room_gift_list_future<F>(_future: F)
487 where
488 F: Future<Output = BpiResult<RoomGiftData>>,
489 {
490 }
491
492 fn assert_blind_gift_info_future<F>(_future: F)
493 where
494 F: Future<Output = BpiResult<BlindGiftData>>,
495 {
496 }
497
498 fn assert_danmu_info_future<F>(_future: F)
499 where
500 F: Future<Output = BpiResult<LiveDanmuInfoData>>,
501 {
502 }
503
504 fn assert_emoticons_future<F>(_future: F)
505 where
506 F: Future<Output = BpiResult<EmoticonData>>,
507 {
508 }
509
510 fn assert_lottery_info_future<F>(_future: F)
511 where
512 F: Future<Output = BpiResult<LotteryInfoData>>,
513 {
514 }
515
516 fn assert_my_medals_future<F>(_future: F)
517 where
518 F: Future<Output = BpiResult<MyMedalsData>>,
519 {
520 }
521
522 fn assert_follow_up_list_future<F>(_future: F)
523 where
524 F: Future<Output = BpiResult<FollowUpLiveData>>,
525 {
526 }
527
528 fn assert_follow_up_web_list_future<F>(_future: F)
529 where
530 F: Future<Output = BpiResult<LiveWebListData>>,
531 {
532 }
533
534 fn assert_replay_list_future<F>(_future: F)
535 where
536 F: Future<Output = BpiResult<ReplayListData>>,
537 {
538 }
539
540 fn assert_guard_list_future<F>(_future: F)
541 where
542 F: Future<Output = BpiResult<GuardListData>>,
543 {
544 }
545
546 fn assert_silent_users_future<F>(_future: F)
547 where
548 F: Future<Output = BpiResult<SilentUserListData>>,
549 {
550 }
551
552 fn assert_banned_users_future<F>(_future: F)
553 where
554 F: Future<Output = BpiResult<BannedUserListData>>,
555 {
556 }
557
558 fn assert_shield_keywords_future<F>(_future: F)
559 where
560 F: Future<Output = BpiResult<ShieldKeywordListData>>,
561 {
562 }
563
564 fn assert_web_heart_beat_future<F>(_future: F)
565 where
566 F: Future<Output = BpiResult<HeartBeatData>>,
567 {
568 }
569
570 fn contract(endpoint: &str) -> BpiResult<EndpointContract> {
571 let bytes: &[u8] = match endpoint {
572 "area-list" => {
573 include_bytes!("../../tests/contracts/live/public-core/area-list/contract.json")
574 }
575 "room-info" => {
576 include_bytes!("../../tests/contracts/live/public-core/room-info/contract.json")
577 }
578 "stream" => {
579 include_bytes!("../../tests/contracts/live/public-core/stream/contract.json")
580 }
581 "recommend" => {
582 include_bytes!("../../tests/contracts/live/public-core/recommend/contract.json")
583 }
584 "version" => {
585 include_bytes!("../../tests/contracts/live/public-core/version/contract.json")
586 }
587 _ => {
588 return Err(BpiError::invalid_parameter(
589 "endpoint",
590 "unknown live contract",
591 ));
592 }
593 };
594
595 EndpointContract::from_slice(bytes)
596 }
597
598 #[test]
599 fn live_public_core_methods_return_payload_futures() -> Result<(), BpiError> {
600 let client = BpiClient::new()?;
601 let live = client.live();
602
603 assert_area_list_future(live.area_list());
604 assert_room_info_future(live.room_info(23_174_842));
605 assert_stream_future(live.stream(14_073_662, Some("web"), None, Some(10_000)));
606 assert_recommend_future(live.recommend());
607 assert_version_future(live.version());
608 Ok(())
609 }
610
611 #[test]
612 fn live_client_exposes_remaining_read_methods() -> BpiResult<()> {
613 let client = BpiClient::new()?;
614 let live = client.live();
615
616 assert_gift_types_future(live.gift_types());
617 assert_room_gift_list_future(live.room_gift_list(23_174_842, None, None));
618 assert_blind_gift_info_future(live.blind_gift_info(32_251));
619 assert_danmu_info_future(live.danmu_info(21_733_448, 0));
620 assert_emoticons_future(live.emoticons(14_047, "pc"));
621 assert_lottery_info_future(live.lottery_info(23_174_842));
622 assert_my_medals_future(live.my_medals(1, 10));
623 assert_follow_up_list_future(live.follow_up_list(Some(1), Some(2), Some(1), Some(true)));
624 assert_follow_up_web_list_future(live.follow_up_web_list(Some(false)));
625 assert_replay_list_future(live.replay_list(Some(1), Some(2)));
626 assert_guard_list_future(live.guard_list(23_174_842, 504_140_200, None, None, None));
627 assert_silent_users_future(
628 live.silent_users(
629 LiveSilentUserListParams::new(RoomId::new(3_818_081)?).page_size(10)?,
630 ),
631 );
632 assert_banned_users_future(
633 live.banned_users(LiveBannedUserListParams::new(Mid::new(4_279_370)?).page_size(10)?),
634 );
635 assert_shield_keywords_future(
636 live.shield_keywords(LiveShieldKeywordListParams::new(RoomId::new(3_818_081)?)),
637 );
638 assert_web_heart_beat_future(live.web_heart_beat(LiveWebHeartBeatParams::new(23_174_842)?));
639
640 let source = include_str!("client.rs");
641 let payload_helper = concat!(".send_", "bpi_payload");
642 assert!(
643 source.matches(payload_helper).count() >= 20,
644 "LiveClient should use payload helpers for public-core and remaining promoted read methods"
645 );
646
647 Ok(())
648 }
649
650 #[test]
651 fn live_public_core_contracts_match_module_client_endpoints() -> BpiResult<()> {
652 let client = BpiClient::new()?;
653 let live = client.live();
654 let cases = [
655 ("area-list", "live.area_list", live.area_list_endpoint()),
656 ("room-info", "live.room_info", live.room_info_endpoint()),
657 ("stream", "live.stream", live.stream_endpoint()),
658 ("recommend", "live.recommend", live.recommend_endpoint()),
659 ("version", "live.version", live.version_endpoint()),
660 ];
661
662 for (endpoint, name, url) in cases {
663 let contract = contract(endpoint)?;
664
665 assert_eq!(contract.name, name);
666 assert_eq!(contract.request.method, HttpMethod::Get);
667 assert_eq!(contract.request.url.as_str(), url);
668 }
669
670 Ok(())
671 }
672}