Skip to main content

bpi_rs/video/collection/
info.rs

1//! B站视频合集信息相关接口
2//!
3//! [查看 API 文档](https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/video)
4use crate::{ BilibiliRequest, BpiClient, BpiError, BpiResponse };
5use serde::{ Deserialize, Serialize };
6
7/// 稿件信息
8#[derive(Debug, Clone, Deserialize, Serialize)]
9pub struct ArchiveStat {
10    /// 稿件播放量
11    pub view: u64,
12    /// vt
13    pub vt: Option<u64>,
14}
15
16/// 合集/系列中的视频信息
17#[derive(Debug, Clone, Deserialize, Serialize)]
18pub struct Archive {
19    /// 稿件 avid
20    pub aid: u64,
21    /// 稿件 bvid
22    pub bvid: String,
23    /// 创建时间Unix 时间戳
24    pub ctime: u64,
25    /// 视频时长,单位为秒
26    pub duration: u64,
27    /// 是否是互动视频
28    pub interactive_video: bool,
29    /// 封面 URL
30    pub pic: String,
31    /// 会随着播放时间增长,播放完成后为 -1。单位为 %
32    pub playback_position: u64,
33    /// 发布日期Unix 时间戳
34    pub pubdate: u64,
35    /// 稿件信息
36    pub stat: ArchiveStat,
37    /// state
38    pub state: u64,
39    /// 稿件标题
40    pub title: String,
41    /// UGC 付费? 0: 否
42    pub ugc_pay: u64,
43    /// vt_display
44    pub vt_display: String,
45
46    pub is_lesson_video: Option<u32>,
47}
48
49/// 分页信息
50#[derive(Debug, Clone, Deserialize, Serialize)]
51pub struct PageInfo {
52    /// 分页页码
53    #[serde(alias = "num")]
54    pub page_num: u64,
55    /// 单页个数
56    #[serde(alias = "size")]
57    pub page_size: u64,
58    /// 总页数/总数量
59    pub total: u64,
60}
61
62/// 合集元数据
63#[derive(Debug, Clone, Deserialize, Serialize)]
64pub struct SeasonsArchivesMeta {
65    /// category
66    pub category: u64,
67    /// 合集封面 URL
68    pub cover: String,
69    /// 合集描述
70    pub description: String,
71    /// UP 主 ID
72    pub mid: u64,
73    /// 合集标题
74    pub name: String,
75    /// 发布时间Unix 时间戳
76    pub ptime: u64,
77    /// 合集 ID
78    pub season_id: u64,
79    /// 合集内视频数量
80    pub total: u64,
81}
82
83/// 获取视频合集信息响应数据
84#[derive(Debug, Clone, Deserialize, Serialize)]
85pub struct GetSeasonsArchivesData {
86    /// 稿件 avid 列表
87    pub aids: Vec<u64>,
88    /// 合集中的视频
89    pub archives: Vec<Archive>,
90    /// 合集元数据
91    pub meta: SeasonsArchivesMeta,
92    /// 分页信息
93    pub page: PageInfo,
94}
95
96/// 合集元数据
97#[derive(Debug, Clone, Deserialize, Serialize)]
98pub struct SeasonsMeta {
99    /// category
100    pub category: u64,
101    /// 封面 URL
102    pub cover: String,
103    /// 描述
104    pub description: String,
105    /// UP 主 ID
106    pub mid: u64,
107    /// 标题
108    pub name: String,
109    /// 创建时间?
110    pub ptime: u64,
111    /// 合集 ID
112    pub season_id: u64,
113    /// 视频数量
114    pub total: u64,
115}
116
117/// 系列元数据
118#[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/// 合集列表中的单个合集信息
136#[derive(Debug, Clone, Deserialize, Serialize)]
137pub struct SeasonsItem {
138    /// 系列视频列表
139    pub archives: Vec<Archive>,
140    /// 系列元数据
141    pub meta: SeasonsMeta,
142    /// 系列视频 aid 列表
143    pub recent_aids: Vec<u64>,
144}
145/// 系列列表中的单个系列信息
146#[derive(Debug, Clone, Deserialize, Serialize)]
147pub struct SeriesItem {
148    /// 系列视频列表
149    pub archives: Vec<Archive>,
150    /// 系列元数据
151    pub meta: SeriesMeta,
152    /// 系列视频 aid 列表
153    pub recent_aids: Vec<u64>,
154}
155
156/// 系列和合集列表信息
157#[derive(Debug, Clone, Deserialize, Serialize)]
158pub struct ItemsList {
159    /// 分页信息
160    pub page: PageInfo,
161    /// 合集列表
162    pub seasons_list: Vec<SeasonsItem>,
163    /// 系列列表
164    pub series_list: Vec<SeriesItem>,
165}
166
167/// 获取系列视频列表响应数据
168#[derive(Debug, Clone, Deserialize, Serialize)]
169pub struct GetSeasonsSeriesData {
170    /// 内容列表
171    pub items_lists: ItemsList,
172}
173
174/// 查询指定系列响应数据
175#[derive(Debug, Clone, Deserialize, Serialize)]
176pub struct GetSeriesData {
177    /// 系列信息
178    pub meta: SeriesMeta,
179    /// 系列 aid 列表
180    pub recent_aids: Vec<u64>,
181}
182
183/// 获取指定系列视频响应数据
184#[derive(Debug, Clone, Deserialize, Serialize)]
185pub struct GetSeriesArchivesData {
186    /// 视频 aid 列表
187    pub aids: Vec<u64>,
188    /// 页码信息
189    pub page: PageInfo,
190    /// 视频信息列表
191    pub archives: Vec<Archive>,
192}
193
194impl BpiClient {
195    /// 获取视频合集信息
196    ///
197    /// 此接口用于获取特定UP主某个视频合集的详细信息,包括合集内的所有视频列表和元数据。
198    ///
199    /// # 参数
200    /// * `mid` - 用户 mid,必填。
201    /// * `season_id` - 视频合集 ID,必填。
202    /// * `sort_reverse` - 排序方式,可选。`true`: 升序排序,`false`: 默认排序。
203    /// * `page_num` - 页码索引,可选,默认为 1。
204    /// * `page_size` - 单页内容数量,可选,默认为 30。
205    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            // 默认分页参数
217            ("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        // 签名
232        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(&params)
238            .send_bpi("获取视频合集信息").await
239    }
240
241    /// 只获取系列视频列表
242    ///
243    /// 此接口用于获取特定UP主创建的系列视频列表。
244    ///
245    /// # 参数
246    /// * `mid` - 用户 mid,必填。
247    /// * `page_num` - 页码索引,必填。
248    /// * `page_size` - 单页内容数量,必填。
249    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        // 签名
262        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(&params)
267            .send_bpi("只获取系列视频列表").await
268    }
269
270    /// 获取系列和合集视频列表
271    ///
272    /// 此接口用于获取特定UP主创建的系列和合集视频列表,返回结果包含两种类型。
273    ///
274    /// # 参数
275    /// * `mid` - 用户 mid,必填。
276    /// * `page_num` - 页码索引,可选,默认为 1。
277    /// * `page_size` - 每页数量,可选,默认为 20。
278    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        // 签名
294        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(&params)
299            .send_bpi("获取系列和合集视频列表").await
300    }
301
302    /// 查询指定系列信息
303    ///
304    /// 此接口用于获取指定系列的基本信息,如名称、描述、总视频数量等。
305    ///
306    /// # 参数
307    /// * `series_id` - 系列ID,必填。
308    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    /// 获取指定系列视频列表
320    ///
321    /// 此接口用于获取指定系列内的所有视频列表,支持分页和排序。
322    ///
323    /// # 参数
324    /// * `mid` - 用户 mid,必填。
325    /// * `series_id` - 系列ID,必填。
326    /// * `only_normal` - 作用尚不明确,可选,默认为 true。
327    /// * `sort` - 排序方式,可选。`desc`: 默认排序,`asc`: 升序排序。
328    /// * `page_num` - 页码索引,可选,默认为 1。
329    /// * `page_size` - 每页数量,可选,默认为 20。
330    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    // 测试用的 mid
372    const TEST_MID: u64 = 4279370;
373    // 测试用的合集 ID
374    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        // 注意:合集列表可能为空,无法直接断言不为空
410        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}