Skip to main content

bpi_rs/video/
interact_video.rs

1//! 互动视频相关接口
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/// 互动视频模块详细信息响应数据
10#[derive(Debug, Clone, Deserialize, Serialize)]
11pub struct InteractiveVideoInfoResponseData {
12    /// 视频模块(分P)标题
13    pub title: String,
14    /// 当前模块 ID
15    pub edge_id: u64,
16    /// 进度回溯信息
17    #[serde(default)]
18    pub story_list: Vec<InteractiveVideoStory>,
19    /// 当前模块信息
20    pub edges: Option<InteractiveVideoEdges>,
21    /// 预加载的分P
22    pub preload: Option<InteractiveVideoPreload>,
23    /// 变量列表
24    #[serde(default)]
25    pub hidden_vars: Vec<InteractiveVideoHiddenVar>,
26    /// 是否为结束模块, 0: 普通模块, 1: 结束模块
27    pub is_leaf: u8,
28    /// 禁止记录选择, 1: 禁止
29    #[serde(default)]
30    pub no_tutorial: u8,
31    /// 禁止进度回溯, 1: 禁止
32    #[serde(default)]
33    pub no_backtracking: u8,
34    /// 禁止结尾评分, 1: 禁止
35    #[serde(default)]
36    pub no_evaluation: u8,
37}
38
39/// 进度回溯信息
40#[derive(Debug, Clone, Deserialize, Serialize)]
41pub struct InteractiveVideoStory {
42    /// 模块编号
43    pub node_id: u64,
44    /// 同上
45    pub edge_id: u64,
46    /// 模块(分P)标题
47    pub title: String,
48    /// 模块(分P)cid
49    pub cid: u64,
50    /// 记录播放开始位置,单位为毫秒
51    pub start_pos: u64,
52    /// 分P封面 url
53    pub cover: String,
54    /// 是否为当前模块, 1: 是
55    #[serde(default)]
56    pub is_current: u8,
57    /// 进度序号,从0开始向上增长
58    pub cursor: u64,
59}
60
61/// 当前模块信息
62#[derive(Debug, Clone, Deserialize, Serialize)]
63pub struct InteractiveVideoEdges {
64    /// 当前分P分辨率
65    pub dimension: Option<InteractiveVideoDimension>,
66    /// 问题列表,问题结束模块无此项
67    #[serde(default)]
68    pub questions: Vec<InteractiveVideoQuestion>,
69    /// 问题外观
70    pub skin: Option<serde_json::Value>,
71}
72
73/// 分辨率信息
74#[derive(Debug, Clone, Deserialize, Serialize)]
75pub struct InteractiveVideoDimension {
76    /// 宽度
77    pub width: u32,
78    /// 高度
79    pub height: u32,
80    /// 是否将宽高对换, 0: 正常, 1: 对换
81    pub rotate: u8,
82    /// 作用尚不明确
83    pub sar: String,
84}
85
86/// 问题信息
87#[derive(Debug, Clone, Deserialize, Serialize)]
88pub struct InteractiveVideoQuestion {
89    /// 作用尚不明确
90    pub id: u64,
91    /// 选项显示模式, 0: 不显示选项, 1: 底部选项模式, 2: 坐标定点模式
92    #[serde(rename = "type")]
93    pub question_type: u8,
94    /// 作用尚不明确
95    pub start_time_r: u32,
96    /// 回答限时,单位为毫秒,不限时为-1
97    pub duration: i64,
98    /// 是否暂停播放视频, 0: 不暂停, 1: 暂停播放
99    pub pause_video: u8,
100    /// 作用尚不明确
101    pub title: String,
102    /// 选项列表
103    pub choices: Vec<InteractiveVideoChoice>,
104}
105
106/// 选项信息
107#[derive(Debug, Clone, Deserialize, Serialize)]
108pub struct InteractiveVideoChoice {
109    /// 选项所跳转的模块 id
110    pub id: u64,
111    /// 跳转信息文字, 例如 `JUMP+{模块编号}+{cid}`
112    pub platform_action: String,
113    /// 点击后对变量运算语句
114    pub native_action: String,
115    /// 选项出现条件判断语句
116    pub condition: String,
117    /// 选项所跳转分P的cid
118    pub cid: u64,
119    /// 选项文字
120    pub option: String,
121    /// 是否为默认选项, 1: 是
122    #[serde(default)]
123    pub is_default: Option<u8>,
124    /// 是否为隐藏选项, 1: 是
125    #[serde(default)]
126    pub is_hidden: Option<u8>,
127}
128
129/// 预加载的分P信息
130#[derive(Debug, Clone, Deserialize, Serialize)]
131pub struct InteractiveVideoPreload {
132    /// 预加载的分P列表
133    #[serde(default)]
134    pub video: Vec<InteractiveVideoPreloadVideo>,
135}
136
137/// 预加载的分P
138#[derive(Debug, Clone, Deserialize, Serialize)]
139pub struct InteractiveVideoPreloadVideo {
140    /// 稿件avid
141    pub aid: u64,
142    /// 分P cid
143    pub cid: u64,
144}
145
146/// 变量信息
147#[derive(Debug, Clone, Deserialize, Serialize)]
148pub struct InteractiveVideoHiddenVar {
149    /// 变量值
150    pub value: i64,
151    /// 变量编号
152    pub id: String,
153    /// 变量编号,语句中一般使用此项
154    pub id_v2: String,
155    /// 变量类型, 1: 普通变量, 2: 随机值
156    #[serde(rename = "type")]
157    pub var_type: u8,
158    /// 是否展示变量, 0: 否, 1: 是
159    pub is_show: u8,
160    /// 变量名
161    pub name: String,
162}
163
164impl BpiClient {
165    /// 获取互动视频模块详细信息
166    ///
167    /// # 文档
168    /// [查看API文档](https://socialsisteryi.github.io/bilibili-API-collect/docs/video/interact_video.html#获取互动视频信息)
169    ///
170    /// # 参数
171    /// | 名称           | 类型           | 说明                 |
172    /// | -------------- | --------------| -------------------- |
173    /// | `aid`          | `Option<u64>`   | 稿件 avid,可选      |
174    /// | `bvid`         | `Option<&str>`  | 稿件 bvid,可选      |
175    /// | `graph_version`| u64           | 剧情图 ID            |
176    /// | `edge_id`      | `Option<u64>`   | 模块编号,0或留空为起始模块,可选 |
177    ///
178    /// `aid` 和 `bvid` 必须提供一个。
179    pub async fn video_interactive_video_info(
180        &self,
181        aid: Option<u64>,
182        bvid: Option<&str>,
183        graph_version: u64,
184        edge_id: Option<u64>
185    ) -> Result<BpiResponse<InteractiveVideoInfoResponseData>, BpiError> {
186        if aid.is_none() && bvid.is_none() {
187            return Err(BpiError::parse("必须提供 aid 或 bvid"));
188        }
189
190        let mut req = self
191            .get("https://api.bilibili.com/x/stein/edgeinfo_v2")
192            .query(&[("graph_version", &graph_version.to_string())]);
193
194        if let Some(a) = aid {
195            req = req.query(&[("aid", &a.to_string())]);
196        }
197        if let Some(b) = bvid {
198            req = req.query(&[("bvid", b)]);
199        }
200        if let Some(e) = edge_id {
201            req = req.query(&[("edge_id", &e.to_string())]);
202        }
203
204        req.send_bpi("获取互动视频模块详细信息").await
205    }
206}
207
208// --- 测试模块 ---
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213    use tracing::info;
214
215    const TEST_AID: u64 = 114347430905959;
216    const TEST_GRAPH_VERSION: u64 = 1273647;
217
218    #[tokio::test]
219    async fn test_video_interactive_video_info_by_aid() -> Result<(), BpiError> {
220        let bpi = BpiClient::new();
221        let resp = bpi.video_interactive_video_info(
222            Some(TEST_AID),
223            None,
224            TEST_GRAPH_VERSION,
225            None
226        ).await?;
227        let data = resp.into_data()?;
228
229        info!("互动视频信息: {:?}", data);
230        assert!(!data.title.is_empty());
231        assert!(!data.story_list.is_empty());
232
233        Ok(())
234    }
235}