Skip to main content

bpi_rs/cheese/
videostream_url.rs

1//! 课程视频流 URL API
2//!
3//! [参考文档](https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/cheese/videostream_url.md)
4
5use std::collections::HashMap;
6
7use crate::models::{ DashStreams, Fnval, SupportFormat, VideoQuality };
8use crate::{ BilibiliRequest, BpiClient, BpiError, BpiResponse };
9use serde::{ Deserialize, Serialize };
10
11/// 课程视频流数据
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct CourseVideoStreamData {
14    #[serde(flatten)]
15    pub base: crate::models::VideoStreamData,
16
17    /// 定位参数
18    pub seek_param: String,
19    /// 是否为视频项目
20    pub video_project: bool,
21    /// 数据类型
22    #[serde(rename = "type")]
23    pub data_type: String,
24    /// 结果状态
25    pub result: String,
26    /// 定位类型
27    pub seek_type: String,
28    /// 来源
29    pub from: String,
30    /// 是否重编码
31    pub no_rexcode: i32,
32    /// 响应消息
33    pub message: String,
34    /// 分片视频信息
35    pub fragment_videos: Option<Vec<FragmentVideo>>,
36    /// 状态码
37    pub status: i32,
38}
39
40/// 分片视频
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct FragmentVideo {
43    pub fragment_info: FragmentInfo,
44    pub playable_status: bool,
45    pub video_info: VideoInfo,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct FragmentInfo {
50    pub fragment_type: String,
51    pub index: i64,
52    pub aid: i64,
53    pub fragment_position: String,
54    pub cid: i64,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct VideoInfo {
59    pub no_rexcode: i64,
60    pub fnval: i64,
61    pub video_project: bool,
62    pub expire_time: i64,
63    pub backup_url: Vec<Option<serde_json::Value>>,
64    pub fnver: i64,
65    pub support_formats: Vec<String>,
66    pub support_description: Vec<String>,
67    #[serde(rename = "type")]
68    pub video_info_type: String,
69    pub url: String,
70    pub quality: i64,
71    pub timelength: i64,
72    pub volume: CourseVolume,
73    pub accept_formats: Vec<SupportFormat>,
74    pub support_quality: Vec<i64>,
75    pub file_info: HashMap<String, FileInfo>,
76    pub dash: DashStreams,
77    pub video_codecid: i64,
78    pub cid: i64,
79}
80
81/// 音量信息
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct CourseVolume {
84    pub measured_i: f64,
85    pub target_i: f64,
86    pub target_offset: f64,
87    pub measured_lra: f64,
88    pub target_tp: f64,
89    pub measured_tp: f64,
90    pub measured_threshold: f64,
91    pub multi_scene_args: MultiSceneArgs,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct MultiSceneArgs {
96    pub normal_target_i: String,
97    pub undersized_target_i: String,
98    pub high_dynamic_target_i: String,
99}
100
101/// 文件信息
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct FileInfo {
104    pub infos: Vec<FileInfoEntry>,
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct FileInfoEntry {
109    pub ahead: String,
110    pub vhead: String,
111    pub filesize: i64,
112    pub order: i64,
113    pub timelength: i64,
114}
115
116impl BpiClient {
117    /// 获取课程视频流 URL
118    ///
119    /// 获取课程视频的播放流地址,支持多种视频质量和格式。
120    /// 仅课程视频可用,与普通视频 API 不互通。
121    ///
122    /// # 参数
123    /// | 名称 | 类型 | 说明 |
124    /// | ---- | ---- | ---- |
125    /// | `avid` | u64 | 课程视频 avid |
126    /// | `ep_id` | u64 | 课程分集 ep_id |
127    /// | `cid` | u64 | 视频 cid |
128    /// | `qn` | `Option<VideoQuality>` | 视频质量,可选 |
129    /// | `fnval` | `Option<Fnval>` | 视频格式标志,可选 |
130    ///
131    /// # 注意
132    /// 需要 Cookie(SESSDATA)和 Referer: `https://www.bilibili.com`
133    ///
134    /// # 文档
135    /// [获取课程视频流 URL](https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/cheese/videostream_url.md)
136    pub async fn cheese_video_stream(
137        &self,
138        avid: u64,
139        ep_id: u64,
140        cid: u64,
141        qn: Option<VideoQuality>,
142        fnval: Option<Fnval>
143    ) -> Result<BpiResponse<CourseVideoStreamData>, BpiError> {
144        let mut params = vec![
145            ("avid", avid.to_string()),
146            ("ep_id", ep_id.to_string()),
147            ("cid", cid.to_string()),
148            ("fnver", "0".to_string())
149        ];
150
151        if fnval.is_some_and(|f| f.is_fourk()) {
152            params.push(("fourk", "1".to_string()));
153        }
154
155        if let Some(q) = qn {
156            params.push(("qn", q.as_u32().to_string()));
157        }
158        if let Some(fv) = fnval {
159            params.push(("fnval", fv.bits().to_string()));
160        }
161
162        self
163            .get("https://api.bilibili.com/pugv/player/web/playurl")
164            .with_bilibili_headers()
165            .query(&params)
166            .send_bpi("获取课程视频流 URL").await
167    }
168}
169
170// ==========================
171// 测试
172// ==========================
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    const TEST_AVID: u64 = 997984154;
179    const TEST_EP_ID: u64 = 163956;
180    const TEST_CID: u64 = 1183682680;
181
182    #[tokio::test]
183    async fn test_cheese_playurl() -> Result<(), Box<BpiError>> {
184        let bpi = BpiClient::new();
185
186        let data = bpi
187            .cheese_video_stream(
188                TEST_AVID,
189                TEST_EP_ID,
190                TEST_CID,
191                Some(VideoQuality::P8K),
192                Some(
193                    Fnval::DASH |
194                        Fnval::FOURK |
195                        Fnval::EIGHTK |
196                        Fnval::HDR |
197                        Fnval::DOLBY_AUDIO |
198                        Fnval::DOLBY_VISION |
199                        Fnval::AV1
200                )
201            ).await?
202            .into_data()?;
203
204        tracing::info!("{:#?}", data);
205
206        Ok(())
207    }
208}