1use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Deserialize, Serialize)]
10pub struct RelationStatResponseData {
11 pub mid: u64,
13 pub following: u64,
15 pub whisper: u64,
17 pub black: u64,
19 pub follower: u64,
21}
22
23#[derive(Debug, Clone, Deserialize, Serialize)]
25pub struct UpstatArchive {
26 pub view: u64,
28}
29
30#[derive(Debug, Clone, Deserialize, Serialize)]
32pub struct UpstatArticle {
33 pub view: u64,
35}
36
37#[derive(Debug, Clone, Deserialize, Serialize)]
39pub struct UpstatResponseData {
40 pub archive: UpstatArchive,
42 pub article: UpstatArticle,
44 pub likes: u64,
46}
47
48#[derive(Debug, Clone, Deserialize, Serialize)]
50pub struct NavnumChannel {
51 pub master: u64,
53 pub guest: u64,
55}
56
57#[derive(Debug, Clone, Deserialize, Serialize)]
59pub struct NavnumFavourite {
60 pub master: u64,
62 pub guest: u64,
64}
65
66#[derive(Debug, Clone, Deserialize, Serialize)]
68pub struct NavnumResponseData {
69 pub video: u64,
71 pub bangumi: u64,
73 pub cinema: u64,
75 pub channel: NavnumChannel,
77 pub favourite: NavnumFavourite,
79 pub tag: u64,
81 pub article: u64,
83 pub playlist: u64,
84 pub album: u64,
86 pub audio: u64,
88 pub pugv: u64,
90 pub opus: u64,
92 #[serde(rename = "season_num")]
94 pub season_num: u64,
95}
96
97#[derive(Debug, Clone, Deserialize, Serialize)]
99pub struct AlbumCountResponseData {
100 pub all_count: u64,
102 pub draw_count: u64,
104 pub photo_count: u64,
106 pub daily_count: u64,
108}
109
110#[cfg(test)]
113mod tests {
114 use super::*;
115 use crate::ids::Mid;
116 use crate::probe::contract::HttpMethod;
117 use crate::probe::endpoint_contract::EndpointContract;
118 use crate::user::params::{
119 UserAlbumCountParams, UserNavStatParams, UserRelationStatParams, UserUpStatParams,
120 };
121 use crate::{ApiEnvelope, BpiClient, BpiError, BpiResult};
122 use tracing::info;
123
124 const TEST_MID: u64 = 332704117;
127 const TEST_UP_MID: u64 = 456664753;
128 const TEST_NAV_MID: u64 = 645769214;
129
130 #[ignore = "legacy live API test; requires explicit BPI_LIVE_TEST review"]
131 #[tokio::test]
132 async fn test_get_relation_stat() -> Result<(), BpiError> {
133 if std::env::var_os("BPI_LIVE_TEST").is_none() {
134 return Ok(());
135 }
136
137 let bpi = BpiClient::new().expect("client should build");
138 let data = bpi
139 .user()
140 .relation_stat(UserRelationStatParams::new(Mid::new(TEST_MID)?))
141 .await?;
142
143 info!("关系状态数: {:?}", data);
144 assert_eq!(data.mid.get(), TEST_MID);
145
146 Ok(())
147 }
148
149 #[ignore = "legacy live API test; requires explicit BPI_LIVE_TEST review"]
150 #[tokio::test]
151 async fn test_get_up_stat() -> Result<(), BpiError> {
152 if std::env::var_os("BPI_LIVE_TEST").is_none() {
153 return Ok(());
154 }
155
156 let bpi = BpiClient::new().expect("client should build");
157 let data = bpi
158 .user()
159 .up_stat(UserUpStatParams::new(Mid::new(TEST_UP_MID)?))
160 .await?;
161
162 info!("UP主状态数: {:?}", data);
163 assert!(data.likes > 0);
164
165 Ok(())
166 }
167
168 #[ignore = "legacy live API test; requires explicit BPI_LIVE_TEST review"]
169 #[tokio::test]
170 async fn test_get_nav_num() -> Result<(), BpiError> {
171 if std::env::var_os("BPI_LIVE_TEST").is_none() {
172 return Ok(());
173 }
174
175 let bpi = BpiClient::new().expect("client should build");
176 let data = bpi
177 .user()
178 .nav_stat(UserNavStatParams::new(Mid::new(TEST_NAV_MID)?))
179 .await?;
180
181 info!("用户导航栏状态数: {:?}", data);
182 assert!(data.video > 0);
183
184 Ok(())
185 }
186
187 #[ignore = "legacy live API test; requires explicit BPI_LIVE_TEST review"]
188 #[tokio::test]
189 async fn test_get_album_count() -> Result<(), BpiError> {
190 if std::env::var_os("BPI_LIVE_TEST").is_none() {
191 return Ok(());
192 }
193
194 let bpi = BpiClient::new().expect("client should build");
195 let data = bpi
196 .user()
197 .album_count(UserAlbumCountParams::new(Mid::new(TEST_NAV_MID)?))
198 .await?;
199
200 info!("相簿投稿数: {:?}", data);
201 assert!(data.all_count > 0);
202
203 Ok(())
204 }
205
206 fn public_read_contract(endpoint: &str) -> BpiResult<EndpointContract> {
207 let bytes: &[u8] = match endpoint {
208 "album-count" => {
209 include_bytes!("../../tests/contracts/user/public-read/album-count/contract.json")
210 }
211 "nav-stat" => {
212 include_bytes!("../../tests/contracts/user/public-read/nav-stat/contract.json")
213 }
214 "relation-stat" => {
215 include_bytes!("../../tests/contracts/user/public-read/relation-stat/contract.json")
216 }
217 "up-stat" => {
218 include_bytes!("../../tests/contracts/user/public-read/up-stat/contract.json")
219 }
220 _ => {
221 return Err(BpiError::invalid_parameter(
222 "endpoint",
223 "unknown user status contract",
224 ));
225 }
226 };
227
228 EndpointContract::from_slice(bytes)
229 }
230
231 #[test]
232 fn legacy_user_status_contracts_match_endpoint_requests() -> BpiResult<()> {
233 let relation = public_read_contract("relation-stat")?;
234 assert_eq!(relation.name, "user.relation_stat");
235 assert_eq!(relation.request.method, HttpMethod::Get);
236 assert_eq!(
237 relation.request.url.as_str(),
238 "https://api.bilibili.com/x/relation/stat"
239 );
240 assert_eq!(
241 relation.request.query.get("vmid").map(String::as_str),
242 Some("2")
243 );
244
245 let up_stat = public_read_contract("up-stat")?;
246 assert_eq!(up_stat.name, "user.up_stat");
247 assert_eq!(up_stat.request.method, HttpMethod::Get);
248 assert_eq!(
249 up_stat.request.url.as_str(),
250 "https://api.bilibili.com/x/space/upstat"
251 );
252 assert_eq!(
253 up_stat.request.query.get("mid").map(String::as_str),
254 Some("456664753")
255 );
256
257 let nav = public_read_contract("nav-stat")?;
258 assert_eq!(nav.name, "user.nav_stat");
259 assert_eq!(
260 nav.request.url.as_str(),
261 "https://api.bilibili.com/x/space/navnum"
262 );
263 assert_eq!(nav.request.query.get("mid").map(String::as_str), Some("2"));
264
265 let album = public_read_contract("album-count")?;
266 assert_eq!(album.name, "user.album_count");
267 assert_eq!(
268 album.request.url.as_str(),
269 "https://api.vc.bilibili.com/link_draw/v1/doc/upload_count"
270 );
271 assert_eq!(
272 album.request.query.get("uid").map(String::as_str),
273 Some("2")
274 );
275 Ok(())
276 }
277
278 #[test]
279 fn legacy_user_status_fixtures_parse_promoted_contract_models() -> BpiResult<()> {
280 let relation = ApiEnvelope::<RelationStatResponseData>::from_slice(include_bytes!(
281 "../../tests/contracts/user/public-read/relation-stat/responses/success.json"
282 ))?
283 .into_payload()?;
284 assert_eq!(relation.mid, 2);
285
286 let up_stat = ApiEnvelope::<UpstatResponseData>::from_slice(include_bytes!(
287 "../../tests/contracts/user/public-read/up-stat/responses/success.json"
288 ))?
289 .into_payload()?;
290 assert!(up_stat.archive.view >= up_stat.article.view);
291
292 let nav = ApiEnvelope::<NavnumResponseData>::from_slice(include_bytes!(
293 "../../tests/contracts/user/public-read/nav-stat/responses/success.json"
294 ))?
295 .into_payload()?;
296 let _total_content = nav.video + nav.article + nav.album + nav.audio + nav.opus;
297
298 let album = ApiEnvelope::<AlbumCountResponseData>::from_slice(include_bytes!(
299 "../../tests/contracts/user/public-read/album-count/responses/success.json"
300 ))?
301 .into_payload()?;
302 assert_eq!(
303 album.all_count,
304 album.draw_count + album.photo_count + album.daily_count
305 );
306 Ok(())
307 }
308}