bpi_rs/video/info/
view.rs

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