use std::{
collections::BTreeMap,
time::{SystemTime, UNIX_EPOCH},
};
use crate::{
actuator::get_basic_video_info,
client::BiliClient,
error::{Error, Result},
login::get_wbi_keys,
};
const MIXIN_KEY_ENC_TAB: [usize; 64] = [
46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49, 33, 9, 42, 19,
29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40, 61, 26, 17, 0, 1, 60, 51, 30, 4,
22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11, 36, 20, 34, 44, 52,
];
#[derive(Copy, Clone)]
pub enum Quality {
K8 = 127,
K4 = 120,
FHD1080P60 = 116,
FHD1080P = 80,
HD720P = 64,
SD480P = 32,
LD360P = 16,
}
#[derive(Copy, Clone)]
pub enum Fnval {
Mp4 = 0,
Dash = 16,
DashFull = 4048, }
pub struct VideoRequestParam {
pub bvid: String,
pub cid: String,
pub qn: Quality,
pub fnval: Fnval,
pub fourk: bool,
fnver: String,
wts: String,
w_rid: String,
}
impl VideoRequestParam {
pub fn to_query_string(&self) -> String {
format!(
"bvid={}&cid={}&qn={}&fnval={}&fnver={}&fourk={}&wts={}&w_rid={}",
self.bvid,
self.cid,
self.qn as u32,
self.fnval as u32,
self.fnver,
self.fourk as u8,
self.wts,
self.w_rid,
)
}
}
struct Wbi {
img_key: String,
sub_key: String,
}
impl Wbi {
fn new(
img_key: impl Into<String>,
sub_key: impl Into<String>,
) -> Self {
Self {
img_key: img_key.into(),
sub_key: sub_key.into(),
}
}
fn mixin_key(&self) -> String {
let orig = format!("{}{}", self.img_key, self.sub_key);
let chars: Vec<char> = orig.chars().collect();
MIXIN_KEY_ENC_TAB
.iter()
.filter_map(|&i| chars.get(i))
.take(32)
.collect()
}
fn sign(
&self,
params: &mut BTreeMap<String, String>,
) -> (String, String) {
let wts = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let wts_str = wts.to_string();
params.insert("wts".to_string(), wts_str.clone());
let mixin_key = self.mixin_key();
let query = params
.iter()
.map(|(k, v)| {
let filtered: String =
v.chars().filter(|c| !"!'()*".contains(*c)).collect();
format!("{}={}", k, urlencoding::encode(&filtered))
})
.collect::<Vec<_>>()
.join("&");
let w_rid = format!("{:x}", md5::compute(format!("{}{}", query, mixin_key)));
(wts_str, w_rid)
}
}
pub struct VideoRequestParamBuilder {
bvid: Option<String>,
cid: Option<String>,
qn: Quality,
fnval: Fnval,
fourk: bool,
fnver: Option<String>,
}
impl Default for VideoRequestParamBuilder {
fn default() -> Self {
Self {
bvid: None,
cid: None,
qn: Quality::FHD1080P, fnval: Fnval::DashFull, fourk: true,
fnver: None, }
}
}
impl VideoRequestParamBuilder {
pub async fn new(
bvid: &str,
bili_client: &BiliClient,
) -> Result<Self> {
let (_, _, cid) = get_basic_video_info(bvid, Some(&bili_client)).await?;
Ok(Self {
bvid: Some(bvid.into()),
cid: Some(cid),
..Default::default()
})
}
pub fn qn(
mut self,
qn: Quality,
) -> Self {
self.qn = qn;
self
}
pub fn fnval(
mut self,
fnval: Fnval,
) -> Self {
self.fnval = fnval;
self
}
pub fn allow_4k(mut self) -> Self {
self.fourk = true;
self
}
pub fn fnver(
mut self,
v: impl Into<String>,
) -> Self {
self.fnver = Some(v.into());
self
}
pub async fn build(
self,
bili_client: &BiliClient,
) -> Result<VideoRequestParam> {
let bvid = self.bvid.ok_or(Error::Build("bvid".into()))?;
let cid = self.cid.ok_or(Error::Build("cid".into()))?;
let (img_key, sub_key) = get_wbi_keys(bili_client).await?;
let wbi = Wbi::new(img_key, sub_key);
let mut params = BTreeMap::new();
params.insert("bvid".to_string(), bvid.clone());
params.insert("cid".to_string(), cid.clone());
params.insert("qn".to_string(), (self.qn as u32).to_string());
params.insert("fnval".to_string(), (self.fnval as u32).to_string());
params.insert("fourk".to_string(), (self.fourk as u8).to_string());
let (wts, w_rid) = wbi.sign(&mut params);
Ok(VideoRequestParam {
bvid,
cid,
qn: self.qn,
fnval: self.fnval,
fourk: self.fourk,
fnver: self.fnver.unwrap_or_else(|| "0".into()),
wts,
w_rid,
})
}
}