Skip to main content

ybd_core/model/
param.rs

1//! 请求参数模型
2
3use std::{
4        collections::BTreeMap,
5        time::{SystemTime, UNIX_EPOCH},
6};
7
8use crate::{
9        actuator::get_basic_video_info,
10        client::BiliClient,
11        error::{Error, Result},
12        login::get_wbi_keys,
13};
14
15const MIXIN_KEY_ENC_TAB: [usize; 64] = [
16        46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49, 33, 9, 42, 19,
17        29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40, 61, 26, 17, 0, 1, 60, 51, 30, 4,
18        22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11, 36, 20, 34, 44, 52,
19];
20
21#[derive(Copy, Clone)]
22pub enum Quality {
23        K8 = 127,
24        K4 = 120,
25        FHD1080P60 = 116,
26        FHD1080P = 80,
27        HD720P = 64,
28        SD480P = 32,
29        LD360P = 16,
30}
31
32#[derive(Copy, Clone)]
33pub enum Fnval {
34        Mp4 = 0,
35        Dash = 16,
36        DashFull = 4048, // DASH + 4K + HDR + 杜比全开
37}
38
39/// 视频信息请求参数
40///
41/// 通过 [`VideoRequestParamBuilder`] 构造,签名相关字段自动计算。
42///
43/// * `bvid`: 视频唯一标识
44/// * `cid`: 视频分P标识
45/// * `qn`: 视频画质等级,见 [`Quality`]
46/// * `fnval`: 视频流格式,见 [`Fnval`]
47/// * `fourk`: 是否允许 4K,默认 true
48/// * `fnver`: B站内部版本号,固定为 "0",B站更新时可通过 Builder 覆盖
49/// * `wts`: WBI 时间戳,构造时自动生成
50/// * `img_key`: WBI 签名所需 img_key,从 B站接口获取
51/// * `sub_key`: WBI 签名所需 sub_key,从 B站接口获取
52/// * `w_rid`: WBI 签名结果,由 `wts`、`img_key`、`sub_key` 自动计算
53pub struct VideoRequestParam {
54        pub bvid: String,
55        pub cid: String,
56        pub qn: Quality,
57        pub fnval: Fnval,
58        pub fourk: bool,
59        fnver: String,
60        wts: String,
61        w_rid: String,
62}
63
64impl VideoRequestParam {
65        pub fn to_query_string(&self) -> String {
66                format!(
67                        "bvid={}&cid={}&qn={}&fnval={}&fnver={}&fourk={}&wts={}&w_rid={}",
68                        self.bvid,
69                        self.cid,
70                        self.qn as u32,
71                        self.fnval as u32,
72                        self.fnver,
73                        self.fourk as u8,
74                        self.wts,
75                        self.w_rid,
76                )
77        }
78}
79
80struct Wbi {
81        img_key: String,
82        sub_key: String,
83}
84
85impl Wbi {
86        fn new(
87                img_key: impl Into<String>,
88                sub_key: impl Into<String>,
89        ) -> Self {
90                Self {
91                        img_key: img_key.into(),
92                        sub_key: sub_key.into(),
93                }
94        }
95
96        fn mixin_key(&self) -> String {
97                let orig = format!("{}{}", self.img_key, self.sub_key);
98                let chars: Vec<char> = orig.chars().collect();
99                MIXIN_KEY_ENC_TAB
100                        .iter()
101                        .filter_map(|&i| chars.get(i))
102                        .take(32)
103                        .collect()
104        }
105
106        /// 对参数进行 WBI 签名,签名依赖wts, 返回 (wts, w_rid)
107        fn sign(
108                &self,
109                params: &mut BTreeMap<String, String>,
110        ) -> (String, String) {
111                let wts = SystemTime::now()
112                        .duration_since(UNIX_EPOCH)
113                        .unwrap()
114                        .as_secs();
115                let wts_str = wts.to_string();
116                params.insert("wts".to_string(), wts_str.clone());
117
118                let mixin_key = self.mixin_key();
119                let query = params
120                        .iter()
121                        .map(|(k, v)| {
122                                let filtered: String =
123                                        v.chars().filter(|c| !"!'()*".contains(*c)).collect();
124                                format!("{}={}", k, urlencoding::encode(&filtered))
125                        })
126                        .collect::<Vec<_>>()
127                        .join("&");
128
129                let w_rid = format!("{:x}", md5::compute(format!("{}{}", query, mixin_key)));
130                (wts_str, w_rid)
131        }
132}
133
134pub struct VideoRequestParamBuilder {
135        bvid: Option<String>,
136        cid: Option<String>,
137        qn: Quality,
138        fnval: Fnval,
139        fourk: bool,
140        fnver: Option<String>,
141}
142
143impl Default for VideoRequestParamBuilder {
144        fn default() -> Self {
145                Self {
146                        bvid: None,
147                        cid: None,
148                        qn: Quality::FHD1080P,  // 默认 1080P
149                        fnval: Fnval::DashFull, // 默认 DASH
150                        fourk: true,
151                        fnver: None, // 默认 "0"
152                }
153        }
154}
155
156impl VideoRequestParamBuilder {
157        pub async fn new(
158                bvid: &str,
159                bili_client: &BiliClient,
160        ) -> Result<Self> {
161                let (_, _, cid) = get_basic_video_info(bvid, Some(&bili_client)).await?;
162                Ok(Self {
163                        bvid: Some(bvid.into()),
164                        cid: Some(cid),
165                        ..Default::default()
166                })
167        }
168
169        pub fn qn(
170                mut self,
171                qn: Quality,
172        ) -> Self {
173                self.qn = qn;
174                self
175        }
176
177        pub fn fnval(
178                mut self,
179                fnval: Fnval,
180        ) -> Self {
181                self.fnval = fnval;
182                self
183        }
184
185        pub fn allow_4k(mut self) -> Self {
186                self.fourk = true;
187                self
188        }
189
190        /// 一般不需要调用,B站改版本号时才用
191        pub fn fnver(
192                mut self,
193                v: impl Into<String>,
194        ) -> Self {
195                self.fnver = Some(v.into());
196                self
197        }
198
199        pub async fn build(
200                self,
201                bili_client: &BiliClient,
202        ) -> Result<VideoRequestParam> {
203                let bvid = self.bvid.ok_or(Error::Build("bvid".into()))?;
204                let cid = self.cid.ok_or(Error::Build("cid".into()))?;
205
206                let (img_key, sub_key) = get_wbi_keys(bili_client).await?;
207
208                let wbi = Wbi::new(img_key, sub_key);
209
210                // 构造待签名参数
211                let mut params = BTreeMap::new();
212                params.insert("bvid".to_string(), bvid.clone());
213                params.insert("cid".to_string(), cid.clone());
214                params.insert("qn".to_string(), (self.qn as u32).to_string());
215                params.insert("fnval".to_string(), (self.fnval as u32).to_string());
216                params.insert("fourk".to_string(), (self.fourk as u8).to_string());
217
218                let (wts, w_rid) = wbi.sign(&mut params);
219
220                Ok(VideoRequestParam {
221                        bvid,
222                        cid,
223                        qn: self.qn,
224                        fnval: self.fnval,
225                        fourk: self.fourk,
226                        fnver: self.fnver.unwrap_or_else(|| "0".into()),
227                        wts,
228                        w_rid,
229                })
230        }
231}