Skip to main content

bpi_rs/video/info/
view.rs

1//! 获取视频详细信息 (Web端)
2//!
3//! [查看 API 文档](https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/video)
4
5use crate::models::Account;
6use crate::{ BilibiliRequest, BpiClient, BpiError, BpiResponse };
7use serde::{ Deserialize, Serialize };
8
9/// 视频分辨率信息
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct Dimension {
12    pub width: u32,
13    pub height: u32,
14    pub rotate: u8,
15}
16
17/// 视频状态统计信息
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct Stat {
20    pub aid: u64,
21    pub view: u64,
22    pub danmaku: u64,
23    pub reply: u64,
24    #[serde(rename = "favorite")]
25    pub favorite: u64,
26    #[serde(rename = "fav")]
27    pub fav: Option<u64>,
28    pub coin: u64,
29    pub share: u64,
30    pub now_rank: i64,
31    pub his_rank: i64,
32    pub like: u64,
33    pub dislike: u64,
34    pub evaluation: String,
35    #[serde(rename = "argue_msg")]
36    pub argue_msg: Option<String>,
37    pub vt: u64,
38    pub vv: Option<u64>,
39}
40
41/// 视频争议/警告信息
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct ArgueInfo {
44    pub argue_link: String,
45    pub argue_msg: String,
46    pub argue_type: i32,
47}
48
49/// UP主信息
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct Owner {
52    pub mid: u64,
53    pub name: String,
54    pub face: String,
55}
56
57/// rights对象
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct Rights {
60    pub bp: u8,
61    pub elec: u8,
62    pub download: u8,
63    pub movie: u8,
64    pub pay: u8,
65    pub hd5: u8,
66    pub no_reprint: u8,
67    pub autoplay: u8,
68    pub ugc_pay: u8,
69    pub is_cooperation: u8,
70    pub ugc_pay_preview: u8,
71    pub no_background: u8,
72    pub clean_mode: u8,
73    pub is_stein_gate: u8,
74    pub arc_pay: u8,
75    pub free_watch: u8,
76    // 新增字段
77    pub is_360: u8,
78    pub no_share: u8,
79}
80
81/// 视频每P信息
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct Page {
84    pub cid: u64,
85    pub page: u32,
86    pub from: String,
87    pub part: String,
88    pub duration: u64,
89    pub vid: String,
90    pub weblink: String,
91    pub dimension: Dimension,
92    pub first_frame: Option<String>,
93    pub ctime: u64,
94}
95
96/// 字幕上传者信息
97
98/// 字幕列表
99#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct SubtitleListItem {
101    pub id: u64,
102    pub lan: String,
103    pub lan_doc: String,
104    pub is_lock: bool,
105    pub subtitle_url: String,
106    #[serde(rename = "type")]
107    pub sub_type: u8,
108    pub id_str: String,
109    pub ai_type: u8,
110    pub ai_status: u8,
111    pub author: Account,
112}
113
114/// 字幕信息
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct Subtitle {
117    pub allow_submit: bool,
118    pub list: Vec<SubtitleListItem>,
119}
120
121/// staff成员大会员状态
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct StaffVip {
124    #[serde(rename = "type")]
125    pub type_: u8,
126    pub status: u8,
127    pub due_date: u64,
128    pub vip_pay_type: u8,
129    pub theme_type: u8,
130    pub label: serde_json::Value,
131}
132
133/// staff成员认证信息
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct StaffOfficial {
136    pub role: i32,
137    pub title: String,
138    pub desc: String,
139    #[serde(rename = "type")]
140    pub type_: i32,
141}
142
143/// staff成员信息
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct StaffItem {
146    pub mid: u64,
147    pub title: String,
148    pub name: String,
149    pub face: String,
150    pub vip: StaffVip,
151    pub official: StaffOfficial,
152    pub follower: u64,
153    pub label_style: u8,
154}
155
156/// honor_reply信息
157#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct HonorItem {
159    pub aid: u64,
160    pub hover_type: u8,
161    pub desc: String,
162    pub weekly_recommend_num: u64,
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct HonorReply {
167    pub honor: Vec<HonorItem>,
168}
169
170/// 用户装扮信息
171#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct UserGarb {
173    pub url_image_ani_cut: String,
174}
175
176/// ugc_season中的episodes
177#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct EpisodeArc {
179    pub aid: u64,
180    pub videos: u32,
181    pub type_id: u32,
182    pub type_name: String,
183    pub copyright: u8,
184    pub pic: String,
185    pub title: String,
186    pub pubdate: u64,
187    pub ctime: u64,
188    pub desc: String,
189    pub state: u8,
190    pub duration: u64,
191    pub rights: Rights,
192    pub author: Owner,
193    pub stat: Stat,
194    pub dynamic: String,
195    pub dimension: Dimension,
196    pub desc_v2: serde_json::Value,
197    pub is_chargeable_season: bool,
198    pub is_blooper: bool,
199    pub enable_vt: u8,
200    pub vt_display: String,
201    pub type_id_v2: u32,
202    pub type_name_v2: String,
203    pub is_lesson_video: u8,
204}
205
206/// ugc_season中的section中的episodes
207#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct SectionEpisode {
209    pub season_id: u64,
210    pub section_id: u64,
211    pub id: u64,
212    pub aid: u64,
213    pub cid: u64,
214    pub title: String,
215    pub attribute: u32,
216    pub arc: EpisodeArc,
217    pub page: Page,
218    pub bvid: String,
219    pub pages: Vec<Page>,
220}
221
222/// ugc_season section
223#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct Section {
225    pub season_id: u64,
226    pub id: u64,
227    pub title: String,
228    #[serde(rename = "type")]
229    pub type_: u8,
230    pub episodes: Vec<SectionEpisode>,
231}
232
233/// ugc_season
234#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct UgcSeasonStat {
236    pub season_id: u64,
237    pub view: u64,
238    pub danmaku: u64,
239    pub reply: u64,
240    pub fav: u64,
241    pub coin: u64,
242    pub share: u64,
243    pub now_rank: i64,
244    pub his_rank: i64,
245    pub like: u64,
246    pub vt: u64,
247    pub vv: u64,
248}
249
250#[derive(Debug, Clone, Serialize, Deserialize)]
251pub struct UgcSeason {
252    pub id: u64,
253    pub title: String,
254    pub cover: String,
255    pub mid: u64,
256    pub intro: String,
257    pub sign_state: u8,
258    pub attribute: u32,
259    pub sections: Vec<Section>,
260    pub stat: UgcSeasonStat,
261    pub ep_count: u32,
262    pub season_type: u8,
263    pub is_pay_season: bool,
264    pub enable_vt: u8,
265}
266
267/// video data完整结构体
268#[derive(Debug, Clone, Serialize, Deserialize)]
269pub struct VideoData {
270    pub aid: u64,
271    pub bvid: String,
272    pub videos: u32,
273    pub tid: u32,
274    pub tid_v2: u32,
275    pub tname: String,
276    pub tname_v2: String,
277    pub copyright: u8,
278    pub pic: String,
279    pub title: String,
280    pub pubdate: u64,
281    pub ctime: u64,
282    pub desc: String,
283    pub desc_v2: Vec<serde_json::Value>,
284    pub state: u32,
285    pub duration: u64, // 单位为秒
286    #[serde(default = "default_forward")] // 仅撞车视频存在此字段
287    pub forward: u64,
288    pub mission_id: Option<u64>,
289    #[serde(default = "default_redirect_url")] // 用于番剧&影视的av/bv->ep
290    pub redirect_url: String,
291    pub rights: Rights,
292    pub owner: Owner,
293    pub stat: Stat,
294    pub argue_info: ArgueInfo,
295    pub dynamic: String,
296    pub cid: u64,
297    pub dimension: Dimension,
298    pub premiere: serde_json::Value,
299    pub teenage_mode: u8,
300    pub is_chargeable_season: bool,
301    pub is_story: bool,
302    pub is_upower_exclusive: bool,
303    pub is_upower_play: bool,
304    pub is_upower_preview: bool,
305    pub no_cache: bool,
306    pub pages: Vec<Page>,
307    pub subtitle: Subtitle,
308    pub ugc_season: Option<UgcSeason>, // 不在合集中的视频无此项
309    #[serde(default = "default_staff")]
310    pub staff: Vec<StaffItem>, // 非合作视频无此项
311    pub is_season_display: bool,
312    pub user_garb: UserGarb,
313    pub honor_reply: serde_json::Value,
314    pub like_icon: String,
315    pub need_jump_bv: bool,
316    pub disable_show_up_info: bool,
317    pub is_story_play: u32,
318    pub is_view_self: bool,
319    pub is_upower_exclusive_with_qa: bool,
320}
321
322fn default_forward() -> u64 {
323    0
324}
325
326fn default_redirect_url() -> String {
327    String::new()
328}
329
330fn default_staff() -> Vec<StaffItem> {
331    let v: Vec<StaffItem> = Vec::new();
332    v
333}
334
335/// 获取视频详细信息响应类型
336pub type VideoInfoResponse = BpiResponse<VideoData>;
337
338impl BpiClient {
339    /// 获取视频详细信息 (Web端)
340    ///
341    /// # 文档
342    /// [查看API文档](https://socialsisteryi.github.io/bilibili-API-collect/docs/video/video.html#获取视频详细信息)
343    ///
344    /// # 参数
345    /// | 名称   | 类型         | 说明                 |
346    /// | ------ | ------------| -------------------- |
347    /// | `aid`  | `Option<u64>` | 稿件 avid,可选      |
348    /// | `bvid` | `Option<&str>`| 稿件 bvid,可选      |
349    ///
350    /// 两者任选一个
351    pub async fn video_info(
352        &self,
353        aid: Option<u64>,
354        bvid: Option<&str>
355    ) -> Result<VideoInfoResponse, BpiError> {
356        let aid = aid.map(|v| v.to_string());
357        let bvid = bvid.map(|v| v.to_string());
358
359        self
360            .get("https://api.bilibili.com/x/web-interface/view")
361            .query(
362                &[
363                    ("aid", aid),
364                    ("bvid", bvid),
365                ]
366            )
367            .send_bpi("视频详细信息").await
368    }
369}
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374
375    #[tokio::test]
376    async fn test_video_info() {
377        let bpi = BpiClient::new();
378
379        let aid = Some(10001);
380        let bvid = None;
381
382        match bpi.video_info(aid, bvid).await {
383            Ok(resp) => {
384                if resp.code == 0 {
385                    // tracing::info!("视频标题: {}", resp.data.title);
386                    // tracing::info!("UP主: {} ({})", resp.data.owner.name, resp.data.owner.mid);
387                    // tracing::info!("总播放数: {}", resp.data.stat.view);
388                    // tracing::info!("分P数量: {}", resp.data.pages.len());
389                } else {
390                    tracing::info!("请求失败: code={}, message={}", resp.code, resp.message);
391                }
392            }
393            Err(err) => {
394                panic!("请求出错: {}", err);
395            }
396        }
397    }
398}