1use crate::{ BilibiliRequest, BpiClient, BpiError, BpiResponse };
5use serde::{ Deserialize, Serialize };
6
7#[derive(Debug, Clone, Deserialize, Serialize)]
9pub struct ArchiveStat {
10 pub view: u64,
12 pub vt: Option<u64>,
14}
15
16#[derive(Debug, Clone, Deserialize, Serialize)]
18pub struct Archive {
19 pub aid: u64,
21 pub bvid: String,
23 pub ctime: u64,
25 pub duration: u64,
27 pub interactive_video: bool,
29 pub pic: String,
31 pub playback_position: u64,
33 pub pubdate: u64,
35 pub stat: ArchiveStat,
37 pub state: u64,
39 pub title: String,
41 pub ugc_pay: u64,
43 pub vt_display: String,
45
46 pub is_lesson_video: Option<u32>,
47}
48
49#[derive(Debug, Clone, Deserialize, Serialize)]
51pub struct PageInfo {
52 #[serde(alias = "num")]
54 pub page_num: u64,
55 #[serde(alias = "size")]
57 pub page_size: u64,
58 pub total: u64,
60}
61
62#[derive(Debug, Clone, Deserialize, Serialize)]
64pub struct SeasonsArchivesMeta {
65 pub category: u64,
67 pub cover: String,
69 pub description: String,
71 pub mid: u64,
73 pub name: String,
75 pub ptime: u64,
77 pub season_id: u64,
79 pub total: u64,
81}
82
83#[derive(Debug, Clone, Deserialize, Serialize)]
85pub struct GetSeasonsArchivesData {
86 pub aids: Vec<u64>,
88 pub archives: Vec<Archive>,
90 pub meta: SeasonsArchivesMeta,
92 pub page: PageInfo,
94}
95
96#[derive(Debug, Clone, Deserialize, Serialize)]
98pub struct SeasonsMeta {
99 pub category: u64,
101 pub cover: String,
103 pub description: String,
105 pub mid: u64,
107 pub name: String,
109 pub ptime: u64,
111 pub season_id: u64,
113 pub total: u64,
115}
116
117#[derive(Debug, Clone, Deserialize, Serialize)]
119pub struct SeriesMeta {
120 pub category: u64,
121 pub creator: String,
122 pub ctime: u64,
123 pub description: String,
124 pub keywords: Vec<String>,
125 pub last_update_ts: u64,
126 pub mid: u64,
127 pub mtime: u64,
128 pub name: String,
129 pub raw_keywords: String,
130 pub series_id: u64,
131 pub state: u64,
132 pub total: u64,
133 pub cover: Option<String>,
134}
135#[derive(Debug, Clone, Deserialize, Serialize)]
137pub struct SeasonsItem {
138 pub archives: Vec<Archive>,
140 pub meta: SeasonsMeta,
142 pub recent_aids: Vec<u64>,
144}
145#[derive(Debug, Clone, Deserialize, Serialize)]
147pub struct SeriesItem {
148 pub archives: Vec<Archive>,
150 pub meta: SeriesMeta,
152 pub recent_aids: Vec<u64>,
154}
155
156#[derive(Debug, Clone, Deserialize, Serialize)]
158pub struct ItemsList {
159 pub page: PageInfo,
161 pub seasons_list: Vec<SeasonsItem>,
163 pub series_list: Vec<SeriesItem>,
165}
166
167#[derive(Debug, Clone, Deserialize, Serialize)]
169pub struct GetSeasonsSeriesData {
170 pub items_lists: ItemsList,
172}
173
174#[derive(Debug, Clone, Deserialize, Serialize)]
176pub struct GetSeriesData {
177 pub meta: SeriesMeta,
179 pub recent_aids: Vec<u64>,
181}
182
183#[derive(Debug, Clone, Deserialize, Serialize)]
185pub struct GetSeriesArchivesData {
186 pub aids: Vec<u64>,
188 pub page: PageInfo,
190 pub archives: Vec<Archive>,
192}
193
194impl BpiClient {
195 pub async fn video_seasons_list(
206 &self,
207 mid: u64,
208 season_id: u64,
209 sort_reverse: Option<bool>,
210 page_num: Option<u64>,
211 page_size: Option<u64>
212 ) -> Result<BpiResponse<GetSeasonsArchivesData>, BpiError> {
213 let mut params = vec![
214 ("mid", mid.to_string()),
215 ("season_id", season_id.to_string()),
216 ("page_num", "1".to_string()),
218 ("page_size", "20".to_string())
219 ];
220
221 if let Some(sort) = sort_reverse {
222 params.push(("sort_reverse", sort.to_string()));
223 }
224 if let Some(num) = page_num {
225 params.push(("page_num", num.to_string()));
226 }
227 if let Some(size) = page_size {
228 params.push(("page_size", size.to_string()));
229 }
230
231 let params = self.get_wbi_sign2(params).await?;
233
234 self
235 .get("https://api.bilibili.com/x/polymer/web-space/seasons_archives_list")
236 .with_bilibili_headers()
237 .query(¶ms)
238 .send_bpi("获取视频合集信息").await
239 }
240
241 pub async fn video_series_list(
250 &self,
251 mid: u64,
252 page_num: u64,
253 page_size: u64
254 ) -> Result<BpiResponse<GetSeasonsSeriesData>, BpiError> {
255 let params = vec![
256 ("mid", mid.to_string()),
257 ("page_num", page_num.to_string()),
258 ("page_size", page_size.to_string())
259 ];
260
261 let params = self.get_wbi_sign2(params).await?;
263
264 self
265 .get("https://api.bilibili.com/x/polymer/web-space/home/seasons_series")
266 .query(¶ms)
267 .send_bpi("只获取系列视频列表").await
268 }
269
270 pub async fn video_seasons_series_list(
279 &self,
280 mid: u64,
281 page_num: Option<u64>,
282 page_size: Option<u64>
283 ) -> Result<BpiResponse<GetSeasonsSeriesData>, BpiError> {
284 let mut params = vec![("mid", mid.to_string())];
285
286 if let Some(num) = page_num {
287 params.push(("page_num", num.to_string()));
288 }
289 if let Some(size) = page_size {
290 params.push(("page_size", size.to_string()));
291 }
292
293 let params = self.get_wbi_sign2(params).await?;
295
296 self
297 .get("https://api.bilibili.com/x/polymer/web-space/seasons_series_list")
298 .query(¶ms)
299 .send_bpi("获取系列和合集视频列表").await
300 }
301
302 pub async fn video_series_info(
309 &self,
310 series_id: u64
311 ) -> Result<BpiResponse<GetSeriesData>, BpiError> {
312 let req = self
313 .get("https://api.bilibili.com/x/series/series")
314 .query(&[("series_id", &series_id.to_string())]);
315
316 req.send_bpi("查询指定系列信息").await
317 }
318
319 pub async fn video_series_archives(
331 &self,
332 mid: u64,
333 series_id: u64,
334 only_normal: Option<bool>,
335 sort: Option<&str>,
336 page_num: Option<u64>,
337 page_size: Option<u64>
338 ) -> Result<BpiResponse<GetSeriesArchivesData>, BpiError> {
339 let mut req = self.get("https://api.bilibili.com/x/series/archives").query(
340 &[
341 ("mid", &mid.to_string()),
342 ("series_id", &series_id.to_string()),
343 ]
344 );
345
346 if let Some(normal) = only_normal {
347 req = req.query(&[("only_normal", &normal.to_string())]);
348 }
349
350 if let Some(s) = sort {
351 req = req.query(&[("sort", s)]);
352 }
353
354 if let Some(num) = page_num {
355 req = req.query(&[("pn", &num.to_string())]);
356 }
357
358 if let Some(size) = page_size {
359 req = req.query(&[("ps", &size.to_string())]);
360 }
361
362 req.send_bpi("获取指定系列视频列表").await
363 }
364}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369 use tracing::info;
370
371 const TEST_MID: u64 = 4279370;
373 const TEST_SEASON_ID: u64 = 4294056;
375
376 const TEST_SERIES_ID: u64 = 250285;
377
378 #[tokio::test]
379 async fn test_video_seasons_archives_list() -> Result<(), BpiError> {
380 let bpi = BpiClient::new();
381 let resp = bpi.video_seasons_list(TEST_MID, TEST_SEASON_ID, Some(false), None, None).await?;
382 let data = resp.into_data()?;
383
384 info!("测试结果: {:?}", data);
385 assert!(!data.archives.is_empty(), "返回的合集视频列表不应为空");
386 assert_eq!(data.meta.season_id, TEST_SEASON_ID, "合集ID应与请求ID一致");
387 Ok(())
388 }
389
390 #[tokio::test]
391 async fn test_video_seasons_series_only() -> Result<(), BpiError> {
392 let bpi = BpiClient::new();
393 let resp = bpi.video_series_list(TEST_MID, 1, 10).await?;
394 let data = resp.into_data()?;
395
396 info!("测试结果: {:?}", data);
397
398 Ok(())
399 }
400
401 #[tokio::test]
402 async fn test_video_seasons_series_list() -> Result<(), BpiError> {
403 let bpi = BpiClient::new();
404 let resp = bpi.video_seasons_series_list(TEST_MID, Some(1), Some(5)).await?;
405 let data = resp.into_data()?;
406
407 info!("测试结果: {:?}", data);
408 assert!(!data.items_lists.series_list.is_empty(), "返回的系列列表不应为空");
409 assert_eq!(data.items_lists.page.page_size, 5, "返回的每页数量应为5");
411 Ok(())
412 }
413
414 #[tokio::test]
415 async fn test_video_series_info() -> Result<(), BpiError> {
416 let bpi = BpiClient::new();
417 let resp = bpi.video_series_info(TEST_SERIES_ID).await?;
418 let data = resp.into_data()?;
419
420 info!("测试结果: {:?}", data);
421 assert_eq!(data.meta.series_id, TEST_SERIES_ID, "返回的系列ID应与请求ID一致");
422 assert!(!data.recent_aids.is_empty(), "最近的aid列表不应为空");
423 Ok(())
424 }
425
426 #[tokio::test]
427 async fn test_video_series_archives() -> Result<(), BpiError> {
428 let bpi = BpiClient::new();
429 let resp = bpi.video_series_archives(
430 TEST_MID,
431 TEST_SERIES_ID,
432 None,
433 Some("asc"),
434 Some(1),
435 Some(10)
436 ).await?;
437 let data = resp.into_data()?;
438
439 info!("测试结果: {:?}", data);
440 assert!(!data.archives.is_empty(), "返回的系列视频列表不应为空");
441 Ok(())
442 }
443}