bpi_rs/video_ranking/
dynamic.rs

1use crate::{BilibiliRequest, BpiClient, BpiError, BpiResponse};
2use serde::{Deserialize, Serialize};
3
4// --- 获取分区最新视频列表 ---
5
6/// 分区最新视频的页面信息
7#[derive(Debug, Clone, Deserialize, Serialize)]
8pub struct RegionPage {
9    /// 总计视频数
10    pub count: u32,
11    /// 当前页码
12    pub num: u32,
13    /// 每页项数
14    pub size: u32,
15}
16
17/// 分区最新视频列表的数据
18#[derive(Debug, Clone, Deserialize, Serialize)]
19pub struct RegionArchivesData {
20    /// 视频列表
21    pub archives: Vec<serde_json::Value>, // archives内容复杂,这里用Value代替
22    /// 页面信息
23    pub page: RegionPage,
24}
25
26#[derive(Debug, Clone, Deserialize, Serialize)]
27pub struct NewListRankResult {
28    /// 发布时间
29    #[serde(rename = "pubdate")]
30    pub pub_date: String,
31    /// 封面图
32    pub pic: String,
33    /// 标签
34    pub tag: String,
35    /// 时长 (秒)
36    pub duration: u32,
37    /// avid
38    pub id: u64,
39    /// 排序分数
40    pub rank_score: Option<u64>,
41    /// 是否有角标
42    pub badgepay: bool,
43    /// 发送时间 (UNIX 秒级时间戳)
44    pub senddate: Option<u64>,
45    /// UP 主名
46    pub author: String,
47    /// 评论数
48    pub review: u64,
49    /// UP 主 mid
50    pub mid: u64,
51    /// 是否为联合投稿
52    pub is_union_video: u8,
53    /// 排序索引号
54    pub rank_index: Option<u64>,
55    /// 类型
56    #[serde(rename = "type")]
57    pub type_name: String,
58    /// 播放数
59    pub play: String,
60    /// 弹幕数
61    #[serde(rename = "video_review")]
62    pub video_review: u64,
63    /// 是否付费
64    pub is_pay: u8,
65    /// 收藏数
66    pub favorites: u64,
67    /// 视频播放页 URL
68    pub arcurl: String,
69    /// bvid
70    pub bvid: String,
71    /// 标题
72    pub title: String,
73    /// 简介
74    pub description: String,
75    // 忽略其他作用不明确的字段
76}
77
78/// 带排序的分区投稿列表数据
79#[derive(Debug, Clone, Deserialize, Serialize)]
80pub struct NewListRankData {
81    /// 结果本体
82    pub result: Option<Vec<NewListRankResult>>,
83    /// 总计视频数
84    #[serde(rename = "numResults")]
85    pub num_results: u32,
86    /// 页码
87    pub page: u32,
88    /// 视频数
89    pub pagesize: u32,
90    /// 结果信息
91    pub msg: String,
92}
93
94impl BpiClient {
95    /// 获取分区最新视频列表
96    ///
97    /// 文档: https://socialsisteryi.github.io/bilibili-API-collect/docs/video_ranking/dynamic.html#获取分区最新视频列表
98    ///
99    /// # 参数
100    /// | 名称   | 类型         | 说明                 |
101    /// | ------ | ------------| -------------------- |
102    /// | `rid`  | u32         | 分区ID               |
103    /// | `pn`   | Option<u32> | 页码,可选           |
104    /// | `ps`   | Option<u32> | 每页数量,可选       |
105    pub async fn video_region_dynamic(
106        &self,
107        rid: u32,
108        pn: Option<u32>,
109        ps: Option<u32>,
110    ) -> Result<BpiResponse<RegionArchivesData>, BpiError> {
111        let mut request = self
112            .get("https://api.bilibili.com/x/web-interface/dynamic/region")
113            .query(&[("rid", rid.to_string())]);
114
115        if let Some(pn) = pn {
116            request = request.query(&[("pn", pn.to_string())]);
117        }
118        if let Some(ps) = ps {
119            request = request.query(&[("ps", ps.to_string())]);
120        }
121
122        request.send_bpi("获取分区最新视频列表").await
123    }
124
125    /// 获取分区标签近期互动列表
126    ///
127    /// 文档: https://socialsisteryi.github.io/bilibili-API-collect/docs/video_ranking/dynamic.html#获取分区标签近期互动列表
128    ///
129    /// # 参数
130    /// | 名称    | 类型         | 说明                 |
131    /// | ------- | ------------| -------------------- |
132    /// | `rid`   | u32         | 分区ID               |
133    /// | `tag_id`| u64         | 标签ID               |
134    /// | `pn`    | Option<u32> | 页码,可选           |
135    /// | `ps`    | Option<u32> | 每页数量,可选       |
136    pub async fn video_region_tag_dynamic(
137        &self,
138        rid: u32,
139        tag_id: u64,
140        pn: Option<u32>,
141        ps: Option<u32>,
142    ) -> Result<BpiResponse<RegionArchivesData>, BpiError> {
143        let mut request = self
144            .get("https://api.bilibili.com/x/web-interface/dynamic/tag")
145            .query(&[("rid", rid.to_string()), ("tag_id", tag_id.to_string())]);
146
147        if let Some(pn) = pn {
148            request = request.query(&[("pn", pn.to_string())]);
149        }
150        if let Some(ps) = ps {
151            request = request.query(&[("ps", ps.to_string())]);
152        }
153
154        request.send_bpi("获取分区标签近期互动列表").await
155    }
156
157    /// 获取分区近期投稿列表
158    ///
159    /// 文档: https://socialsisteryi.github.io/bilibili-API-collect/docs/video_ranking/dynamic.html#获取分区近期投稿列表
160    ///
161    /// # 参数
162    /// | 名称   | 类型         | 说明                 |
163    /// | ------ | ------------| -------------------- |
164    /// | `rid`  | u32         | 分区ID               |
165    /// | `pn`   | Option<u32> | 页码,可选           |
166    /// | `ps`   | Option<u32> | 每页数量,可选       |
167    /// | `typ`  | Option<u32> | 类型,可选           |
168    pub async fn video_region_newlist(
169        &self,
170        rid: u32,
171        pn: Option<u32>,
172        ps: Option<u32>,
173        typ: Option<u32>,
174    ) -> Result<BpiResponse<RegionArchivesData>, BpiError> {
175        let mut request = self
176            .get("https://api.bilibili.com/x/web-interface/newlist")
177            .query(&[("rid", rid.to_string())]);
178
179        if let Some(pn) = pn {
180            request = request.query(&[("pn", pn.to_string())]);
181        }
182        if let Some(ps) = ps {
183            request = request.query(&[("ps", ps.to_string())]);
184        }
185        if let Some(t) = typ {
186            request = request.query(&[("type", t.to_string())]);
187        }
188
189        request.send_bpi("获取分区近期投稿列表").await
190    }
191
192    /// 获取分区近期投稿列表(带排序)
193    ///
194    /// 文档: https://socialsisteryi.github.io/bilibili-API-collect/docs/video_ranking/dynamic.html#获取分区近期投稿列表带排序
195    ///
196    /// # 参数
197    /// | 名称        | 类型           | 说明                 |
198    /// | ----------- | --------------| -------------------- |
199    /// | `cate_id`   | u32           | 分类ID               |
200    /// | `order`     | Option<&str>  | 排序方式,可选       |
201    /// | `page`      | Option<u32>   | 页码,可选           |
202    /// | `pagesize`  | u32           | 每页数量             |
203    /// | `time_from` | &str          | 起始日期(YYYYMMDD)   |
204    /// | `time_to`   | &str          | 结束日期(YYYYMMDD)   |
205    pub async fn video_region_newlist_rank(
206        &self,
207        cate_id: u32,
208        order: Option<&str>,
209        page: Option<u32>,
210        pagesize: u32,
211        time_from: &str,
212        time_to: &str,
213    ) -> Result<BpiResponse<NewListRankData>, BpiError> {
214        let cate_id = cate_id.to_string();
215        let pagesize = pagesize.to_string();
216        let mut request = self
217            .get("https://api.bilibili.com/x/web-interface/newlist_rank")
218            .query(&[
219                ("search_type", "video"),
220                ("view_type", "hot_rank"),
221                ("cate_id", cate_id.as_str()),
222                ("pagesize", pagesize.as_str()),
223                ("time_from", time_from),
224                ("time_to", time_to),
225            ]);
226
227        if let Some(o) = order {
228            request = request.query(&[("order", o)]);
229        }
230        if let Some(p) = page {
231            request = request.query(&[("page", p.to_string())]);
232        }
233
234        request.send_bpi("获取分区近期投稿列表 (带排序)").await
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241    use chrono::{Duration, Local};
242    use tracing::info;
243
244    #[tokio::test]
245    async fn test_video_region_dynamic() {
246        let bpi = BpiClient::new();
247        let rid = 21; // 日常分区
248        let ps = Some(2);
249        let pn = Some(1);
250        let resp = bpi.video_region_dynamic(rid, pn, ps).await;
251
252        info!("{:?}", resp);
253        assert!(resp.is_ok());
254
255        let resp_data = resp.unwrap();
256        info!("code: {}", resp_data.code);
257        if let Some(data) = resp_data.data {
258            info!("total videos: {}", data.page.count);
259            info!("first item: {:?}", data.archives.first());
260        }
261    }
262
263    #[tokio::test]
264    async fn test_video_region_tag_dynamic() {
265        let bpi = BpiClient::new();
266        let rid = 136; // 音游分区
267        let tag_id = 10026108; // Phigros
268        let ps = Some(2);
269        let pn = Some(1);
270        let resp = bpi.video_region_tag_dynamic(rid, tag_id, pn, ps).await;
271
272        info!("{:?}", resp);
273        assert!(resp.is_ok());
274
275        let resp_data = resp.unwrap();
276        info!("code: {}", resp_data.code);
277        if let Some(data) = resp_data.data {
278            info!("total videos: {}", data.page.count);
279            info!("first item: {:?}", data.archives.first());
280        }
281    }
282
283    #[tokio::test]
284    async fn test_video_region_newlist_rank() {
285        let bpi = BpiClient::new();
286        let cate_id = 231; // 计算机技术
287        let pagesize = 2;
288        let today = Local::now().date_naive();
289        let seven_days_ago = today - Duration::days(7);
290
291        let time_from = seven_days_ago.format("%Y%m%d").to_string();
292        let time_to = today.format("%Y%m%d").to_string();
293
294        let resp = bpi
295            .video_region_newlist_rank(
296                cate_id,
297                Some("click"),
298                Some(1),
299                pagesize,
300                &time_from,
301                &time_to,
302            )
303            .await;
304
305        info!("{:?}", resp);
306        assert!(resp.is_ok());
307
308        let resp_data = resp.unwrap();
309        info!("code: {}", resp_data.code);
310        if let Some(data) = resp_data.data {
311            info!("total results: {}", data.num_results);
312            info!("first result: {:?}", data.result.unwrap().first());
313        }
314    }
315}