Skip to main content

bpi_rs/video/
recommend.rs

1//! 视频推荐相关接口
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
9/// 视频作者信息
10#[derive(Debug, Clone, Deserialize, Serialize)]
11pub struct Owner {
12    /// UP主mid
13    pub mid: u64,
14    /// UP昵称
15    pub name: String,
16    /// 头像URL
17    pub face: String,
18}
19
20/// 视频统计数据
21#[derive(Debug, Clone, Deserialize, Serialize)]
22pub struct Stat {
23    /// 播放量
24    pub view: u64,
25    /// 视频aid
26    pub aid: u64,
27    /// 弹幕数
28    pub danmaku: u64,
29    /// 评论数
30    pub reply: u64,
31    /// 收藏数
32    pub favorite: u64,
33    /// 硬币数
34    pub coin: u64,
35    /// 分享数
36    pub share: u64,
37    /// 当前排名
38    pub now_rank: u64,
39    /// 历史最高排名
40    pub his_rank: u64,
41    /// 点赞数
42    pub like: u64,
43    /// 点踩数
44    pub dislike: u64,
45}
46
47/// 主页推荐视频/直播统计数据
48#[derive(Debug, Clone, Deserialize, Serialize)]
49pub struct HomeRmdStat {
50    /// 播放量
51    pub view: u64,
52    /// 弹幕数
53    pub danmaku: u64,
54    /// 点赞数
55    pub like: u64,
56}
57
58/// 视频版权信息
59#[derive(Debug, Clone, Deserialize, Serialize)]
60pub struct Rights {
61    pub bp: u8,
62    pub elec: u8,
63    pub download: u8,
64    pub movie: u8,
65    pub pay: u8,
66    pub hd5: u8,
67    pub no_reprint: u8,
68    pub autoplay: u8,
69    pub ugc_pay: u8,
70    pub is_cooperation: u8,
71    pub ugc_pay_preview: u8,
72    pub no_background: u8,
73}
74
75/// 视频分辨率信息
76#[derive(Debug, Clone, Deserialize, Serialize)]
77pub struct Dimension {
78    pub width: u32,
79    pub height: u32,
80    pub rotate: u8,
81}
82
83/// 单视频推荐列表项
84#[derive(Debug, Clone, Deserialize, Serialize)]
85pub struct RelatedVideo {
86    pub aid: u64,
87    pub videos: u32,
88    pub tid: u32,
89    pub tname: String,
90    pub copyright: u8,
91    pub pic: String,
92    pub title: String,
93    pub pubdate: u64,
94    pub ctime: u64,
95    pub desc: String,
96    pub state: i8,
97    pub duration: u64,
98    pub rights: Rights,
99    pub owner: Owner,
100    pub stat: Stat,
101    pub dynamic: String,
102    pub cid: u64,
103    pub dimension: Dimension,
104    pub bvid: String,
105    #[serde(default)]
106    pub short_link_v2: String,
107}
108
109/// 首页推荐视频列表项中的推荐理由
110#[derive(Debug, Clone, Deserialize, Serialize)]
111pub struct RcmdReason {
112    /// 原因类型, 0: 无, 1: 已关注, 3: 高点赞量
113    #[serde(rename = "reason_type")]
114    pub reason_type: u8,
115    /// 原因描述
116    pub content: Option<String>,
117}
118
119/// 首页推荐视频列表项
120#[derive(Debug, Clone, Deserialize, Serialize)]
121pub struct RcmdItem {
122    pub av_feature: Option<serde_json::Value>,
123    /// 商业推广信息,若无则为 null
124    pub business_info: Option<serde_json::Value>,
125    /// 视频bvid
126    pub bvid: String,
127    /// 稿件cid
128    pub cid: u64,
129    /// 视频时长
130    pub duration: u64,
131    /// 目标类型, "av": 视频, "ogv": 边栏, "live": 直播
132    pub goto: String,
133    /// 视频aid / 直播间id
134    pub id: u64,
135    /// 是否已关注, 0: 未关注, 1: 已关注
136    pub is_followed: u8,
137    pub is_stock: u8,
138    /// UP主信息
139    pub owner: Owner,
140    /// 封面
141    pub pic: String,
142    pub pos: u8,
143    /// 发布时间
144    pub pubdate: u64,
145    /// 推荐理由
146    pub rcmd_reason: Option<RcmdReason>,
147    /// 直播间信息
148    pub room_info: Option<serde_json::Value>,
149    pub show_info: u8,
150    /// 视频状态信息
151    pub stat: Option<HomeRmdStat>,
152    /// 标题
153    pub title: String,
154    pub track_id: String,
155    /// 目标页 URI
156    pub uri: String,
157}
158
159/// 首页推荐列表响应数据
160#[derive(Debug, Clone, Deserialize, Serialize)]
161pub struct RcmdFeedResponseData {
162    /// 推荐列表
163    pub item: Vec<RcmdItem>,
164    /// 用户mid,未登录为0
165    pub mid: u64,
166    pub preload_expose_pct: f32,
167    pub preload_floor_expose_pct: f32,
168}
169
170impl BpiClient {
171    /// 获取单视频推荐列表
172    ///
173    /// # 文档
174    /// [查看API文档](https://socialsisteryi.github.io/bilibili-API-collect/docs/video/recommend.html#获取单视频推荐列表)
175    ///
176    /// # 参数
177    /// | 名称   | 类型         | 说明                 |
178    /// | ------ | ------------| -------------------- |
179    /// | `aid`  | `Option<u64>` | 稿件 avid,可选      |
180    /// | `bvid` | `Option<&str>`| 稿件 bvid,可选      |
181    ///
182    /// `aid` 和 `bvid` 必须提供一个。
183    pub async fn video_related_videos(
184        &self,
185        aid: Option<u64>,
186        bvid: Option<&str>
187    ) -> Result<BpiResponse<Vec<RelatedVideo>>, BpiError> {
188        if aid.is_none() && bvid.is_none() {
189            return Err(BpiError::parse("必须提供 aid 或 bvid"));
190        }
191
192        let mut req = self.get("https://api.bilibili.com/x/web-interface/archive/related");
193
194        if let Some(a) = aid {
195            req = req.query(&[("aid", &a.to_string())]);
196        }
197        if let Some(b) = bvid {
198            req = req.query(&[("bvid", b)]);
199        }
200
201        req.send_bpi("获取单视频推荐列表").await
202    }
203
204    /// 获取首页视频推荐列表
205    ///
206    /// # 文档
207    /// [查看API文档](https://socialsisteryi.github.io/bilibili-API-collect/docs/video/recommend.html#获取首页视频推荐列表)
208    ///
209    /// # 参数
210    /// | 名称        | 类型         | 说明                 |
211    /// | ----------- | ------------| -------------------- |
212    /// | `ps`        | `Option<u8>`  | 单页返回的记录条数,最多30,可选 |
213    /// | `fresh_idx` | `Option<u32>` | 当前翻页号,可选,默认1 |
214    /// | `fetch_row` | `Option<u32>` | 本次抓取的最后一行行号,可选 |
215    pub async fn video_homepage_recommendations(
216        &self,
217        ps: Option<u8>,
218        fresh_idx: Option<u32>,
219        fetch_row: Option<u32>
220    ) -> Result<BpiResponse<RcmdFeedResponseData>, BpiError> {
221        let ps_val = ps.unwrap_or(12);
222        let fresh_idx_val = fresh_idx.unwrap_or(1);
223        let fetch_row_val = fetch_row.unwrap_or(1);
224        let params = vec![
225            ("fresh_type", "4".to_string()),
226            ("ps", ps_val.to_string()),
227            ("fresh_idx", fresh_idx_val.to_string()),
228            ("fresh_idx_1h", fresh_idx_val.to_string()),
229            ("brush", fresh_idx_val.to_string()),
230            ("fetch_row", fetch_row_val.to_string())
231        ];
232        let params = self.get_wbi_sign2(params).await?;
233
234        let req = self
235            .get("https://api.bilibili.com/x/web-interface/wbi/index/top/feed/rcmd")
236            .query(&params);
237
238        req.send_bpi("获取首页视频推荐列表").await
239    }
240}
241
242// --- 测试模块 ---
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247    use tracing::info;
248
249    const TEST_AID: u64 = 10001;
250
251    #[tokio::test]
252    async fn test_video_related_videos_by_aid() -> Result<(), BpiError> {
253        let bpi = BpiClient::new();
254        let resp = bpi.video_related_videos(Some(TEST_AID), None).await?;
255        let data = resp.into_data()?;
256
257        info!("单视频推荐列表: {:?}", data);
258
259        assert!(!data.is_empty());
260        assert!(data.len() <= 40);
261
262        Ok(())
263    }
264
265    #[tokio::test]
266    async fn test_video_homepage_recommendations() -> Result<(), BpiError> {
267        let bpi = BpiClient::new();
268        let resp = bpi.video_homepage_recommendations(Some(12), Some(1), Some(1)).await?;
269        let data = resp.into_data()?;
270
271        info!("首页推荐列表: {:?}", data);
272
273        assert!(!data.item.is_empty());
274        assert!(data.item.len() <= 30);
275
276        Ok(())
277    }
278}