1use 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, }
38
39pub 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 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, fnval: Fnval::DashFull, fourk: true,
151 fnver: None, }
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 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 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}