use crate::{ BilibiliRequest, BpiClient, BpiError, BpiResponse };
use serde::{ Deserialize, Serialize };
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ArchiveStat {
pub view: u64,
pub vt: Option<u64>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Archive {
pub aid: u64,
pub bvid: String,
pub ctime: u64,
pub duration: u64,
pub interactive_video: bool,
pub pic: String,
pub playback_position: u64,
pub pubdate: u64,
pub stat: ArchiveStat,
pub state: u64,
pub title: String,
pub ugc_pay: u64,
pub vt_display: String,
pub is_lesson_video: Option<u32>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct PageInfo {
#[serde(alias = "num")]
pub page_num: u64,
#[serde(alias = "size")]
pub page_size: u64,
pub total: u64,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SeasonsArchivesMeta {
pub category: u64,
pub cover: String,
pub description: String,
pub mid: u64,
pub name: String,
pub ptime: u64,
pub season_id: u64,
pub total: u64,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct GetSeasonsArchivesData {
pub aids: Vec<u64>,
pub archives: Vec<Archive>,
pub meta: SeasonsArchivesMeta,
pub page: PageInfo,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SeasonsMeta {
pub category: u64,
pub cover: String,
pub description: String,
pub mid: u64,
pub name: String,
pub ptime: u64,
pub season_id: u64,
pub total: u64,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SeriesMeta {
pub category: u64,
pub creator: String,
pub ctime: u64,
pub description: String,
pub keywords: Vec<String>,
pub last_update_ts: u64,
pub mid: u64,
pub mtime: u64,
pub name: String,
pub raw_keywords: String,
pub series_id: u64,
pub state: u64,
pub total: u64,
pub cover: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SeasonsItem {
pub archives: Vec<Archive>,
pub meta: SeasonsMeta,
pub recent_aids: Vec<u64>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SeriesItem {
pub archives: Vec<Archive>,
pub meta: SeriesMeta,
pub recent_aids: Vec<u64>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ItemsList {
pub page: PageInfo,
pub seasons_list: Vec<SeasonsItem>,
pub series_list: Vec<SeriesItem>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct GetSeasonsSeriesData {
pub items_lists: ItemsList,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct GetSeriesData {
pub meta: SeriesMeta,
pub recent_aids: Vec<u64>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct GetSeriesArchivesData {
pub aids: Vec<u64>,
pub page: PageInfo,
pub archives: Vec<Archive>,
}
impl BpiClient {
pub async fn video_seasons_list(
&self,
mid: u64,
season_id: u64,
sort_reverse: Option<bool>,
page_num: Option<u64>,
page_size: Option<u64>
) -> Result<BpiResponse<GetSeasonsArchivesData>, BpiError> {
let mut params = vec![
("mid", mid.to_string()),
("season_id", season_id.to_string()),
("page_num", "1".to_string()),
("page_size", "20".to_string())
];
if let Some(sort) = sort_reverse {
params.push(("sort_reverse", sort.to_string()));
}
if let Some(num) = page_num {
params.push(("page_num", num.to_string()));
}
if let Some(size) = page_size {
params.push(("page_size", size.to_string()));
}
let params = self.get_wbi_sign2(params).await?;
self
.get("https://api.bilibili.com/x/polymer/web-space/seasons_archives_list")
.with_bilibili_headers()
.query(¶ms)
.send_bpi("获取视频合集信息").await
}
pub async fn video_series_list(
&self,
mid: u64,
page_num: u64,
page_size: u64
) -> Result<BpiResponse<GetSeasonsSeriesData>, BpiError> {
let params = vec![
("mid", mid.to_string()),
("page_num", page_num.to_string()),
("page_size", page_size.to_string())
];
let params = self.get_wbi_sign2(params).await?;
self
.get("https://api.bilibili.com/x/polymer/web-space/home/seasons_series")
.query(¶ms)
.send_bpi("只获取系列视频列表").await
}
pub async fn video_seasons_series_list(
&self,
mid: u64,
page_num: Option<u64>,
page_size: Option<u64>
) -> Result<BpiResponse<GetSeasonsSeriesData>, BpiError> {
let mut params = vec![("mid", mid.to_string())];
if let Some(num) = page_num {
params.push(("page_num", num.to_string()));
}
if let Some(size) = page_size {
params.push(("page_size", size.to_string()));
}
let params = self.get_wbi_sign2(params).await?;
self
.get("https://api.bilibili.com/x/polymer/web-space/seasons_series_list")
.query(¶ms)
.send_bpi("获取系列和合集视频列表").await
}
pub async fn video_series_info(
&self,
series_id: u64
) -> Result<BpiResponse<GetSeriesData>, BpiError> {
let req = self
.get("https://api.bilibili.com/x/series/series")
.query(&[("series_id", &series_id.to_string())]);
req.send_bpi("查询指定系列信息").await
}
pub async fn video_series_archives(
&self,
mid: u64,
series_id: u64,
only_normal: Option<bool>,
sort: Option<&str>,
page_num: Option<u64>,
page_size: Option<u64>
) -> Result<BpiResponse<GetSeriesArchivesData>, BpiError> {
let mut req = self.get("https://api.bilibili.com/x/series/archives").query(
&[
("mid", &mid.to_string()),
("series_id", &series_id.to_string()),
]
);
if let Some(normal) = only_normal {
req = req.query(&[("only_normal", &normal.to_string())]);
}
if let Some(s) = sort {
req = req.query(&[("sort", s)]);
}
if let Some(num) = page_num {
req = req.query(&[("pn", &num.to_string())]);
}
if let Some(size) = page_size {
req = req.query(&[("ps", &size.to_string())]);
}
req.send_bpi("获取指定系列视频列表").await
}
}
#[cfg(test)]
mod tests {
use super::*;
use tracing::info;
const TEST_MID: u64 = 4279370;
const TEST_SEASON_ID: u64 = 4294056;
const TEST_SERIES_ID: u64 = 250285;
#[tokio::test]
async fn test_video_seasons_archives_list() -> Result<(), BpiError> {
let bpi = BpiClient::new();
let resp = bpi.video_seasons_list(TEST_MID, TEST_SEASON_ID, Some(false), None, None).await?;
let data = resp.into_data()?;
info!("测试结果: {:?}", data);
assert!(!data.archives.is_empty(), "返回的合集视频列表不应为空");
assert_eq!(data.meta.season_id, TEST_SEASON_ID, "合集ID应与请求ID一致");
Ok(())
}
#[tokio::test]
async fn test_video_seasons_series_only() -> Result<(), BpiError> {
let bpi = BpiClient::new();
let resp = bpi.video_series_list(TEST_MID, 1, 10).await?;
let data = resp.into_data()?;
info!("测试结果: {:?}", data);
Ok(())
}
#[tokio::test]
async fn test_video_seasons_series_list() -> Result<(), BpiError> {
let bpi = BpiClient::new();
let resp = bpi.video_seasons_series_list(TEST_MID, Some(1), Some(5)).await?;
let data = resp.into_data()?;
info!("测试结果: {:?}", data);
assert!(!data.items_lists.series_list.is_empty(), "返回的系列列表不应为空");
assert_eq!(data.items_lists.page.page_size, 5, "返回的每页数量应为5");
Ok(())
}
#[tokio::test]
async fn test_video_series_info() -> Result<(), BpiError> {
let bpi = BpiClient::new();
let resp = bpi.video_series_info(TEST_SERIES_ID).await?;
let data = resp.into_data()?;
info!("测试结果: {:?}", data);
assert_eq!(data.meta.series_id, TEST_SERIES_ID, "返回的系列ID应与请求ID一致");
assert!(!data.recent_aids.is_empty(), "最近的aid列表不应为空");
Ok(())
}
#[tokio::test]
async fn test_video_series_archives() -> Result<(), BpiError> {
let bpi = BpiClient::new();
let resp = bpi.video_series_archives(
TEST_MID,
TEST_SERIES_ID,
None,
Some("asc"),
Some(1),
Some(10)
).await?;
let data = resp.into_data()?;
info!("测试结果: {:?}", data);
assert!(!data.archives.is_empty(), "返回的系列视频列表不应为空");
Ok(())
}
}