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.get("https://api.bilibili.com/pgc/player/web/playurl")
90 .with_bilibili_headers()
91 .query(¶ms)
92 .send_bpi("获取番剧视频流URL")
93 .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)
114 .await
115 }
116
117 pub async fn bangumi_video_stream_by_cid(
129 &self,
130 cid: u64,
131 qn: Option<VideoQuality>,
132 fnval: Option<Fnval>,
133 ) -> Result<BpiResponse<BangumiVideoStreamData>, BpiError> {
134 self.bangumi_video_stream(None, Some(cid), qn, fnval).await
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 const TEST_EP_ID: u64 = 10001; const TEST_CID: u64 = 772096113;
144
145 #[tokio::test]
146 async fn test_bangumi_video_stream_url_simple() -> Result<(), Box<BpiError>> {
147 let bpi = BpiClient::new();
148 let result = bpi
149 .bangumi_video_stream_by_epid(
150 TEST_EP_ID,
151 Some(VideoQuality::P8K),
152 Some(
153 Fnval::DASH
154 | Fnval::FOURK
155 | Fnval::EIGHTK
156 | Fnval::HDR
157 | Fnval::DOLBY_AUDIO
158 | Fnval::DOLBY_VISION
159 | Fnval::AV1,
160 ),
161 )
162 .await?;
163
164 let data = result.into_data()?;
165 tracing::info!(
166 "==========最佳格式==========\n{:#?}",
167 data.base.best_format()
168 );
169 tracing::info!(
170 "==========最佳视频==========\n{:#?}",
171 data.base.best_video()
172 );
173
174 assert!(data.base.timelength.unwrap() > 0);
175 assert!(!data.base.accept_format.is_empty());
176 assert!(!data.base.accept_quality.is_empty());
177
178 Ok(())
179 }
180
181 #[tokio::test]
182 async fn test_bangumi_video_stream_url_by_cid() -> Result<(), Box<BpiError>> {
183 let bpi = BpiClient::new();
184 let result = bpi
185 .bangumi_video_stream_by_cid(TEST_CID, Some(VideoQuality::P480), Some(Fnval::DASH))
186 .await?;
187 let data = result.into_data()?;
188 tracing::info!("{:#?}", data);
189 Ok(())
190 }
191
192 #[tokio::test]
193 async fn test_bangumi_video_stream_url_no_params() {
194 let bpi = BpiClient::new();
195 let result = bpi.bangumi_video_stream(None, None, None, None).await;
196 assert!(result.is_err());
197 let error = result.unwrap_err();
198 match error {
199 BpiError::InvalidParameter { field, message } => {
200 assert_eq!(field, "ep_id/cid");
201 assert_eq!(message, "ep_id和cid必须提供其中一个");
202 }
203 _ => panic!("Expected InvalidParameter error"),
204 }
205 }
206}