Skip to main content

bpi_rs/video/
player.rs

1//! B站 web 播放器相关接口
2//!
3//! [查看 API 文档](https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/video)
4use crate::{ BilibiliRequest, BpiClient, BpiError, BpiResponse };
5use serde::{ Deserialize, Serialize };
6
7// --- 响应数据结构体 ---
8
9/// web 播放器信息响应数据
10#[derive(Debug, Clone, Deserialize, Serialize)]
11pub struct PlayerInfoResponseData {
12    /// 视频 aid
13    pub aid: u64,
14    /// 视频 bvid
15    pub bvid: String,
16    pub allow_bp: bool,
17    pub no_share: bool,
18    /// 视频 cid
19    pub cid: u64,
20    /// webmask 防挡字幕信息
21    pub dm_mask: Option<DmMaskInfo>,
22    /// 字幕信息
23    pub subtitle: Option<SubtitleInfo>,
24    /// 分段章节信息
25    #[serde(default)]
26    pub view_points: Vec<ViewPoint>,
27    /// 请求 IP 信息
28    pub ip_info: Option<serde_json::Value>,
29    /// 登录用户 mid
30    pub login_mid: u64,
31    pub login_mid_hash: Option<String>,
32    /// 是否为该视频 UP 主
33    pub is_owner: bool,
34    pub name: String,
35    pub permission: String,
36    /// 登录用户等级信息
37    pub level_info: Option<serde_json::Value>,
38    /// 登录用户 VIP 信息
39    pub vip: Option<serde_json::Value>,
40    /// 答题状态
41    pub answer_status: u8,
42    pub block_time: u64,
43    pub role: String,
44    /// 上次观看时间
45    pub last_play_time: u64,
46    /// 上次观看 cid
47    pub last_play_cid: u64,
48    /// 当前 UNIX 秒级时间戳
49    pub now_time: u64,
50    /// 在线人数
51    pub online_count: Option<u64>,
52    /// 是否必须登陆才能查看字幕
53    pub need_login_subtitle: bool,
54    /// 预告提示
55    pub preview_toast: String,
56    /// 互动视频资讯
57    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    /// 背景音乐信息
66    pub bgm_info: Option<BgmInfo>,
67    pub toast_block: bool,
68    /// 是否为充电专属视频
69    pub is_upower_exclusive: bool,
70    pub is_upower_play: bool,
71    pub is_ugc_pay_preview: bool,
72    /// 充电专属视频信息
73    pub elec_high_level: Option<ElecHighLevel>,
74    pub disable_show_up_info: bool,
75}
76
77/// webmask 防挡字幕信息
78#[derive(Debug, Clone, Deserialize, Serialize)]
79pub struct DmMaskInfo {
80    /// 视频 cid
81    pub cid: u64,
82    pub plat: u8,
83    /// webmask 取样 fps
84    pub fps: u64,
85    pub time: u64,
86    /// webmask 资源 url
87    pub mask_url: String,
88}
89
90/// 字幕信息
91#[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/// 单个字幕信息
101#[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    /// 语言类型英文字母缩写
109    pub lan: String,
110    /// 语言类型中文名称
111    pub lan_doc: String,
112    /// 资源 url 地址
113    pub subtitle_url: String,
114    #[serde(rename = "type")]
115    pub subtitle_type: u8,
116}
117
118/// 分段章节信息
119#[derive(Debug, Clone, Deserialize, Serialize)]
120pub struct ViewPoint {
121    /// 分段章节名
122    pub content: String,
123    /// 分段章节起始秒数
124    pub from: u64,
125    /// 分段章节结束秒数
126    pub to: u64,
127    #[serde(rename = "type")]
128    pub point_type: u8,
129    /// 图片资源地址
130    #[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/// 互动视频资讯
139#[derive(Debug, Clone, Deserialize, Serialize)]
140pub struct InteractionInfo {
141    /// 剧情图 id
142    pub graph_version: u64,
143    /// 未登录有机会返回
144    pub msg: Option<String>,
145    /// 错误信息
146    pub error_toast: Option<String>,
147    pub mark: Option<u8>,
148    pub need_reload: Option<u8>,
149}
150
151/// 播放器选项
152#[derive(Debug, Clone, Deserialize, Serialize)]
153pub struct PlayerOptions {
154    /// 是否 360 全景视频
155    pub is_360: bool,
156    pub without_vip: bool,
157}
158
159/// 背景音乐信息
160#[derive(Debug, Clone, Deserialize, Serialize)]
161pub struct BgmInfo {
162    /// 音乐 id
163    pub music_id: String,
164    /// 音乐标题
165    pub music_title: String,
166    /// 跳转 URL
167    pub jump_url: String,
168}
169
170/// 充电专属视频信息
171#[derive(Debug, Clone, Deserialize, Serialize)]
172pub struct ElecHighLevel {
173    /// 解锁视频所需最低定价档位的代码
174    pub privilege_type: u64,
175    /// 提示标题
176    pub title: String,
177    /// 提示子标题
178    pub sub_title: String,
179    /// 是否显示按钮
180    pub show_button: bool,
181    /// 按钮文本
182    pub button_text: String,
183    /// 跳转url信息
184    pub jump_url: Option<serde_json::Value>,
185    /// 充电介绍语
186    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    /// 获取 web 播放器信息
199    ///
200    /// # 文档
201    /// [查看API文档](https://socialsisteryi.github.io/bilibili-API-collect/docs/video/player.html#获取web播放器信息)
202    ///
203    /// # 参数
204    /// | 名称        | 类型           | 说明                 |
205    /// | ----------- | --------------| -------------------- |
206    /// | `aid`       | `Option<u64>`   | 稿件 avid,可选      |
207    /// | `bvid`      | `Option<&str>`  | 稿件 bvid,可选      |
208    /// | `cid`       | u64           | 稿件 cid             |
209    /// | `season_id` | `Option<u64>`   | 番剧 season_id,可选 |
210    /// | `ep_id`     | `Option<u64>`   | 剧集 ep_id,可选     |
211    ///
212    /// `aid` 和 `bvid` 必须提供一个。
213    pub async fn video_player_info_v2(
214        &self,
215        aid: Option<u64>,
216        bvid: Option<&str>,
217        cid: u64,
218        season_id: Option<u64>,
219        ep_id: Option<u64>
220    ) -> Result<BpiResponse<PlayerInfoResponseData>, BpiError> {
221        if aid.is_none() && bvid.is_none() {
222            return Err(BpiError::parse("必须提供 aid 或 bvid"));
223        }
224
225        let mut params = vec![("cid", cid.to_string())];
226        if let Some(a) = aid {
227            params.push(("aid", a.to_string()));
228        }
229        if let Some(b) = bvid {
230            params.push(("bvid", b.to_string()));
231        }
232        if let Some(s) = season_id {
233            params.push(("season_id", s.to_string()));
234        }
235        if let Some(e) = ep_id {
236            params.push(("ep_id", e.to_string()));
237        }
238        let params = self.get_wbi_sign2(params).await?;
239
240        self
241            .get("https://api.bilibili.com/x/player/wbi/v2")
242            .query(&params)
243            .send_bpi("获取 web 播放器信息").await
244    }
245}
246
247// --- 测试模块 ---
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252    use tracing::info;
253
254    const TEST_AID: u64 = 1906473802;
255    const TEST_CID: u64 = 636329244;
256
257    #[tokio::test]
258    async fn test_video_player_info_v2_by_aid() -> Result<(), BpiError> {
259        let bpi = BpiClient::new();
260        let resp = bpi.video_player_info_v2(Some(TEST_AID), None, TEST_CID, None, None).await?;
261        let data = resp.into_data()?;
262
263        info!("播放器信息: {:?}", data);
264
265        assert_eq!(data.aid, TEST_AID);
266        assert_eq!(data.cid, TEST_CID);
267
268        Ok(())
269    }
270
271    #[tokio::test]
272    async fn test_video_player_info_v2_by_bvid() -> Result<(), BpiError> {
273        let bpi = BpiClient::new();
274        let resp = bpi.video_player_info_v2(Some(TEST_AID), None, TEST_CID, None, None).await?;
275        let data = resp.into_data()?;
276
277        info!("播放器信息: {:?}", data);
278
279        assert_eq!(data.aid, TEST_AID);
280        assert_eq!(data.cid, TEST_CID);
281
282        Ok(())
283    }
284}