bpi_rs/bangumi/
videostream_url.rs1use crate::models::{ Fnval, VideoQuality };
5use crate::{ BilibiliRequest, BpiClient, BpiError, BpiResponse };
6use serde::{ Deserialize, Serialize };
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct BangumiVideoStreamData {
11 #[serde(flatten)]
12 pub base: crate::models::VideoStreamData,
13
14 pub code: u32,
16 pub fnver: u32,
18 pub video_project: bool,
20 pub r#type: String,
22 pub bp: u32,
24 pub vip_type: Option<u32>,
26 pub vip_status: Option<u32>,
28 pub is_drm: bool,
30 pub no_rexcode: u32,
32 pub record_info: Option<BangumiRecordInfo>,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct BangumiRecordInfo {
38 pub record_icon: String,
39 pub record: String,
40}
41
42impl BpiClient {
43 pub async fn bangumi_video_stream(
56 &self,
57 ep_id: Option<u64>,
58 cid: Option<u64>,
59 qn: Option<VideoQuality>,
60 fnval: Option<Fnval>
61 ) -> Result<BpiResponse<BangumiVideoStreamData>, BpiError> {
62 if ep_id.is_none() && cid.is_none() {
64 return Err(BpiError::InvalidParameter {
65 field: "ep_id/cid",
66 message: "ep_id和cid必须提供其中一个",
67 });
68 }
69
70 let mut params = vec![("fnver", "0".to_string())];
71
72 if fnval.is_some_and(|f| f.is_fourk()) {
73 params.push(("fourk", "1".to_string()));
74 }
75
76 if let Some(ep) = ep_id {
77 params.push(("ep_id", ep.to_string()));
78 }
79 if let Some(c) = cid {
80 params.push(("cid", c.to_string()));
81 }
82 if let Some(q) = qn {
83 params.push(("qn", q.as_u32().to_string()));
84 }
85 if let Some(fv) = fnval {
86 params.push(("fnval", fv.bits().to_string()));
87 }
88
89 self
90 .get("https://api.bilibili.com/pgc/player/web/playurl")
91 .with_bilibili_headers()
92 .query(¶ms)
93 .send_bpi("获取番剧视频流URL").await
94 }
95
96 pub async fn bangumi_video_stream_by_epid(
108 &self,
109 ep_id: u64,
110 qn: Option<VideoQuality>,
111 fnval: Option<Fnval>
112 ) -> Result<BpiResponse<BangumiVideoStreamData>, BpiError> {
113 self.bangumi_video_stream(Some(ep_id), None, qn, fnval).await
114 }
115
116 pub async fn bangumi_video_stream_by_cid(
128 &self,
129 cid: u64,
130 qn: Option<VideoQuality>,
131 fnval: Option<Fnval>
132 ) -> Result<BpiResponse<BangumiVideoStreamData>, BpiError> {
133 self.bangumi_video_stream(None, Some(cid), qn, fnval).await
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 const TEST_EP_ID: u64 = 10001; const TEST_CID: u64 = 772096113;
143
144 #[tokio::test]
145 async fn test_bangumi_video_stream_url_simple() -> Result<(), Box<BpiError>> {
146 let bpi = BpiClient::new();
147 let result = bpi.bangumi_video_stream_by_epid(
148 TEST_EP_ID,
149 Some(VideoQuality::P8K),
150 Some(
151 Fnval::DASH |
152 Fnval::FOURK |
153 Fnval::EIGHTK |
154 Fnval::HDR |
155 Fnval::DOLBY_AUDIO |
156 Fnval::DOLBY_VISION |
157 Fnval::AV1
158 )
159 ).await?;
160
161 let data = result.into_data()?;
162 tracing::info!("==========最佳格式==========\n{:#?}", data.base.best_format());
163 tracing::info!("==========最佳视频==========\n{:#?}", data.base.best_video());
164
165 assert!(data.base.timelength.unwrap() > 0);
166 assert!(!data.base.accept_format.is_empty());
167 assert!(!data.base.accept_quality.is_empty());
168
169 Ok(())
170 }
171
172 #[tokio::test]
173 async fn test_bangumi_video_stream_url_by_cid() -> Result<(), Box<BpiError>> {
174 let bpi = BpiClient::new();
175 let result = bpi.bangumi_video_stream_by_cid(
176 TEST_CID,
177 Some(VideoQuality::P480),
178 Some(Fnval::DASH)
179 ).await?;
180 let data = result.into_data()?;
181 tracing::info!("{:#?}", data);
182 Ok(())
183 }
184
185 #[tokio::test]
186 async fn test_bangumi_video_stream_url_no_params() {
187 let bpi = BpiClient::new();
188 let result = bpi.bangumi_video_stream(None, None, None, None).await;
189 assert!(result.is_err());
190 let error = result.unwrap_err();
191 match error {
192 BpiError::InvalidParameter { field, message } => {
193 assert_eq!(field, "ep_id/cid");
194 assert_eq!(message, "ep_id和cid必须提供其中一个");
195 }
196 _ => panic!("Expected InvalidParameter error"),
197 }
198 }
199}