1use crate::{BilibiliRequest, BpiClient, BpiError, BpiResponse};
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Deserialize, Serialize)]
11pub struct Owner {
12 pub mid: u64,
14 pub name: String,
16 pub face: String,
18}
19
20#[derive(Debug, Clone, Deserialize, Serialize)]
22pub struct Stat {
23 pub view: u64,
25 pub aid: u64,
27 pub danmaku: u64,
29 pub reply: u64,
31 pub favorite: u64,
33 pub coin: u64,
35 pub share: u64,
37 pub now_rank: u64,
39 pub his_rank: u64,
41 pub like: u64,
43 pub dislike: u64,
45}
46
47#[derive(Debug, Clone, Deserialize, Serialize)]
49pub struct HomeRmdStat {
50 pub view: u64,
52 pub danmaku: u64,
54 pub like: u64,
56}
57
58#[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#[derive(Debug, Clone, Deserialize, Serialize)]
77pub struct Dimension {
78 pub width: u32,
79 pub height: u32,
80 pub rotate: u8,
81}
82
83#[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#[derive(Debug, Clone, Deserialize, Serialize)]
111pub struct RcmdReason {
112 #[serde(rename = "reason_type")]
114 pub reason_type: u8,
115 pub content: Option<String>,
117}
118
119#[derive(Debug, Clone, Deserialize, Serialize)]
121pub struct RcmdItem {
122 pub av_feature: Option<serde_json::Value>,
123 pub business_info: Option<serde_json::Value>,
125 pub bvid: String,
127 pub cid: u64,
129 pub duration: u64,
131 pub goto: String,
133 pub id: u64,
135 pub is_followed: u8,
137 pub is_stock: u8,
138 pub owner: Owner,
140 pub pic: String,
142 pub pos: u8,
143 pub pubdate: u64,
145 pub rcmd_reason: Option<RcmdReason>,
147 pub room_info: Option<serde_json::Value>,
149 pub show_info: u8,
150 pub stat: Option<HomeRmdStat>,
152 pub title: String,
154 pub track_id: String,
155 pub uri: String,
157}
158
159#[derive(Debug, Clone, Deserialize, Serialize)]
161pub struct RcmdFeedResponseData {
162 pub item: Vec<RcmdItem>,
164 pub mid: u64,
166 pub preload_expose_pct: f32,
167 pub preload_floor_expose_pct: f32,
168}
169
170impl BpiClient {
171 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 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(¶ms);
235
236 req.send_bpi("获取首页视频推荐列表").await
237 }
238}
239
240#[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}