Skip to main content

bpi_rs/user/
status_number.rs

1//! B站用户关系、UP主状态、导航栏等相关接口
2//!
3//! [查看 API 文档](https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/user)
4use serde::{Deserialize, Serialize};
5
6// --- 响应数据结构体 ---
7
8/// 用户关系状态数响应数据
9#[derive(Debug, Clone, Deserialize, Serialize)]
10pub struct RelationStatResponseData {
11    /// 目标用户 mid
12    pub mid: u64,
13    /// 关注数
14    pub following: u64,
15    /// 悄悄关注数
16    pub whisper: u64,
17    /// 黑名单数
18    pub black: u64,
19    /// 粉丝数
20    pub follower: u64,
21}
22
23/// UP主状态数中的视频播放量
24#[derive(Debug, Clone, Deserialize, Serialize)]
25pub struct UpstatArchive {
26    /// 视频播放量
27    pub view: u64,
28}
29
30/// UP主状态数中的专栏阅读量
31#[derive(Debug, Clone, Deserialize, Serialize)]
32pub struct UpstatArticle {
33    /// 专栏阅读量
34    pub view: u64,
35}
36
37/// UP主状态数响应数据
38#[derive(Debug, Clone, Deserialize, Serialize)]
39pub struct UpstatResponseData {
40    /// 视频播放量
41    pub archive: UpstatArchive,
42    /// 专栏阅读量
43    pub article: UpstatArticle,
44    /// 获赞次数
45    pub likes: u64,
46}
47
48/// 用户导航栏状态数中的视频列表数
49#[derive(Debug, Clone, Deserialize, Serialize)]
50pub struct NavnumChannel {
51    /// 视频列表数
52    pub master: u64,
53    /// 视频列表数
54    pub guest: u64,
55}
56
57/// 用户导航栏状态数中的收藏夹数
58#[derive(Debug, Clone, Deserialize, Serialize)]
59pub struct NavnumFavourite {
60    /// 全部收藏夹数
61    pub master: u64,
62    /// 公开收藏夹数
63    pub guest: u64,
64}
65
66/// 用户导航栏状态数响应数据
67#[derive(Debug, Clone, Deserialize, Serialize)]
68pub struct NavnumResponseData {
69    /// 投稿视频数
70    pub video: u64,
71    /// 追番数
72    pub bangumi: u64,
73    /// 追剧数
74    pub cinema: u64,
75    /// 视频列表数
76    pub channel: NavnumChannel,
77    /// 收藏夹数
78    pub favourite: NavnumFavourite,
79    /// 关注 TAG 数
80    pub tag: u64,
81    /// 投稿专栏数
82    pub article: u64,
83    pub playlist: u64,
84    /// 投稿图文数
85    pub album: u64,
86    /// 投稿音频数
87    pub audio: u64,
88    /// 投稿课程数
89    pub pugv: u64,
90    /// 动态数
91    pub opus: u64,
92    /// 视频合集数
93    #[serde(rename = "season_num")]
94    pub season_num: u64,
95}
96
97/// 相簿投稿数响应数据
98#[derive(Debug, Clone, Deserialize, Serialize)]
99pub struct AlbumCountResponseData {
100    /// 相簿总数
101    pub all_count: u64,
102    /// 发布绘画数
103    pub draw_count: u64,
104    /// 发布摄影数
105    pub photo_count: u64,
106    /// 发布日常(图片动态)数
107    pub daily_count: u64,
108}
109
110// --- 测试模块 ---
111
112#[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    // 请在运行测试前设置环境变量 `BPI_COOKIE`,以包含 SESSDATA 等登录信息
125    // mid 根据实际情况修改
126    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}