Skip to main content

ybd_core/model/
video.rs

1//! bilibili视频详细信息模型
2
3use serde::Deserialize;
4use serde::Serialize;
5use serde_json::Value;
6
7use crate::client::BiliClient;
8use crate::error::Error;
9use crate::error::Result;
10use crate::model::param::VideoRequestParamBuilder;
11use crate::model::quality::AudioQuality;
12use crate::model::quality::VideoEncode;
13use crate::model::quality::VideoQuality;
14use crate::url::VIDEO_DOWNLOAD_URL;
15
16#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
17#[serde(rename_all = "camelCase")]
18pub struct PlayUrlResponse {
19        pub code: i64,
20        data: VideoData,
21        pub message: String,
22        pub ttl: i64,
23}
24
25impl PlayUrlResponse {
26        pub async fn new(
27                bili_client: &BiliClient,
28                bvid: &str,
29        ) -> Result<Self> {
30                // 这里构造的是视频请求参数,参数签名逻辑也封装在内
31                let vrp = VideoRequestParamBuilder::new(bvid, bili_client)
32                        .await?
33                        .build(bili_client)
34                        .await?;
35                let url = format!("{}?{}", VIDEO_DOWNLOAD_URL, vrp.to_query_string());
36                // TODO: 老视频不兼容PlayUrlResponse这个结构
37                Ok(bili_client.get(&url).send().await?.json().await?)
38        }
39
40        pub fn get_data(&self) -> Result<&VideoData> {
41                self.valid()?;
42                Ok(&self.data)
43        }
44
45        fn valid(&self) -> Result<()> {
46                match self.code {
47                        0 => Ok(()),
48                        -101 => Err(Error::Normal("账号未登录".into())),
49                        -403 => Err(Error::Normal("权限不足,可能需要大会员".into())),
50                        -404 => Err(Error::Normal("视频不存在".into())),
51                        -412 => Err(Error::Normal("请求被拦截,请稍后再试".into())),
52                        -10403 => Err(Error::Normal("地区限制,无法观看".into())),
53                        _ => Err(Error::Normal(format!("未知错误码: {}", self.code))),
54                }
55        }
56}
57
58#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
59#[serde(rename_all = "camelCase")]
60pub struct VideoData {
61        #[serde(rename = "accept_description")]
62        pub accept_description: Vec<String>,
63        #[serde(rename = "accept_format")]
64        pub accept_format: String,
65        #[serde(rename = "accept_quality")]
66        pub accept_quality: Vec<i64>,
67        #[serde(rename = "auto_qn_resp")]
68        pub auto_qn_resp: AutoQnResp,
69        #[serde(rename = "cur_language")]
70        pub cur_language: String,
71        #[serde(rename = "cur_production_type")]
72        pub cur_production_type: i64,
73        // 老视频的信息中无此字段,直接抛出错误
74        pub dash: Dash,
75        pub format: String,
76        pub from: String,
77        #[serde(rename = "high_format")]
78        pub high_format: Value,
79        #[serde(rename = "last_play_cid")]
80        pub last_play_cid: i64,
81        #[serde(rename = "last_play_time")]
82        pub last_play_time: i64,
83        pub message: String,
84        #[serde(rename = "play_conf")]
85        pub play_conf: PlayConf,
86        pub quality: i64,
87        pub result: String,
88        #[serde(rename = "seek_param")]
89        pub seek_param: String,
90        #[serde(rename = "seek_type")]
91        pub seek_type: String,
92        #[serde(rename = "support_formats")]
93        pub support_formats: Vec<SupportFormat>,
94        pub timelength: i64,
95        #[serde(rename = "video_codecid")]
96        pub video_codecid: i64,
97        #[serde(rename = "view_info")]
98        pub view_info: Value,
99}
100
101impl VideoData {
102        pub fn best_video_quality_url(&self) -> Option<&str> {
103                self.dash
104                        .video
105                        .iter()
106                        .filter(|v| self.accept_quality.contains(&v.id))
107                        .max_by_key(|v| v.id)
108                        .map(|v| v.base_url.as_str())
109        }
110
111        pub fn get_specified_video_url(
112                &self,
113                video_quality: Option<VideoQuality>,
114                video_encode: Option<VideoEncode>,
115        ) -> Option<&str> {
116                if video_quality.is_none() && video_encode.is_none() {
117                        return self.best_video_quality_url();
118                }
119
120                // 1. 尝试完全匹配指定画质 + 指定编码
121                if let Some(url) = self
122                        .dash
123                        .video
124                        .iter()
125                        .filter(|v| self.accept_quality.contains(&v.id))
126                        .filter(|v| video_quality.is_none_or(|vq| v.id == vq as i64))
127                        .find(|v| video_encode.is_none_or(|ve| v.codecs.starts_with(ve.as_str())))
128                        .map(|v| v.base_url.as_str())
129                {
130                        return Some(url);
131                }
132
133                // 2. 容错降级:如果找不到指定编码,尝试匹配指定画质的任意可用编码
134                if let Some(vq) = video_quality {
135                        if let Some(url) = self
136                                .dash
137                                .video
138                                .iter()
139                                .filter(|v| self.accept_quality.contains(&v.id))
140                                .filter(|v| v.id == vq as i64)
141                                .map(|v| v.base_url.as_str())
142                                .next()
143                        {
144                                return Some(url);
145                        }
146
147                        // 3. 容错降级:如果视频分辨率低于指定画质,寻找小于等于指定画质的最高画质
148                        if let Some(url) = self
149                                .dash
150                                .video
151                                .iter()
152                                .filter(|v| self.accept_quality.contains(&v.id))
153                                .filter(|v| v.id <= vq as i64)
154                                .max_by_key(|v| v.id)
155                                .map(|v| v.base_url.as_str())
156                        {
157                                return Some(url);
158                        }
159                }
160
161                // 4. 最终容错兜底:自动选取当前可用的最高画质视频流
162                self.best_video_quality_url()
163        }
164
165        pub fn best_audio_url(&self) -> Option<&str> {
166                if let Some(flac) = &self.dash.flac {
167                        return Some(&flac.audio.base_url);
168                }
169                self.dash
170                        .audio
171                        .iter()
172                        .max_by_key(|a| a.id)
173                        .map(|v| v.base_url.as_str())
174        }
175
176        pub fn get_specified_audio_url(
177                &self,
178                audio_quality: AudioQuality,
179        ) -> Option<&str> {
180                let direct_match = match audio_quality {
181                        AudioQuality::HiRes => self
182                                .dash
183                                .flac
184                                .as_ref()
185                                .map(|flac| flac.audio.base_url.as_str()),
186                        AudioQuality::Dolby => self
187                                .dash
188                                .dolby
189                                .as_ref()
190                                .and_then(|db| db.audio.as_ref())
191                                .and_then(|audio_vec| audio_vec.first())
192                                .map(|a| a.base_url.as_str()),
193                        _ => self
194                                .dash
195                                .audio
196                                .iter()
197                                .find(|a| a.id == audio_quality as i64)
198                                .map(|a| a.base_url.as_str()),
199                };
200
201                // 容错降级:如果指定音质(如 Hi-Res / 杜比)不存在,自动回退到最佳可用音质
202                direct_match.or_else(|| self.best_audio_url())
203        }
204}
205
206#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
207#[serde(rename_all = "camelCase")]
208pub struct AutoQnResp {
209        pub dyeid: String,
210}
211
212#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
213#[serde(rename_all = "camelCase")]
214pub struct Dash {
215        pub audio: Vec<Audio>,
216        pub dolby: Option<Dolby>,
217        pub duration: i64,
218        pub flac: Option<Flac>,
219        pub min_buffer_time: f64,
220        #[serde(rename = "min_buffer_time")]
221        pub min_buffer_time2: f64,
222        pub video: Vec<Video>,
223}
224
225#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
226#[serde(rename_all = "camelCase")]
227pub struct Audio {
228        #[serde(rename = "SegmentBase")]
229        pub segment_base: SegmentBase,
230        pub backup_url: Vec<String>,
231        #[serde(rename = "backup_url")]
232        pub backup_url2: Vec<String>,
233        pub bandwidth: i64,
234        pub base_url: String,
235        #[serde(rename = "base_url")]
236        pub base_url2: String,
237        pub codecid: i64,
238        pub codecs: String,
239        pub frame_rate: String,
240        #[serde(rename = "frame_rate")]
241        pub frame_rate2: String,
242        pub height: i64,
243        pub id: i64,
244        pub mime_type: String,
245        #[serde(rename = "mime_type")]
246        pub mime_type2: String,
247        pub sar: String,
248        #[serde(rename = "segment_base")]
249        pub segment_base2: SegmentBase2,
250        pub start_with_sap: i64,
251        #[serde(rename = "start_with_sap")]
252        pub start_with_sap2: i64,
253        pub width: i64,
254}
255
256#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
257#[serde(rename_all = "camelCase")]
258pub struct SegmentBase {
259        #[serde(rename = "Initialization")]
260        pub initialization: String,
261        pub index_range: String,
262}
263
264#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
265#[serde(rename_all = "camelCase")]
266pub struct SegmentBase2 {
267        #[serde(rename = "index_range")]
268        pub index_range: String,
269        pub initialization: String,
270}
271
272#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
273#[serde(rename_all = "camelCase")]
274pub struct Dolby {
275        pub audio: Option<Vec<Audio2>>,
276        #[serde(rename = "type")]
277        pub type_field: i64,
278}
279#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
280#[serde(rename_all = "camelCase")]
281pub struct Audio2 {
282        #[serde(rename = "SegmentBase")]
283        pub segment_base: SegmentBase3,
284        pub backup_url: Vec<String>,
285        #[serde(rename = "backup_url")]
286        pub backup_url2: Vec<String>,
287        pub bandwidth: i64,
288        pub base_url: String,
289        #[serde(rename = "base_url")]
290        pub base_url2: String,
291        pub codecid: i64,
292        pub codecs: String,
293        pub frame_rate: String,
294        #[serde(rename = "frame_rate")]
295        pub frame_rate2: String,
296        pub height: i64,
297        pub id: i64,
298        pub mime_type: String,
299        #[serde(rename = "mime_type")]
300        pub mime_type2: String,
301        pub sar: String,
302        #[serde(rename = "segment_base")]
303        pub segment_base2: SegmentBase4,
304        pub start_with_sap: i64,
305        #[serde(rename = "start_with_sap")]
306        pub start_with_sap2: i64,
307        pub width: i64,
308}
309
310#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
311#[serde(rename_all = "camelCase")]
312pub struct Video {
313        #[serde(rename = "SegmentBase")]
314        pub segment_base: SegmentBase3,
315        pub backup_url: Vec<String>,
316        #[serde(rename = "backup_url")]
317        pub backup_url2: Vec<String>,
318        pub bandwidth: i64,
319        pub base_url: String,
320        #[serde(rename = "base_url")]
321        pub base_url2: String,
322        pub codecid: i64,
323        pub codecs: String,
324        pub frame_rate: String,
325        #[serde(rename = "frame_rate")]
326        pub frame_rate2: String,
327        pub height: i64,
328        pub id: i64,
329        pub mime_type: String,
330        #[serde(rename = "mime_type")]
331        pub mime_type2: String,
332        pub sar: String,
333        #[serde(rename = "segment_base")]
334        pub segment_base2: SegmentBase4,
335        pub start_with_sap: i64,
336        #[serde(rename = "start_with_sap")]
337        pub start_with_sap2: i64,
338        pub width: i64,
339}
340
341#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
342#[serde(rename_all = "camelCase")]
343pub struct SegmentBase3 {
344        #[serde(rename = "Initialization")]
345        pub initialization: String,
346        pub index_range: String,
347}
348
349#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
350#[serde(rename_all = "camelCase")]
351pub struct SegmentBase4 {
352        #[serde(rename = "index_range")]
353        pub index_range: String,
354        pub initialization: String,
355}
356
357#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
358#[serde(rename_all = "camelCase")]
359pub struct Flac {
360        pub audio: Audio3,
361        pub display: bool,
362}
363
364#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
365#[serde(rename_all = "camelCase")]
366pub struct Audio3 {
367        #[serde(rename = "SegmentBase")]
368        pub segment_base: SegmentBase5,
369        pub backup_url: Vec<String>,
370        #[serde(rename = "backup_url")]
371        pub backup_url2: Vec<String>,
372        pub bandwidth: i64,
373        pub base_url: String,
374        #[serde(rename = "base_url")]
375        pub base_url2: String,
376        pub codecid: i64,
377        pub codecs: String,
378        pub frame_rate: String,
379        #[serde(rename = "frame_rate")]
380        pub frame_rate2: String,
381        pub height: i64,
382        pub id: i64,
383        pub mime_type: String,
384        #[serde(rename = "mime_type")]
385        pub mime_type2: String,
386        pub sar: String,
387        #[serde(rename = "segment_base")]
388        pub segment_base2: SegmentBase6,
389        pub start_with_sap: i64,
390        #[serde(rename = "start_with_sap")]
391        pub start_with_sap2: i64,
392        pub width: i64,
393}
394
395#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
396#[serde(rename_all = "camelCase")]
397pub struct SegmentBase5 {
398        #[serde(rename = "Initialization")]
399        pub initialization: String,
400        pub index_range: String,
401}
402
403#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
404#[serde(rename_all = "camelCase")]
405pub struct SegmentBase6 {
406        #[serde(rename = "index_range")]
407        pub index_range: String,
408        pub initialization: String,
409}
410
411#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
412#[serde(rename_all = "camelCase")]
413pub struct PlayConf {
414        #[serde(rename = "is_new_description")]
415        pub is_new_description: bool,
416}
417
418#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
419#[serde(rename_all = "camelCase")]
420pub struct SupportFormat {
421        #[serde(rename = "can_watch_qn_reason")]
422        pub can_watch_qn_reason: i64,
423        pub codecs: Vec<String>,
424        #[serde(rename = "display_desc")]
425        pub display_desc: String,
426        pub format: String,
427        #[serde(rename = "limit_watch_reason")]
428        pub limit_watch_reason: i64,
429        #[serde(rename = "new_description")]
430        pub new_description: String,
431        pub quality: i64,
432        pub report: Report,
433        pub superscript: String,
434}
435
436#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
437#[serde(rename_all = "camelCase")]
438pub struct Report {}