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(
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 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(¶ms);
237
238 req.send_bpi("获取首页视频推荐列表").await
239 }
240}
241
242#[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}