1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Serialize, Clone, Deserialize)]
6pub struct WatchedShow {
7 pub switch: bool,
9 pub num: i32,
11 pub text_small: String,
13 pub text_large: String,
15 pub icon: String,
17 pub icon_location: i32,
19 pub icon_web: String,
21}
22
23#[derive(Debug, Serialize, Clone, Deserialize)]
24pub struct RecommendRoom {
25 pub head_box: Option<serde_json::Value>,
27 pub area_v2_id: i32,
29 pub area_v2_parent_id: i32,
31 pub area_v2_name: String,
33 pub area_v2_parent_name: String,
35 pub broadcast_type: i32,
37 pub cover: String,
39 pub link: String,
41 pub online: i32,
43 #[serde(rename = "pendant_Info")]
45 pub pendant_info: serde_json::Value,
46 pub roomid: i64,
48 pub title: String,
50 pub uname: String,
52 pub face: String,
54 pub verify: serde_json::Value,
56 pub uid: i64,
58 pub keyframe: String,
60 pub is_auto_play: i32,
62 pub head_box_type: i32,
64 pub flag: i32,
66 pub session_id: String,
68 pub show_callback: String,
70 pub click_callback: String,
72 pub special_id: i32,
74 pub watched_show: WatchedShow,
76 pub is_nft: i32,
78 pub nft_dmark: String,
80 pub is_ad: bool,
82 pub ad_transparent_content: Option<serde_json::Value>,
84 pub show_ad_icon: bool,
86 pub status: bool,
88 pub followers: i32,
90}
91
92#[derive(Debug, Serialize, Clone, Deserialize)]
93pub struct RecommendData {
94 pub recommend_room_list: Vec<RecommendRoom>,
96 pub top_room_id: i64,
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use crate::probe::contract::HttpMethod;
104 use crate::probe::endpoint_contract::EndpointContract;
105 use crate::{ApiEnvelope, BpiClient, BpiError, BpiResult};
106
107 fn contract() -> BpiResult<EndpointContract> {
108 EndpointContract::from_slice(include_bytes!(
109 "../../tests/contracts/live/public-core/recommend/contract.json"
110 ))
111 }
112
113 #[ignore = "legacy live API test; requires explicit BPI_LIVE_TEST review"]
114 #[tokio::test]
115 async fn test_get_live_recommend() -> Result<(), Box<BpiError>> {
116 let bpi = BpiClient::new().expect("client should build");
117 let data = bpi.live().recommend().await?;
118
119 assert!(!data.recommend_room_list.is_empty());
120 Ok(())
121 }
122
123 #[test]
124 fn live_recommend_contract_matches_endpoint_request() -> BpiResult<()> {
125 let contract = contract()?;
126
127 assert_eq!(contract.name, "live.recommend");
128 assert_eq!(contract.request.method, HttpMethod::Get);
129 assert_eq!(
130 contract.request.url.as_str(),
131 "https://api.live.bilibili.com/xlive/web-interface/v1/webMain/getMoreRecList"
132 );
133 assert_eq!(
134 contract.request.query.get("platform").map(String::as_str),
135 Some("web")
136 );
137 assert_eq!(
138 contract
139 .request
140 .query
141 .get("web_location")
142 .map(String::as_str),
143 Some("333.1007")
144 );
145 assert_eq!(contract.cases.len(), 3);
146 assert_eq!(
147 contract.cases[0].response.rust_model.as_deref(),
148 Some("RecommendData")
149 );
150 Ok(())
151 }
152
153 #[test]
154 fn live_recommend_response_fixture_parses_declared_model() -> BpiResult<()> {
155 let payload = ApiEnvelope::<RecommendData>::from_slice(include_bytes!(
156 "../../tests/contracts/live/public-core/recommend/responses/success.json"
157 ))?
158 .into_payload()?;
159
160 assert_eq!(payload.recommend_room_list.len(), 1);
161 assert_eq!(payload.recommend_room_list[0].roomid, 1);
162 Ok(())
163 }
164
165 fn local_probe_body(profile: &str) -> Option<serde_json::Value> {
166 let path =
167 format!("target/bpi-probe-runs/live/public-core/recommend/{profile}.response.json");
168 let bytes = std::fs::read(path).ok()?;
169 let value: serde_json::Value = serde_json::from_slice(&bytes).ok()?;
170 value
171 .get("response")
172 .and_then(|response| response.get("body"))
173 .cloned()
174 }
175
176 #[test]
177 fn live_recommend_model_matches_local_probe_outputs_when_available() -> BpiResult<()> {
178 for profile in ["anonymous", "normal", "vip"] {
179 if let Some(body) = local_probe_body(profile) {
180 let payload =
181 serde_json::from_value::<ApiEnvelope<RecommendData>>(body)?.into_payload()?;
182 assert!(!payload.recommend_room_list.is_empty());
183 }
184 }
185 Ok(())
186 }
187}