use crate::{ BilibiliRequest, BpiClient, BpiError, BpiResponse };
use serde::{ Deserialize, Serialize };
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Owner {
pub mid: u64,
pub name: String,
pub face: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Stat {
pub view: u64,
pub aid: u64,
pub danmaku: u64,
pub reply: u64,
pub favorite: u64,
pub coin: u64,
pub share: u64,
pub now_rank: u64,
pub his_rank: u64,
pub like: u64,
pub dislike: u64,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct HomeRmdStat {
pub view: u64,
pub danmaku: u64,
pub like: u64,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Rights {
pub bp: u8,
pub elec: u8,
pub download: u8,
pub movie: u8,
pub pay: u8,
pub hd5: u8,
pub no_reprint: u8,
pub autoplay: u8,
pub ugc_pay: u8,
pub is_cooperation: u8,
pub ugc_pay_preview: u8,
pub no_background: u8,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Dimension {
pub width: u32,
pub height: u32,
pub rotate: u8,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RelatedVideo {
pub aid: u64,
pub videos: u32,
pub tid: u32,
pub tname: String,
pub copyright: u8,
pub pic: String,
pub title: String,
pub pubdate: u64,
pub ctime: u64,
pub desc: String,
pub state: i8,
pub duration: u64,
pub rights: Rights,
pub owner: Owner,
pub stat: Stat,
pub dynamic: String,
pub cid: u64,
pub dimension: Dimension,
pub bvid: String,
#[serde(default)]
pub short_link_v2: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RcmdReason {
#[serde(rename = "reason_type")]
pub reason_type: u8,
pub content: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RcmdItem {
pub av_feature: Option<serde_json::Value>,
pub business_info: Option<serde_json::Value>,
pub bvid: String,
pub cid: u64,
pub duration: u64,
pub goto: String,
pub id: u64,
pub is_followed: u8,
pub is_stock: u8,
pub owner: Owner,
pub pic: String,
pub pos: u8,
pub pubdate: u64,
pub rcmd_reason: Option<RcmdReason>,
pub room_info: Option<serde_json::Value>,
pub show_info: u8,
pub stat: Option<HomeRmdStat>,
pub title: String,
pub track_id: String,
pub uri: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RcmdFeedResponseData {
pub item: Vec<RcmdItem>,
pub mid: u64,
pub preload_expose_pct: f32,
pub preload_floor_expose_pct: f32,
}
impl BpiClient {
pub async fn video_related_videos(
&self,
aid: Option<u64>,
bvid: Option<&str>
) -> Result<BpiResponse<Vec<RelatedVideo>>, BpiError> {
if aid.is_none() && bvid.is_none() {
return Err(BpiError::parse("必须提供 aid 或 bvid"));
}
let mut req = self.get("https://api.bilibili.com/x/web-interface/archive/related");
if let Some(a) = aid {
req = req.query(&[("aid", &a.to_string())]);
}
if let Some(b) = bvid {
req = req.query(&[("bvid", b)]);
}
req.send_bpi("获取单视频推荐列表").await
}
pub async fn video_homepage_recommendations(
&self,
ps: Option<u8>,
fresh_idx: Option<u32>,
fetch_row: Option<u32>
) -> Result<BpiResponse<RcmdFeedResponseData>, BpiError> {
let ps_val = ps.unwrap_or(12);
let fresh_idx_val = fresh_idx.unwrap_or(1);
let fetch_row_val = fetch_row.unwrap_or(1);
let params = vec![
("fresh_type", "4".to_string()),
("ps", ps_val.to_string()),
("fresh_idx", fresh_idx_val.to_string()),
("fresh_idx_1h", fresh_idx_val.to_string()),
("brush", fresh_idx_val.to_string()),
("fetch_row", fetch_row_val.to_string())
];
let params = self.get_wbi_sign2(params).await?;
let req = self
.get("https://api.bilibili.com/x/web-interface/wbi/index/top/feed/rcmd")
.query(¶ms);
req.send_bpi("获取首页视频推荐列表").await
}
}
#[cfg(test)]
mod tests {
use super::*;
use tracing::info;
const TEST_AID: u64 = 10001;
#[tokio::test]
async fn test_video_related_videos_by_aid() -> Result<(), BpiError> {
let bpi = BpiClient::new();
let resp = bpi.video_related_videos(Some(TEST_AID), None).await?;
let data = resp.into_data()?;
info!("单视频推荐列表: {:?}", data);
assert!(!data.is_empty());
assert!(data.len() <= 40);
Ok(())
}
#[tokio::test]
async fn test_video_homepage_recommendations() -> Result<(), BpiError> {
let bpi = BpiClient::new();
let resp = bpi.video_homepage_recommendations(Some(12), Some(1), Some(1)).await?;
let data = resp.into_data()?;
info!("首页推荐列表: {:?}", data);
assert!(!data.item.is_empty());
assert!(data.item.len() <= 30);
Ok(())
}
}