feat: 更换部分 API,重构 wbi 签名实现,增加额外风控检测 (#503)

This commit is contained in:
ᴀᴍᴛᴏᴀᴇʀ
2025-10-15 02:01:41 +08:00
committed by GitHub
parent 84d353365a
commit ff6db0ad97
12 changed files with 179 additions and 237 deletions

14
Cargo.lock generated
View File

@@ -145,12 +145,6 @@ version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
[[package]]
name = "assert_matches"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9"
[[package]]
name = "async-compression"
version = "0.4.11"
@@ -354,7 +348,6 @@ version = "2.7.0"
dependencies = [
"anyhow",
"arc-swap",
"assert_matches",
"async-stream",
"async-tempfile",
"axum",
@@ -365,7 +358,6 @@ dependencies = [
"chrono",
"clap",
"cookie",
"cow-utils",
"dashmap",
"dirs",
"enum_dispatch",
@@ -711,12 +703,6 @@ version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
[[package]]
name = "cow-utils"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "417bef24afe1460300965a25ff4a24b8b45ad011948302ec221e8a0a81eb2c79"
[[package]]
name = "cpufeatures"
version = "0.2.12"

View File

@@ -17,9 +17,8 @@ bili_sync_migration = { path = "crates/bili_sync_migration" }
anyhow = { version = "1.0.100", features = ["backtrace"] }
arc-swap = { version = "1.7.1", features = ["serde"] }
assert_matches = "1.5.0"
async-stream = "0.3.6"
async-tempfile = {version = "0.7.0", features = ["uuid"]}
async-tempfile = { version = "0.7.0", features = ["uuid"] }
async-trait = "0.1.89"
axum = { version = "0.8.6", features = ["macros", "ws"] }
base64 = "0.22.1"
@@ -27,7 +26,6 @@ built = { version = "0.7.7", features = ["git2", "chrono"] }
chrono = { version = "0.4.42", features = ["serde"] }
clap = { version = "4.5.48", features = ["env", "string"] }
cookie = "0.18.1"
cow-utils = "0.1.3"
dashmap = "6.1.0"
derivative = "2.2.0"
dirs = "6.0.0"

View File

@@ -21,7 +21,6 @@ bili_sync_migration = { workspace = true }
chrono = { workspace = true }
clap = { workspace = true }
cookie = { workspace = true }
cow-utils = { workspace = true }
dashmap = { workspace = true }
dirs = { workspace = true }
enum_dispatch = { workspace = true }
@@ -59,9 +58,6 @@ ua_generator = { workspace = true }
uuid = { workspace = true }
validator = { workspace = true }
[dev-dependencies]
assert_matches = { workspace = true }
[build-dependencies]
built = { workspace = true }
git2 = { workspace = true }

View File

@@ -7,8 +7,7 @@ use reqwest::Method;
use serde::Deserialize;
use serde_json::Value;
use crate::bilibili::credential::encoded_query;
use crate::bilibili::{BiliClient, MIXIN_KEY, Validate, VideoInfo};
use crate::bilibili::{BiliClient, Validate, VideoInfo};
#[derive(PartialEq, Eq, Hash, Clone, Debug, Default, Copy)]
pub enum CollectionType {
@@ -139,46 +138,35 @@ impl<'a> Collection<'a> {
}
async fn get_videos(&self, page: i32) -> Result<Value> {
let page = page.to_string();
let (url, query) = match self.collection.collection_type {
CollectionType::Series => (
"https://api.bilibili.com/x/series/archives",
encoded_query(
vec![
("mid", self.collection.mid.as_str()),
("series_id", self.collection.sid.as_str()),
("only_normal", "true"),
("sort", "desc"),
("pn", page.as_str()),
("ps", "30"),
],
MIXIN_KEY.load().as_deref(),
),
),
CollectionType::Season => (
"https://api.bilibili.com/x/polymer/web-space/seasons_archives_list",
encoded_query(
vec![
("mid", self.collection.mid.as_str()),
("season_id", self.collection.sid.as_str()),
("sort_reverse", "true"),
("page_num", page.as_str()),
("page_size", "30"),
],
MIXIN_KEY.load().as_deref(),
),
),
let req = match self.collection.collection_type {
CollectionType::Series => self
.client
.request(Method::GET, "https://api.bilibili.com/x/series/archives")
.await
.query(&[("pn", page)])
.query(&[
("mid", self.collection.mid.as_str()),
("series_id", self.collection.sid.as_str()),
("only_normal", "true"),
("sort", "desc"),
("ps", "30"),
]),
CollectionType::Season => self
.client
.request(
Method::GET,
"https://api.bilibili.com/x/polymer/web-space/seasons_archives_list",
)
.await
.query(&[("page_num", page)])
.query(&[
("mid", self.collection.mid.as_str()),
("season_id", self.collection.sid.as_str()),
("sort_reverse", "true"),
("page_size", "30"),
]),
};
self.client
.request(Method::GET, url)
.await
.query(&query)
.send()
.await?
.error_for_status()?
.json::<Value>()
.await?
.validate()
req.send().await?.error_for_status()?.json::<Value>().await?.validate()
}
pub fn into_video_stream(self) -> impl Stream<Item = Result<VideoInfo>> + 'a {

View File

@@ -1,9 +1,7 @@
use std::borrow::Cow;
use std::collections::HashSet;
use anyhow::{Context, Result, bail, ensure};
use cookie::Cookie;
use cow_utils::CowUtils;
use regex::Regex;
use reqwest::{Method, header};
use rsa::pkcs8::DecodePublicKey;
@@ -30,17 +28,13 @@ pub struct Credential {
#[derive(Debug, Deserialize)]
pub struct WbiImg {
img_url: String,
sub_url: String,
pub(crate) img_url: String,
pub(crate) sub_url: String,
}
impl From<WbiImg> for Option<String> {
/// 尝试将 WbiImg 转换成 mixin_key
fn from(value: WbiImg) -> Self {
let key = match (
get_filename(value.img_url.as_str()),
get_filename(value.sub_url.as_str()),
) {
impl WbiImg {
pub fn into_mixin_key(self) -> Option<String> {
let key = match (get_filename(self.img_url.as_str()), get_filename(self.sub_url.as_str())) {
(Some(img_key), Some(sub_key)) => img_key.to_string() + sub_key,
_ => return None,
};
@@ -213,47 +207,8 @@ fn get_filename(url: &str) -> Option<&str> {
.map(|(s, _)| s)
}
pub fn encoded_query<'a>(
params: Vec<(&'a str, impl Into<Cow<'a, str>>)>,
mixin_key: Option<impl AsRef<str>>,
) -> Vec<(&'a str, Cow<'a, str>)> {
match mixin_key {
Some(key) => _encoded_query(params, key.as_ref(), chrono::Local::now().timestamp().to_string()),
None => params.into_iter().map(|(k, v)| (k, v.into())).collect(),
}
}
fn _encoded_query<'a>(
params: Vec<(&'a str, impl Into<Cow<'a, str>>)>,
mixin_key: &str,
timestamp: String,
) -> Vec<(&'a str, Cow<'a, str>)> {
let disallowed = ['!', '\'', '(', ')', '*'];
let mut params: Vec<(&'a str, Cow<'a, str>)> = params
.into_iter()
.map(|(k, v)| {
(
k,
match Into::<Cow<'a, str>>::into(v) {
Cow::Borrowed(v) => v.cow_replace(&disallowed[..], ""),
Cow::Owned(v) => v.replace(&disallowed[..], "").into(),
},
)
})
.collect();
params.push(("wts", timestamp.into()));
params.sort_by(|a, b| a.0.cmp(b.0));
let query = serde_urlencoded::to_string(&params)
.expect("fail to encode query")
.replace('+', "%20");
params.push(("w_rid", format!("{:x}", md5::compute(query.clone() + mixin_key)).into()));
params
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use super::*;
#[test]
@@ -283,56 +238,4 @@ mod tests {
"bar=%E4%BA%94%E4%B8%80%E5%9B%9B&baz=1919810&foo=one%20one%20four"
);
}
#[test]
fn test_wbi_key() {
let key = WbiImg {
img_url: "https://i0.hdslb.com/bfs/wbi/7cd084941338484aae1ad9425b84077c.png".to_string(),
sub_url: "https://i0.hdslb.com/bfs/wbi/4932caff0ff746eab6f01bf08b70ac45.png".to_string(),
};
let key = Option::<String>::from(key).expect("fail to convert key");
assert_eq!(key.as_str(), "ea1db124af3c7062474693fa704f4ff8");
// 没有特殊字符
assert_matches!(
&_encoded_query(
vec![("foo", "114"), ("bar", "514"), ("zab", "1919810")],
key.as_str(),
"1702204169".to_string(),
)[..],
[
("bar", Cow::Borrowed(a)),
("foo", Cow::Borrowed(b)),
("wts", Cow::Owned(c)),
("zab", Cow::Borrowed(d)),
("w_rid", Cow::Owned(e)),
] => {
assert_eq!(*a, "514");
assert_eq!(*b, "114");
assert_eq!(c, "1702204169");
assert_eq!(*d, "1919810");
assert_eq!(e, "8f6f2b5b3d485fe1886cec6a0be8c5d4");
}
);
// 有特殊字符
assert_matches!(
&_encoded_query(
vec![("foo", "'1(1)4'"), ("bar", "!5*1!14"), ("zab", "1919810")],
key.as_str(),
"1702204169".to_string(),
)[..],
[
("bar", Cow::Owned(a)),
("foo", Cow::Owned(b)),
("wts", Cow::Owned(c)),
("zab", Cow::Borrowed(d)),
("w_rid", Cow::Owned(e)),
] => {
assert_eq!(a, "5114");
assert_eq!(b, "114");
assert_eq!(c, "1702204169");
assert_eq!(*d, "1919810");
assert_eq!(e, "6a2c86c4b0648ce062ba0dac2de91a85");
}
);
}
}

View File

@@ -6,8 +6,7 @@ use futures::Stream;
use reqwest::Method;
use serde_json::Value;
use crate::bilibili::credential::encoded_query;
use crate::bilibili::{BiliClient, MIXIN_KEY, Validate, VideoInfo};
use crate::bilibili::{BiliClient, MIXIN_KEY, Validate, VideoInfo, WbiSign};
pub struct Dynamic<'a> {
client: &'a BiliClient,
@@ -29,16 +28,15 @@ impl<'a> Dynamic<'a> {
self.client
.request(
Method::GET,
"https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/all",
"https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/space",
)
.await
.query(&encoded_query(
vec![
("host_mid", self.upper_id.as_str()),
("offset", offset.as_deref().unwrap_or("")),
],
MIXIN_KEY.load().as_deref(),
))
.query(&[
("host_mid", self.upper_id.as_str()),
("offset", offset.as_deref().unwrap_or("")),
("type", "video"),
])
.wbi_sign(MIXIN_KEY.load().as_deref())?
.send()
.await?
.error_for_status()?

View File

@@ -38,12 +38,8 @@ impl<'a> Me<'a> {
.client
.request(Method::GET, "https://api.bilibili.com/x/v3/fav/folder/collected/list")
.await
.query(&[
("up_mid", self.mid.as_str()),
("pn", page_num.to_string().as_str()),
("ps", page_size.to_string().as_str()),
("platform", "web"),
])
.query(&[("up_mid", self.mid.as_str()), ("platform", "web")])
.query(&[("pn", page_num), ("ps", page_size)])
.send()
.await?
.error_for_status()?
@@ -59,11 +55,8 @@ impl<'a> Me<'a> {
.client
.request(Method::GET, "https://api.bilibili.com/x/relation/followings")
.await
.query(&[
("vmid", self.mid.as_str()),
("pn", page_num.to_string().as_str()),
("ps", page_size.to_string().as_str()),
])
.query(&[("vmid", self.mid.as_str())])
.query(&[("pn", page_num), ("ps", page_size)])
.send()
.await?
.error_for_status()?

View File

@@ -1,3 +1,4 @@
use std::borrow::Cow;
use std::sync::Arc;
pub use analyzer::{BestStream, FilterOption};
@@ -7,7 +8,7 @@ use chrono::serde::ts_seconds;
use chrono::{DateTime, Utc};
pub use client::{BiliClient, Client};
pub use collection::{Collection, CollectionItem, CollectionType};
pub use credential::Credential;
pub use credential::{Credential, WbiImg};
pub use danmaku::DanmakuOption;
pub use dynamic::Dynamic;
pub use error::BiliError;
@@ -15,6 +16,7 @@ pub use favorite_list::FavoriteList;
use favorite_list::Upper;
pub use me::Me;
use once_cell::sync::Lazy;
use reqwest::RequestBuilder;
pub use submission::Submission;
pub use video::{Dimension, PageInfo, Video};
pub use watch_later::WatchLater;
@@ -54,10 +56,47 @@ impl Validate for serde_json::Value {
_ => bail!("no code or message found"),
};
ensure!(code == 0, BiliError::RequestFailed(code, msg.to_owned()));
ensure!(self["data"]["v_voucher"].is_null(), BiliError::RiskControlOccurred);
Ok(self)
}
}
pub(crate) trait WbiSign {
type Output;
fn wbi_sign(self, mixin_key: Option<impl AsRef<str>>) -> Result<Self::Output>;
}
impl WbiSign for RequestBuilder {
type Output = RequestBuilder;
fn wbi_sign(self, mixin_key: Option<impl AsRef<str>>) -> Result<Self::Output> {
let Some(mixin_key) = mixin_key else {
return Ok(self);
};
let (client, req) = self.build_split();
let mut req = match req {
Ok(req) => req,
Err(e) => return Err(e.into()),
};
sign_request(&mut req, mixin_key.as_ref(), chrono::Utc::now().timestamp())?;
Ok(RequestBuilder::from_parts(client, req))
}
}
fn sign_request(req: &mut reqwest::Request, mixin_key: &str, timestamp: i64) -> Result<()> {
let mut query_pairs = req.url().query_pairs().collect::<Vec<_>>();
let timestamp = timestamp.to_string();
query_pairs.push(("wts".into(), Cow::Borrowed(timestamp.as_str())));
query_pairs.sort_by(|a, b| a.0.cmp(&b.0));
let query_str = serde_urlencoded::to_string(query_pairs)?.replace('+', "%20");
let w_rid = format!("{:x}", md5::compute(query_str + mixin_key));
req.url_mut()
.query_pairs_mut()
.extend_pairs([("w_rid", w_rid), ("wts", timestamp)]);
Ok(())
}
#[derive(Debug, serde::Deserialize)]
#[serde(untagged)]
/// 注意此处的顺序是有要求的,因为对于 untagged 的 enum 来说serde 会按照顺序匹配
@@ -155,9 +194,12 @@ pub enum VideoInfo {
mod tests {
use std::path::Path;
use anyhow::Context;
use futures::StreamExt;
use reqwest::Method;
use super::*;
use crate::bilibili::credential::WbiImg;
use crate::config::VersionedConfig;
use crate::database::setup_database;
use crate::utils::init_logger;
@@ -169,9 +211,7 @@ mod tests {
init_logger("None,bili_sync=debug", None);
let bili_client = BiliClient::new();
// 请求 UP 主视频必须要获取 mixin key使用 key 计算请求参数的签名,否则直接提示权限不足返回空
let Ok(Some(mixin_key)) = bili_client.wbi_img().await.map(|wbi_img| wbi_img.into()) else {
panic!("获取 mixin key 失败");
};
let mixin_key = bili_client.wbi_img().await?.into_mixin_key().context("no mixin key")?;
set_global_mixin_key(mixin_key);
let collection = Collection::new(
&bili_client,
@@ -236,9 +276,7 @@ mod tests {
#[tokio::test]
async fn test_subtitle_parse() -> Result<()> {
let bili_client = BiliClient::new();
let Ok(Some(mixin_key)) = bili_client.wbi_img().await.map(|wbi_img| wbi_img.into()) else {
panic!("获取 mixin key 失败");
};
let mixin_key = bili_client.wbi_img().await?.into_mixin_key().context("no mixin key")?;
set_global_mixin_key(mixin_key);
let video = Video::new(&bili_client, "BV1gLfnY8E6D".to_string());
let pages = video.get_pages().await?;
@@ -259,9 +297,7 @@ mod tests {
async fn test_upower_parse() -> Result<()> {
VersionedConfig::init_for_test(&setup_database(Path::new("./test.sqlite")).await?).await?;
let bili_client = BiliClient::new();
let Ok(Some(mixin_key)) = bili_client.wbi_img().await.map(|wbi_img| wbi_img.into()) else {
panic!("获取 mixin key 失败");
};
let mixin_key = bili_client.wbi_img().await?.into_mixin_key().context("no mixin key")?;
set_global_mixin_key(mixin_key);
for (bvid, (upower_exclusive, upower_play)) in [
("BV1HxXwYEEqt", (true, false)), // 充电专享且无权观看
@@ -289,9 +325,7 @@ mod tests {
async fn test_ep_parse() -> Result<()> {
VersionedConfig::init_for_test(&setup_database(Path::new("./test.sqlite")).await?).await?;
let bili_client = BiliClient::new();
let Ok(Some(mixin_key)) = bili_client.wbi_img().await.map(|wbi_img| wbi_img.into()) else {
panic!("获取 mixin key 失败");
};
let mixin_key = bili_client.wbi_img().await?.into_mixin_key().context("no mixin key")?;
set_global_mixin_key(mixin_key);
for (bvid, redirect_is_none) in [
("BV1SF411g796", false), // EP
@@ -307,4 +341,56 @@ mod tests {
}
Ok(())
}
#[test]
fn test_wbi_key() -> Result<()> {
let key = WbiImg {
img_url: "https://i0.hdslb.com/bfs/wbi/7cd084941338484aae1ad9425b84077c.png".to_string(),
sub_url: "https://i0.hdslb.com/bfs/wbi/4932caff0ff746eab6f01bf08b70ac45.png".to_string(),
};
let key = key.into_mixin_key().context("no mixin key")?;
assert_eq!(key.as_str(), "ea1db124af3c7062474693fa704f4ff8");
let client = Client::new();
let mut req = client
.request(Method::GET, "https://www.baidu.com/", None)
.query(&[("foo", "114"), ("bar", "514")])
.query(&[("zab", "1919810")])
.build()?;
sign_request(&mut req, key.as_str(), 1702204169).unwrap();
let query: Vec<_> = req.url().query_pairs().collect();
assert_eq!(
query,
vec![
("foo".into(), "114".into()),
("bar".into(), "514".into()),
("zab".into(), "1919810".into()),
("w_rid".into(), "8f6f2b5b3d485fe1886cec6a0be8c5d4".into()),
("wts".into(), "1702204169".into()),
]
);
let key = WbiImg {
img_url: "https://i0.hdslb.com/bfs/wbi/7cd084941338484aae1ad9425b84077c.png".to_string(),
sub_url: "https://i0.hdslb.com/bfs/wbi/4932caff0ff746eab6f01bf08b70ac45.png".to_string(),
};
let key = key.into_mixin_key().context("no mixin key")?;
let mut req = client
.request(Method::GET, "https://www.baidu.com/", None)
.query(&[("mid", "11997177"), ("token", "")])
.query(&[("platform", "web"), ("web_location", "1550101")])
.build()?;
sign_request(&mut req, key.as_str(), 1703513649).unwrap();
let query: Vec<_> = req.url().query_pairs().collect();
assert_eq!(
query,
vec![
("mid".into(), "11997177".into()),
("token".into(), "".into()),
("platform".into(), "web".into()),
("web_location".into(), "1550101".into()),
("w_rid".into(), "7d4428b3f2f9ee2811e116ec6fd41a4f".into()),
("wts".into(), "1703513649".into()),
]
);
Ok(())
}
}

View File

@@ -4,9 +4,8 @@ use futures::Stream;
use reqwest::Method;
use serde_json::Value;
use crate::bilibili::credential::encoded_query;
use crate::bilibili::favorite_list::Upper;
use crate::bilibili::{BiliClient, Dynamic, MIXIN_KEY, Validate, VideoInfo};
use crate::bilibili::{BiliClient, Dynamic, MIXIN_KEY, Validate, VideoInfo, WbiSign};
pub struct Submission<'a> {
client: &'a BiliClient,
pub upper_id: String,
@@ -42,18 +41,16 @@ impl<'a> Submission<'a> {
self.client
.request(Method::GET, "https://api.bilibili.com/x/space/wbi/arc/search")
.await
.query(&encoded_query(
vec![
("mid", self.upper_id.as_str()),
("order", "pubdate"),
("order_avoided", "true"),
("platform", "web"),
("web_location", "1550101"),
("pn", page.to_string().as_str()),
("ps", "30"),
],
MIXIN_KEY.load().as_deref(),
))
.query(&[
("mid", self.upper_id.as_str()),
("order", "pubdate"),
("order_avoided", "true"),
("platform", "web"),
("web_location", "1550101"),
("ps", "30"),
])
.query(&[("pn", page)])
.wbi_sign(MIXIN_KEY.load().as_deref())?
.send()
.await?
.error_for_status()?

View File

@@ -6,10 +6,9 @@ use reqwest::Method;
use crate::bilibili::analyzer::PageAnalyzer;
use crate::bilibili::client::BiliClient;
use crate::bilibili::credential::encoded_query;
use crate::bilibili::danmaku::{DanmakuElem, DanmakuWriter, DmSegMobileReply};
use crate::bilibili::subtitle::{SubTitle, SubTitleBody, SubTitleInfo, SubTitlesInfo};
use crate::bilibili::{MIXIN_KEY, Validate, VideoInfo};
use crate::bilibili::{MIXIN_KEY, Validate, VideoInfo, WbiSign};
pub struct Video<'a> {
client: &'a BiliClient,
@@ -43,9 +42,10 @@ impl<'a> Video<'a> {
pub async fn get_view_info(&self) -> Result<VideoInfo> {
let mut res = self
.client
.request(Method::GET, "https://api.bilibili.com/x/web-interface/view")
.request(Method::GET, "https://api.bilibili.com/x/web-interface/wbi/view")
.await
.query(&[("bvid", &self.bvid)])
.wbi_sign(MIXIN_KEY.load().as_deref())?
.send()
.await?
.error_for_status()?
@@ -55,7 +55,7 @@ impl<'a> Video<'a> {
Ok(serde_json::from_value(res["data"].take())?)
}
#[allow(dead_code)]
#[cfg(test)]
pub async fn get_pages(&self) -> Result<Vec<PageInfo>> {
let mut res = self
.client
@@ -105,9 +105,10 @@ impl<'a> Video<'a> {
async fn get_danmaku_segment(&self, page: &PageInfo, segment_idx: i64) -> Result<Vec<DanmakuElem>> {
let mut res = self
.client
.request(Method::GET, "http://api.bilibili.com/x/v2/dm/web/seg.so")
.request(Method::GET, "https://api.bilibili.com/x/v2/dm/wbi/web/seg.so")
.await
.query(&[("type", 1), ("oid", page.cid), ("segment_index", segment_idx)])
.wbi_sign(MIXIN_KEY.load().as_deref())?
.send()
.await?
.error_for_status()?;
@@ -127,17 +128,15 @@ impl<'a> Video<'a> {
.client
.request(Method::GET, "https://api.bilibili.com/x/player/wbi/playurl")
.await
.query(&encoded_query(
vec![
("bvid", self.bvid.as_str()),
("cid", page.cid.to_string().as_str()),
("qn", "127"),
("otype", "json"),
("fnval", "4048"),
("fourk", "1"),
],
MIXIN_KEY.load().as_deref(),
))
.query(&[
("bvid", self.bvid.as_str()),
("qn", "127"),
("otype", "json"),
("fnval", "4048"),
("fourk", "1"),
])
.query(&[("cid", page.cid)])
.wbi_sign(MIXIN_KEY.load().as_deref())?
.send()
.await?
.error_for_status()?
@@ -152,10 +151,9 @@ impl<'a> Video<'a> {
.client
.request(Method::GET, "https://api.bilibili.com/x/player/wbi/v2")
.await
.query(&encoded_query(
vec![("cid", &page.cid.to_string()), ("bvid", &self.bvid)],
MIXIN_KEY.load().as_deref(),
))
.query(&[("bvid", self.bvid.as_str())])
.query(&[("cid", page.cid)])
.wbi_sign(MIXIN_KEY.load().as_deref())?
.send()
.await?
.error_for_status()?

View File

@@ -4,7 +4,7 @@ use sea_orm::DatabaseConnection;
use tokio::time;
use crate::adapter::VideoSource;
use crate::bilibili::{self, BiliClient};
use crate::bilibili::{self, BiliClient, WbiImg};
use crate::config::VersionedConfig;
use crate::utils::model::get_enabled_video_sources;
use crate::utils::task_notifier::TASK_STATUS_NOTIFIER;
@@ -22,7 +22,7 @@ pub async fn video_downloader(connection: DatabaseConnection, bili_client: Arc<B
error!("配置检查失败,跳过本轮执行:\n{:#}", e);
break 'inner;
}
match bili_client.wbi_img().await.map(|wbi_img| wbi_img.into()) {
match bili_client.wbi_img().await.map(WbiImg::into_mixin_key) {
Ok(Some(mixin_key)) => bilibili::set_global_mixin_key(mixin_key),
Ok(_) => {
error!("解析 mixin key 失败,等待下一轮执行");

View File

@@ -420,9 +420,8 @@
</Tooltip.Trigger>
<Tooltip.Content>
<p class="text-xs">
只有使用动态 API
才能拉取到动态视频,但该接口不提供筛选参数,需要拉取全部类型的动态后在本地筛选出视频。<br
/>这在扫描时会获取到较多无效数据并增加请求次数,可根据实际情况酌情选择,推荐仅在
只有使用动态 API 才能拉取到动态视频,但该接口不提供分页参数,每次请求只能拉取 12
条视频。<br />这会一定程度上增加请求次数,用户可根据实际情况酌情选择,推荐仅在
UP 主有较多动态视频时开启。
</p>
</Tooltip.Content>