1use std::collections::HashMap;
6
7use crate::models::{ DashStreams, Fnval, SupportFormat, VideoQuality };
8use crate::{ BilibiliRequest, BpiClient, BpiError, BpiResponse };
9use serde::{ Deserialize, Serialize };
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct CourseVideoStreamData {
14 #[serde(flatten)]
15 pub base: crate::models::VideoStreamData,
16
17 pub seek_param: String,
19 pub video_project: bool,
21 #[serde(rename = "type")]
23 pub data_type: String,
24 pub result: String,
26 pub seek_type: String,
28 pub from: String,
30 pub no_rexcode: i32,
32 pub message: String,
34 pub fragment_videos: Option<Vec<FragmentVideo>>,
36 pub status: i32,
38}
39
40#[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#[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#[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 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(¶ms)
166 .send_bpi("获取课程视频流 URL").await
167 }
168}
169
170#[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}