1use crate::{BilibiliRequest, BpiClient, BpiError, BpiResponse};
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Deserialize, Serialize)]
11pub struct DashInfo {
12 pub video: Vec<DashStream>,
13 pub audio: Vec<DashStream>,
14 #[serde(rename = "dolby")]
15 pub dolby: Option<DashDolby>,
16 pub flac: Option<DashFlac>,
17 pub duration: u64,
18}
19
20#[derive(Debug, Clone, Deserialize, Serialize)]
22pub struct DashDolby {
23 pub r#type: u8,
24 pub audio: Option<Vec<DashStream>>,
25}
26
27#[derive(Debug, Clone, Deserialize, Serialize)]
29pub struct DashFlac {
30 pub audio: Vec<DashStream>,
31}
32
33#[derive(Debug, Clone, Deserialize, Serialize)]
35pub struct DashStream {
36 pub id: u64,
37 #[serde(rename = "baseUrl")]
38 pub base_url: String,
39
40 #[serde(rename = "backupUrl")]
41 pub backup_url: Vec<String>,
42 pub bandwidth: u64,
43 #[serde(rename = "mimeType")]
44 pub mime_type: String,
45 pub codecs: String,
46 pub width: Option<u32>,
47 pub height: Option<u32>,
48 #[serde(rename = "frameRate")]
49 pub frame_rate: Option<String>,
50 pub sar: Option<String>,
51 pub start_with_sap: Option<u8>,
52 pub segment_base: Option<serde_json::Value>,
53 pub md5: Option<String>,
54 pub size: Option<u64>,
55 pub db_type: Option<u8>,
56 pub r#type: Option<String>,
57 pub stream_name: Option<String>,
58 pub orientation: Option<u8>,
59}
60
61#[derive(Debug, Clone, Deserialize, Serialize)]
63pub struct DurlInfo {
64 pub order: u32,
65 pub length: u64,
66 pub size: u64,
67 pub ahead: String,
68 pub vhead: String,
69 pub url: String,
70 pub backup_url: Vec<String>,
71}
72
73#[derive(Debug, Clone, Deserialize, Serialize)]
75pub struct SupportFormat {
76 pub quality: u64,
77 pub format: String,
78 pub new_description: String,
79 pub display_desc: String,
80 pub superscript: String,
81 pub codecs: Option<Vec<String>>,
82}
83
84#[derive(Debug, Clone, Deserialize, Serialize)]
86pub struct PlayUrlResponseData {
87 pub from: String,
88 pub result: String,
89 pub message: String,
90 pub quality: u64,
91 pub format: String,
92 pub timelength: u64,
93 pub accept_format: String,
94 pub accept_description: Vec<String>,
95 pub accept_quality: Vec<u64>,
96 pub video_codecid: u8,
97 pub seek_param: String,
98 pub seek_type: String,
99 pub durl: Option<Vec<DurlInfo>>,
100 pub dash: Option<DashInfo>,
101 pub support_formats: Vec<SupportFormat>,
102 pub high_format: Option<serde_json::Value>,
103 pub last_play_time: u64,
104 pub last_play_cid: u64,
105}
106
107impl BpiClient {
110 pub async fn video_playurl(
130 &self,
131 aid: Option<u64>,
132 bvid: Option<&str>,
133 cid: u64,
134 qn: Option<u64>,
135 fnval: Option<u64>,
136 fnver: Option<u64>,
137 fourk: Option<u8>,
138 platform: Option<&str>,
139 high_quality: Option<u8>,
140 try_look: Option<u8>,
141 ) -> Result<BpiResponse<PlayUrlResponseData>, BpiError> {
142 if aid.is_none() && bvid.is_none() {
143 return Err(BpiError::parse("必须提供 aid 或 bvid"));
144 }
145
146 let mut params = vec![("cid", cid.to_string())];
147
148 if let Some(a) = aid {
149 params.push(("avid", a.to_string()));
150 }
151 if let Some(b) = bvid {
152 params.push(("bvid", b.to_string()));
153 }
154 if let Some(q) = qn {
155 params.push(("qn", q.to_string()));
156 }
157 if let Some(f) = fnval {
158 params.push(("fnval", f.to_string()));
159 }
160 if let Some(f) = fnver {
161 params.push(("fnver", f.to_string()));
162 }
163 if let Some(f) = fourk {
164 params.push(("fourk", f.to_string()));
165 }
166 if let Some(p) = platform {
167 params.push(("platform", p.to_string()));
168 } else {
169 params.push(("platform", "pc".to_string()));
170 }
171 if let Some(h) = high_quality {
172 params.push(("high_quality", h.to_string()));
173 }
174 if let Some(t) = try_look {
175 params.push(("try_look", t.to_string()));
176 }
177
178 let params = self.get_wbi_sign2(params).await?;
180
181 self.get("https://api.bilibili.com/x/player/wbi/playurl")
182 .with_bilibili_headers()
183 .query(¶ms)
184 .send_bpi("获取视频流地址")
185 .await
186 }
187}
188
189#[cfg(test)]
192mod tests {
193 use super::*;
194 use tracing::info;
195
196 const TEST_AID: u64 = 113898824998659;
197 const TEST_CID: u64 = 28104724389;
198
199 #[tokio::test]
200
201 async fn test_video_playurl_mp4_by_aid() -> Result<(), BpiError> {
202 let bpi = BpiClient::new();
203 let resp = bpi
205 .video_playurl(
206 Some(TEST_AID),
207 None,
208 TEST_CID,
209 Some(64),
210 Some(1),
211 None,
212 None,
213 None,
214 None,
215 None,
216 )
217 .await?;
218 let data = resp.into_data()?;
219
220 info!("MP4 视频流信息: {:?}", data);
221 assert!(!data.durl.is_none());
222 assert_eq!(data.quality, 64);
223
224 Ok(())
225 }
226
227 #[tokio::test]
228
229 async fn test_video_playurl_4k() -> Result<(), BpiError> {
230 let bpi = BpiClient::new();
231 let resp = bpi
233 .video_playurl(
234 Some(TEST_AID),
235 None,
236 TEST_CID,
237 Some(120),
238 Some(16 | 128),
239 Some(0),
240 Some(1),
241 None,
242 None,
243 None,
244 )
245 .await?;
246 let data = resp.into_data()?;
247
248 info!("4K 视频流信息: {:?}", data);
249 assert!(!data.dash.is_none());
250 assert_eq!(data.quality, 120);
251
252 Ok(())
253 }
254}