Skip to main content

bpi_rs/user/
search.rs

1//! B站用户搜索相关接口
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 CollectionStat {
11    pub coin: u64,
12    pub danmaku: u64,
13    pub favorite: u64,
14    pub like: u64,
15    pub mtime: u64,
16    pub reply: u64,
17    pub season_id: u64,
18    pub share: u64,
19    pub view: u64,
20    pub vt: u64,
21    pub vv: u64,
22}
23
24/// 所属合集或课堂元数据
25#[derive(Debug, Clone, Deserialize, Serialize)]
26pub struct VideoMeta {
27    pub attribute: u64,
28    pub cover: String,
29    pub ep_count: u64,
30    pub ep_num: u64,
31    pub first_aid: u64,
32    pub id: u64,
33    pub intro: String,
34    pub mid: u64,
35    pub ptime: u64,
36    pub sign_state: u64,
37    pub stat: Option<CollectionStat>,
38    pub title: String,
39}
40
41/// 投稿视频列表项
42#[derive(Debug, Clone, Deserialize, Serialize)]
43pub struct ContributedVideo {
44    pub aid: u64,
45    pub attribute: u64,
46    pub author: String,
47    pub bvid: String,
48    pub comment: u64,
49    pub copyright: String,
50    pub created: u64,
51    pub description: String,
52    pub elec_arc_type: u8,
53    pub enable_vt: u8,
54    pub hide_click: bool,
55    pub is_avoided: u8,
56    pub is_charging_arc: bool,
57    pub is_lesson_video: u8,
58    pub is_lesson_finished: u8,
59    pub is_live_playback: u8,
60    pub is_pay: u8,
61    pub is_self_view: bool,
62    pub is_steins_gate: u8,
63    pub is_union_video: u8,
64    pub jump_url: Option<String>,
65    pub length: String,
66    pub mid: u64,
67    pub meta: Option<VideoMeta>,
68    pub pic: String,
69    pub play: u64,
70    pub playback_position: u64,
71    pub review: u64,
72    pub season_id: u64,
73    pub subtitle: String,
74    pub title: String,
75    pub typeid: u64,
76    pub video_review: u64,
77    pub vt: u64,
78    pub vt_display: String,
79}
80
81/// 投稿视频列表
82#[derive(Debug, Clone, Deserialize, Serialize)]
83pub struct ContributedVideoList {
84    #[serde(default)]
85    pub slist: Vec<serde_json::Value>,
86    #[serde(default)]
87    pub tlist: serde_json::Value,
88    #[serde(default)]
89    pub vlist: Vec<ContributedVideo>,
90}
91
92/// 页面信息
93#[derive(Debug, Clone, Deserialize, Serialize)]
94pub struct PageInfo {
95    pub count: u64,
96    pub pn: u32,
97    pub ps: u32,
98}
99
100/// 播放全部按钮
101#[derive(Debug, Clone, Deserialize, Serialize)]
102pub struct EpisodicButton {
103    pub text: String,
104    pub uri: String,
105}
106
107/// 用户投稿视频明细响应数据
108#[derive(Debug, Clone, Deserialize, Serialize)]
109pub struct ContributedVideosResponseData {
110    /// 列表信息
111    pub list: ContributedVideoList,
112    /// 页面信息
113    pub page: PageInfo,
114    /// “播放全部”按钮
115    #[serde(default)]
116    pub episodic_button: Option<EpisodicButton>,
117    #[serde(default)]
118    pub is_risk: bool,
119    #[serde(default)]
120    pub gaia_res_type: u8,
121    #[serde(default)]
122    pub gaia_data: Option<serde_json::Value>,
123}
124
125// --- 测试模块 ---
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130    use crate::ids::Mid;
131    use crate::probe::contract::HttpMethod;
132    use crate::probe::endpoint_contract::EndpointContract;
133    use crate::user::params::UserUploadedVideosParams;
134    use crate::{ApiEnvelope, BpiClient, BpiError, BpiResult};
135    use tracing::info;
136
137    // 假设这是一个已知用户
138    const TEST_MID: u64 = 53456;
139    const TEST_KEYWORD: &str = "科技";
140
141    #[ignore = "legacy live API test; requires explicit BPI_LIVE_TEST review"]
142    #[tokio::test]
143    async fn test_user_contributed_videos_default() -> Result<(), BpiError> {
144        if std::env::var_os("BPI_LIVE_TEST").is_none() {
145            return Ok(());
146        }
147
148        let bpi = BpiClient::new().expect("client should build");
149        let data = bpi
150            .user()
151            .uploaded_videos(
152                UserUploadedVideosParams::new(Mid::new(TEST_MID)?)
153                    .with_page(1)?
154                    .with_page_size(2)?,
155            )
156            .await?;
157
158        info!("用户投稿视频明细: {:?}", data);
159        assert_eq!(data.page.pn, 1);
160        assert_eq!(data.page.ps, 2);
161        assert_eq!(data.list.videos.len(), 2);
162        assert!(data.page.count > 0);
163
164        Ok(())
165    }
166
167    #[ignore = "legacy live API test; requires explicit BPI_LIVE_TEST review"]
168    #[tokio::test]
169    async fn test_user_contributed_videos_with_keyword() -> Result<(), BpiError> {
170        if std::env::var_os("BPI_LIVE_TEST").is_none() {
171            return Ok(());
172        }
173
174        let bpi = BpiClient::new().expect("client should build");
175        let data = bpi
176            .user()
177            .uploaded_videos(
178                UserUploadedVideosParams::new(Mid::new(TEST_MID)?)
179                    .with_keyword(TEST_KEYWORD)
180                    .with_page(1)?
181                    .with_page_size(10)?,
182            )
183            .await?;
184
185        info!("用户投稿视频明细(关键词): {:?}", data);
186        assert!(data.page.count > 0);
187
188        Ok(())
189    }
190
191    fn uploaded_videos_contract() -> BpiResult<EndpointContract> {
192        EndpointContract::from_slice(include_bytes!(
193            "../../tests/contracts/user/public-read/uploaded-videos/contract.json"
194        ))
195    }
196
197    #[test]
198    fn legacy_user_contributed_videos_contract_matches_endpoint_request() -> BpiResult<()> {
199        let contract = uploaded_videos_contract()?;
200
201        assert_eq!(contract.name, "user.uploaded_videos");
202        assert_eq!(contract.request.method, HttpMethod::Get);
203        assert_eq!(
204            contract.request.url.as_str(),
205            "https://api.bilibili.com/x/space/wbi/arc/search"
206        );
207        assert!(contract.request.auth.requires_wbi());
208        assert_eq!(
209            contract.request.query.get("mid").map(String::as_str),
210            Some("2")
211        );
212        assert_eq!(
213            contract.request.query.get("order").map(String::as_str),
214            Some("pubdate")
215        );
216        assert_eq!(
217            contract.request.query.get("pn").map(String::as_str),
218            Some("1")
219        );
220        assert_eq!(
221            contract.request.query.get("ps").map(String::as_str),
222            Some("30")
223        );
224        Ok(())
225    }
226
227    #[test]
228    fn legacy_user_contributed_videos_fixture_parses_promoted_contract_model() -> BpiResult<()> {
229        let videos = ApiEnvelope::<ContributedVideosResponseData>::from_slice(include_bytes!(
230            "../../tests/contracts/user/public-read/uploaded-videos/responses/success.json"
231        ))?
232        .into_payload()?;
233
234        assert_eq!(videos.page.pn, 1);
235        assert_eq!(videos.page.ps, 30);
236        Ok(())
237    }
238}