1use serde::{Deserialize, Serialize};
5
6#[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#[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#[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#[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#[derive(Debug, Clone, Deserialize, Serialize)]
94pub struct PageInfo {
95 pub count: u64,
96 pub pn: u32,
97 pub ps: u32,
98}
99
100#[derive(Debug, Clone, Deserialize, Serialize)]
102pub struct EpisodicButton {
103 pub text: String,
104 pub uri: String,
105}
106
107#[derive(Debug, Clone, Deserialize, Serialize)]
109pub struct ContributedVideosResponseData {
110 pub list: ContributedVideoList,
112 pub page: PageInfo,
114 #[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#[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 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}