bpi_rs/live/
live_stream.rs1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Serialize, Clone, Deserialize)]
4pub struct QualityDescription {
5 pub qn: i32,
7 pub desc: String,
9}
10
11#[derive(Debug, Serialize, Clone, Deserialize)]
12pub struct LiveStreamUrl {
13 pub url: String,
15 pub order: i32,
17 pub stream_type: i32,
19 pub p2p_type: i32,
21}
22
23#[derive(Debug, Serialize, Clone, Deserialize)]
24pub struct LiveStreamData {
25 pub current_quality: i32,
27 pub accept_quality: Vec<String>,
29 pub current_qn: i32,
31 pub quality_description: Vec<QualityDescription>,
33 pub durl: Vec<LiveStreamUrl>,
35}
36
37#[cfg(test)]
38mod tests {
39 use super::*;
40 use crate::probe::contract::HttpMethod;
41 use crate::probe::endpoint_contract::EndpointContract;
42 use crate::{ApiEnvelope, BpiClient, BpiResult};
43
44 fn contract() -> BpiResult<EndpointContract> {
45 EndpointContract::from_slice(include_bytes!(
46 "../../tests/contracts/live/public-core/stream/contract.json"
47 ))
48 }
49
50 #[ignore = "legacy live API test; requires explicit BPI_LIVE_TEST review"]
51 #[tokio::test]
52 async fn test_get_live_stream() {
53 let bpi = BpiClient::new().expect("client should build");
54 let data = bpi
55 .live()
56 .stream(14073662, Some("web"), None, Some(10000))
57 .await
58 .unwrap();
59 tracing::info!("{:?}", data);
60 }
61
62 #[test]
63 fn live_stream_contract_matches_endpoint_request() -> BpiResult<()> {
64 let contract = contract()?;
65
66 assert_eq!(contract.name, "live.stream");
67 assert_eq!(contract.request.method, HttpMethod::Get);
68 assert_eq!(
69 contract.request.url.as_str(),
70 "https://api.live.bilibili.com/room/v1/Room/playUrl"
71 );
72 assert_eq!(
73 contract.request.query.get("cid").map(String::as_str),
74 Some("14073662")
75 );
76 assert_eq!(
77 contract.request.query.get("platform").map(String::as_str),
78 Some("web")
79 );
80 assert_eq!(
81 contract.request.query.get("qn").map(String::as_str),
82 Some("10000")
83 );
84 assert_eq!(contract.cases.len(), 3);
85 assert_eq!(
86 contract.cases[0].response.rust_model.as_deref(),
87 Some("LiveStreamData")
88 );
89 Ok(())
90 }
91
92 #[test]
93 fn live_stream_response_fixture_parses_declared_model() -> BpiResult<()> {
94 let payload = ApiEnvelope::<LiveStreamData>::from_slice(include_bytes!(
95 "../../tests/contracts/live/public-core/stream/responses/success.json"
96 ))?
97 .into_payload()?;
98
99 assert_eq!(payload.current_qn, 10000);
100 assert_eq!(payload.durl.len(), 1);
101 Ok(())
102 }
103
104 fn local_probe_body(profile: &str) -> Option<serde_json::Value> {
105 let path = format!("target/bpi-probe-runs/live/public-core/stream/{profile}.response.json");
106 let bytes = std::fs::read(path).ok()?;
107 let value: serde_json::Value = serde_json::from_slice(&bytes).ok()?;
108 value
109 .get("response")
110 .and_then(|response| response.get("body"))
111 .cloned()
112 }
113
114 #[test]
115 fn live_stream_model_matches_local_probe_outputs_when_available() -> BpiResult<()> {
116 for profile in ["anonymous", "normal", "vip"] {
117 if let Some(body) = local_probe_body(profile) {
118 let payload =
119 serde_json::from_value::<ApiEnvelope<LiveStreamData>>(body)?.into_payload()?;
120 assert!(!payload.durl.is_empty());
121 }
122 }
123 Ok(())
124 }
125}