1use crate::{BilibiliRequest, BpiClient, BpiError, BpiResponse};
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Deserialize, Serialize)]
11pub struct PlayerInfoResponseData {
12 pub aid: u64,
14 pub bvid: String,
16 pub allow_bp: bool,
17 pub no_share: bool,
18 pub cid: u64,
20 pub dm_mask: Option<DmMaskInfo>,
22 pub subtitle: Option<SubtitleInfo>,
24 #[serde(default)]
26 pub view_points: Vec<ViewPoint>,
27 pub ip_info: Option<serde_json::Value>,
29 pub login_mid: u64,
31 pub login_mid_hash: Option<String>,
32 pub is_owner: bool,
34 pub name: String,
35 pub permission: String,
36 pub level_info: Option<serde_json::Value>,
38 pub vip: Option<serde_json::Value>,
40 pub answer_status: u8,
42 pub block_time: u64,
43 pub role: String,
44 pub last_play_time: u64,
46 pub last_play_cid: u64,
48 pub now_time: u64,
50 pub online_count: Option<u64>,
52 pub need_login_subtitle: bool,
54 pub preview_toast: String,
56 pub interaction: Option<InteractionInfo>,
58 pub options: Option<PlayerOptions>,
59 pub guide_attention: Option<serde_json::Value>,
60 pub jump_card: Option<serde_json::Value>,
61 pub operation_card: Option<serde_json::Value>,
62 pub online_switch: Option<serde_json::Value>,
63 pub fawkes: Option<serde_json::Value>,
64 pub show_switch: Option<serde_json::Value>,
65 pub bgm_info: Option<BgmInfo>,
67 pub toast_block: bool,
68 pub is_upower_exclusive: bool,
70 pub is_upower_play: bool,
71 pub is_ugc_pay_preview: bool,
72 pub elec_high_level: Option<ElecHighLevel>,
74 pub disable_show_up_info: bool,
75}
76
77#[derive(Debug, Clone, Deserialize, Serialize)]
79pub struct DmMaskInfo {
80 pub cid: u64,
82 pub plat: u8,
83 pub fps: u64,
85 pub time: u64,
86 pub mask_url: String,
88}
89
90#[derive(Debug, Clone, Deserialize, Serialize)]
92pub struct SubtitleInfo {
93 pub allow_submit: bool,
94 pub lan: String,
95 pub lan_doc: String,
96 #[serde(default)]
97 pub subtitles: Vec<SubtitleItem>,
98}
99
100#[derive(Debug, Clone, Deserialize, Serialize)]
102pub struct SubtitleItem {
103 pub ai_status: u8,
104 pub ai_type: u8,
105 pub id: u64,
106 pub id_str: String,
107 pub is_lock: bool,
108 pub lan: String,
110 pub lan_doc: String,
112 pub subtitle_url: String,
114 #[serde(rename = "type")]
115 pub subtitle_type: u8,
116}
117
118#[derive(Debug, Clone, Deserialize, Serialize)]
120pub struct ViewPoint {
121 pub content: String,
123 pub from: u64,
125 pub to: u64,
127 #[serde(rename = "type")]
128 pub point_type: u8,
129 #[serde(rename = "imgUrl")]
131 pub img_url: String,
132 #[serde(rename = "logoUrl")]
133 pub logo_url: String,
134 pub team_type: String,
135 pub team_name: String,
136}
137
138#[derive(Debug, Clone, Deserialize, Serialize)]
140pub struct InteractionInfo {
141 pub graph_version: u64,
143 pub msg: Option<String>,
145 pub error_toast: Option<String>,
147 pub mark: Option<u8>,
148 pub need_reload: Option<u8>,
149}
150
151#[derive(Debug, Clone, Deserialize, Serialize)]
153pub struct PlayerOptions {
154 pub is_360: bool,
156 pub without_vip: bool,
157}
158
159#[derive(Debug, Clone, Deserialize, Serialize)]
161pub struct BgmInfo {
162 pub music_id: String,
164 pub music_title: String,
166 pub jump_url: String,
168}
169
170#[derive(Debug, Clone, Deserialize, Serialize)]
172pub struct ElecHighLevel {
173 pub privilege_type: u64,
175 pub title: String,
177 pub sub_title: String,
179 pub show_button: bool,
181 pub button_text: String,
183 pub jump_url: Option<serde_json::Value>,
185 pub intro: String,
187 #[serde(default)]
188 pub open: bool,
189 #[serde(default)]
190 pub new: bool,
191 #[serde(default)]
192 pub question_text: String,
193 #[serde(default)]
194 pub qa_detail_link: String,
195}
196
197impl BpiClient {
198 pub async fn video_player_info_v2(
213 &self,
214 aid: Option<u64>,
215 bvid: Option<&str>,
216 cid: u64,
217 season_id: Option<u64>,
218 ep_id: Option<u64>,
219 ) -> Result<BpiResponse<PlayerInfoResponseData>, BpiError> {
220 if aid.is_none() && bvid.is_none() {
221 return Err(BpiError::parse("必须提供 aid 或 bvid"));
222 }
223
224 let mut params = vec![("cid", cid.to_string())];
225 if let Some(a) = aid {
226 params.push(("aid", a.to_string()));
227 }
228 if let Some(b) = bvid {
229 params.push(("bvid", b.to_string()));
230 }
231 if let Some(s) = season_id {
232 params.push(("season_id", s.to_string()));
233 }
234 if let Some(e) = ep_id {
235 params.push(("ep_id", e.to_string()));
236 }
237 let params = self.get_wbi_sign2(params).await?;
238
239 self.get("https://api.bilibili.com/x/player/wbi/v2")
240 .query(¶ms)
241 .send_bpi("获取 web 播放器信息")
242 .await
243 }
244}
245
246#[cfg(test)]
249mod tests {
250 use super::*;
251 use tracing::info;
252
253 const TEST_AID: u64 = 1906473802;
254 const TEST_CID: u64 = 636329244;
255
256 #[tokio::test]
257
258 async fn test_video_player_info_v2_by_aid() -> Result<(), BpiError> {
259 let bpi = BpiClient::new();
260 let resp = bpi
261 .video_player_info_v2(Some(TEST_AID), None, TEST_CID, None, None)
262 .await?;
263 let data = resp.into_data()?;
264
265 info!("播放器信息: {:?}", data);
266
267 assert_eq!(data.aid, TEST_AID);
268 assert_eq!(data.cid, TEST_CID);
269
270 Ok(())
271 }
272
273 #[tokio::test]
274
275 async fn test_video_player_info_v2_by_bvid() -> Result<(), BpiError> {
276 let bpi = BpiClient::new();
277 let resp = bpi
278 .video_player_info_v2(Some(TEST_AID), None, TEST_CID, None, None)
279 .await?;
280 let data = resp.into_data()?;
281
282 info!("播放器信息: {:?}", data);
283
284 assert_eq!(data.aid, TEST_AID);
285 assert_eq!(data.cid, TEST_CID);
286
287 Ok(())
288 }
289}