bpi_rs/video/
recommend.rs

1//! 视频推荐相关接口
2//!
3//! 文档: 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    /// 文档: https://socialsisteryi.github.io/bilibili-API-collect/docs/video/recommend.html#获取单视频推荐列表
174    ///
175    /// # 参数
176    /// | 名称   | 类型         | 说明                 |
177    /// | ------ | ------------| -------------------- |
178    /// | `aid`  | Option<u64> | 稿件 avid,可选      |
179    /// | `bvid` | Option<&str>| 稿件 bvid,可选      |
180    ///
181    /// `aid` 和 `bvid` 必须提供一个。
182    pub async fn video_related_videos(
183        &self,
184        aid: Option<u64>,
185        bvid: Option<&str>,
186    ) -> Result<BpiResponse<Vec<RelatedVideo>>, BpiError> {
187        if aid.is_none() && bvid.is_none() {
188            return Err(BpiError::parse("必须提供 aid 或 bvid"));
189        }
190
191        let mut req = self.get("https://api.bilibili.com/x/web-interface/archive/related");
192
193        if let Some(a) = aid {
194            req = req.query(&[("aid", &a.to_string())]);
195        }
196        if let Some(b) = bvid {
197            req = req.query(&[("bvid", b)]);
198        }
199
200        req.send_bpi("获取单视频推荐列表").await
201    }
202
203    /// 获取首页视频推荐列表
204    ///
205    /// 文档: https://socialsisteryi.github.io/bilibili-API-collect/docs/video/recommend.html#获取首页视频推荐列表
206    ///
207    /// # 参数
208    /// | 名称        | 类型         | 说明                 |
209    /// | ----------- | ------------| -------------------- |
210    /// | `ps`        | Option<u8>  | 单页返回的记录条数,最多30,可选 |
211    /// | `fresh_idx` | Option<u32> | 当前翻页号,可选,默认1 |
212    /// | `fetch_row` | Option<u32> | 本次抓取的最后一行行号,可选 |
213    pub async fn video_homepage_recommendations(
214        &self,
215        ps: Option<u8>,
216        fresh_idx: Option<u32>,
217        fetch_row: Option<u32>,
218    ) -> Result<BpiResponse<RcmdFeedResponseData>, BpiError> {
219        let ps_val = ps.unwrap_or(12);
220        let fresh_idx_val = fresh_idx.unwrap_or(1);
221        let fetch_row_val = fetch_row.unwrap_or(1);
222        let params = vec![
223            ("fresh_type", "4".to_string()),
224            ("ps", ps_val.to_string()),
225            ("fresh_idx", fresh_idx_val.to_string()),
226            ("fresh_idx_1h", fresh_idx_val.to_string()),
227            ("brush", fresh_idx_val.to_string()),
228            ("fetch_row", fetch_row_val.to_string()),
229        ];
230        let params = self.get_wbi_sign2(params).await?;
231
232        let req = self
233            .get("https://api.bilibili.com/x/web-interface/wbi/index/top/feed/rcmd")
234            .query(&params);
235
236        req.send_bpi("获取首页视频推荐列表").await
237    }
238}
239
240// --- 测试模块 ---
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245    use tracing::info;
246
247    const TEST_AID: u64 = 10001;
248
249    #[tokio::test]
250    async fn test_video_related_videos_by_aid() -> Result<(), BpiError> {
251        let bpi = BpiClient::new();
252        let resp = bpi.video_related_videos(Some(TEST_AID), None).await?;
253        let data = resp.into_data()?;
254
255        info!("单视频推荐列表: {:?}", data);
256
257        assert!(!data.is_empty());
258        assert!(data.len() <= 40);
259
260        Ok(())
261    }
262
263    #[tokio::test]
264
265    async fn test_video_homepage_recommendations() -> Result<(), BpiError> {
266        let bpi = BpiClient::new();
267        let resp = bpi
268            .video_homepage_recommendations(Some(12), Some(1), Some(1))
269            .await?;
270        let data = resp.into_data()?;
271
272        info!("首页推荐列表: {:?}", data);
273
274        assert!(!data.item.is_empty());
275        assert!(data.item.len() <= 30);
276
277        Ok(())
278    }
279}