feat: 为下载视频接口加入 wbi 签名 (#143)

This commit is contained in:
ᴀᴍᴛᴏᴀᴇʀ
2024-07-21 18:47:09 +08:00
committed by GitHub
parent 2ffe432f37
commit 86e858082d
8 changed files with 73 additions and 120 deletions
+1 -2
View File
@@ -20,12 +20,11 @@ use crate::utils::status::Status;
pub async fn collection_from<'a>( pub async fn collection_from<'a>(
collection_item: &'a CollectionItem, collection_item: &'a CollectionItem,
mixin_key: &'a str,
path: &Path, path: &Path,
bili_client: &'a BiliClient, bili_client: &'a BiliClient,
connection: &DatabaseConnection, connection: &DatabaseConnection,
) -> Result<(Box<dyn VideoListModel>, Pin<Box<dyn Stream<Item = VideoInfo> + 'a>>)> { ) -> Result<(Box<dyn VideoListModel>, Pin<Box<dyn Stream<Item = VideoInfo> + 'a>>)> {
let collection = Collection::new(bili_client, collection_item, mixin_key); let collection = Collection::new(bili_client, collection_item);
let collection_info = collection.get_info().await?; let collection_info = collection.get_info().await?;
collection::Entity::insert(collection::ActiveModel { collection::Entity::insert(collection::ActiveModel {
s_id: Set(collection_info.sid), s_id: Set(collection_info.sid),
+3 -11
View File
@@ -17,13 +17,8 @@ use watch_later::watch_later_from;
use crate::bilibili::{BiliClient, CollectionItem, VideoInfo}; use crate::bilibili::{BiliClient, CollectionItem, VideoInfo};
pub enum Args<'a> { pub enum Args<'a> {
Favorite { Favorite { fid: &'a str },
fid: &'a str, Collection { collection_item: &'a CollectionItem },
},
Collection {
collection_item: &'a CollectionItem,
mixin_key: &'a str,
},
WatchLater, WatchLater,
} }
@@ -35,10 +30,7 @@ pub async fn video_list_from<'a>(
) -> Result<(Box<dyn VideoListModel>, Pin<Box<dyn Stream<Item = VideoInfo> + 'a>>)> { ) -> Result<(Box<dyn VideoListModel>, Pin<Box<dyn Stream<Item = VideoInfo> + 'a>>)> {
match args { match args {
Args::Favorite { fid } => favorite_from(fid, path, bili_client, connection).await, Args::Favorite { fid } => favorite_from(fid, path, bili_client, connection).await,
Args::Collection { Args::Collection { collection_item } => collection_from(collection_item, path, bili_client, connection).await,
collection_item,
mixin_key,
} => collection_from(collection_item, mixin_key, path, bili_client, connection).await,
Args::WatchLater => watch_later_from(path, bili_client, connection).await, Args::WatchLater => watch_later_from(path, bili_client, connection).await,
} }
} }
-9
View File
@@ -87,15 +87,6 @@ impl BiliClient {
CONFIG.save() CONFIG.save()
} }
/// 检查凭据是否已设置且有效
pub async fn is_login(&self) -> Result<()> {
let credential = CONFIG.credential.load();
let Some(credential) = credential.as_deref() else {
bail!("no credential found");
};
credential.is_login(&self.client).await
}
/// 获取 wbi img,用于生成请求签名 /// 获取 wbi img,用于生成请求签名
pub async fn wbi_img(&self) -> Result<WbiImg> { pub async fn wbi_img(&self) -> Result<WbiImg> {
let credential = CONFIG.credential.load(); let credential = CONFIG.credential.load();
+7 -25
View File
@@ -1,9 +1,9 @@
#![allow(dead_code)] #![allow(dead_code)]
use std::borrow::Cow;
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use anyhow::{bail, Result}; use anyhow::Result;
use arc_swap::access::Access;
use async_stream::stream; use async_stream::stream;
use futures::Stream; use futures::Stream;
use reqwest::Method; use reqwest::Method;
@@ -11,7 +11,7 @@ use serde::Deserialize;
use serde_json::Value; use serde_json::Value;
use crate::bilibili::credential::encoded_query; use crate::bilibili::credential::encoded_query;
use crate::bilibili::{BiliClient, Validate, VideoInfo}; use crate::bilibili::{BiliClient, Validate, VideoInfo, MIXIN_KEY};
#[derive(PartialEq, Eq, Hash, Clone, Debug)] #[derive(PartialEq, Eq, Hash, Clone, Debug)]
pub enum CollectionType { pub enum CollectionType {
@@ -58,7 +58,6 @@ pub struct CollectionItem {
pub struct Collection<'a> { pub struct Collection<'a> {
client: &'a BiliClient, client: &'a BiliClient,
collection: &'a CollectionItem, collection: &'a CollectionItem,
mixin_key: Cow<'a, str>,
} }
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
@@ -97,24 +96,8 @@ impl<'de> Deserialize<'de> for CollectionInfo {
} }
impl<'a> Collection<'a> { impl<'a> Collection<'a> {
pub async fn build(client: &'a BiliClient, collection: &'a CollectionItem) -> Result<Self> { pub fn new(client: &'a BiliClient, collection: &'a CollectionItem) -> Self {
let wbi_img = client.wbi_img().await?; Self { client, collection }
let Some(mixin_key) = wbi_img.into_mixin_key() else {
bail!("failed to get mixin key");
};
Ok(Self {
client,
collection,
mixin_key: Cow::Owned(mixin_key),
})
}
pub fn new(client: &'a BiliClient, collection: &'a CollectionItem, mixin_key: &'a str) -> Self {
Self {
client,
collection,
mixin_key: Cow::Borrowed(mixin_key),
}
} }
pub async fn get_info(&self) -> Result<CollectionInfo> { pub async fn get_info(&self) -> Result<CollectionInfo> {
@@ -140,7 +123,6 @@ impl<'a> Collection<'a> {
async fn get_videos(&self, page: i32) -> Result<Value> { async fn get_videos(&self, page: i32) -> Result<Value> {
let page = page.to_string(); let page = page.to_string();
let mixin_key = self.mixin_key.as_ref();
let (url, query) = match self.collection.collection_type { let (url, query) = match self.collection.collection_type {
CollectionType::Series => ( CollectionType::Series => (
"https://api.bilibili.com/x/series/archives", "https://api.bilibili.com/x/series/archives",
@@ -153,7 +135,7 @@ impl<'a> Collection<'a> {
("pn", page.as_str()), ("pn", page.as_str()),
("ps", "30"), ("ps", "30"),
], ],
mixin_key, MIXIN_KEY.load().as_ref().unwrap(),
), ),
), ),
CollectionType::Season => ( CollectionType::Season => (
@@ -166,7 +148,7 @@ impl<'a> Collection<'a> {
("page_num", page.as_str()), ("page_num", page.as_str()),
("page_size", "30"), ("page_size", "30"),
], ],
mixin_key, MIXIN_KEY.load().as_ref().unwrap(),
), ),
), ),
}; };
@@ -67,24 +67,6 @@ impl Credential {
res["data"]["refresh"].as_bool().ok_or(anyhow!("check refresh failed")) res["data"]["refresh"].as_bool().ok_or(anyhow!("check refresh failed"))
} }
/// 需要使用一个需要鉴权的接口来检查是否登录
/// 此处使用查看用户状态数的接口,该接口返回内容少,请求成本低
pub async fn is_login(&self, client: &Client) -> Result<()> {
client
.request(
Method::GET,
"https://api.bilibili.com/x/web-interface/nav/stat",
Some(self),
)
.send()
.await?
.error_for_status()?
.json::<serde_json::Value>()
.await?
.validate()?;
Ok(())
}
pub async fn refresh(&self, client: &Client) -> Result<Self> { pub async fn refresh(&self, client: &Client) -> Result<Self> {
let correspond_path = Self::get_correspond_path(); let correspond_path = Self::get_correspond_path();
let csrf = self.get_refresh_csrf(client, correspond_path).await?; let csrf = self.get_refresh_csrf(client, correspond_path).await?;
+11 -1
View File
@@ -1,5 +1,8 @@
use std::sync::Arc;
pub use analyzer::{BestStream, FilterOption}; pub use analyzer::{BestStream, FilterOption};
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use arc_swap::ArcSwapOption;
use chrono::serde::ts_seconds; use chrono::serde::ts_seconds;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
pub use client::{BiliClient, Client}; pub use client::{BiliClient, Client};
@@ -9,6 +12,7 @@ pub use danmaku::DanmakuOption;
pub use error::BiliError; pub use error::BiliError;
pub use favorite_list::FavoriteList; pub use favorite_list::FavoriteList;
use favorite_list::Upper; use favorite_list::Upper;
use once_cell::sync::Lazy;
pub use video::{Dimension, PageInfo, Video}; pub use video::{Dimension, PageInfo, Video};
pub use watch_later::WatchLater; pub use watch_later::WatchLater;
@@ -22,6 +26,12 @@ mod favorite_list;
mod video; mod video;
mod watch_later; mod watch_later;
static MIXIN_KEY: Lazy<ArcSwapOption<String>> = Lazy::new(Default::default);
pub(crate) fn set_global_mixin_key(key: String) {
MIXIN_KEY.store(Some(Arc::new(key)));
}
pub(crate) trait Validate { pub(crate) trait Validate {
type Output; type Output;
@@ -130,7 +140,7 @@ mod tests {
sid: "387214".to_string(), sid: "387214".to_string(),
collection_type: CollectionType::Series, collection_type: CollectionType::Series,
}; };
let collection = Collection::build(&bili_client, &collection_item).await.unwrap(); let collection = Collection::new(&bili_client, &collection_item);
let stream = collection.into_simple_video_stream(); let stream = collection.into_simple_video_stream();
pin_mut!(stream); pin_mut!(stream);
assert!(matches!(stream.next().await, Some(VideoInfo::Simple { .. }))); assert!(matches!(stream.next().await, Some(VideoInfo::Simple { .. })));
+7 -2
View File
@@ -4,6 +4,8 @@ use futures::TryStreamExt;
use prost::Message; use prost::Message;
use reqwest::Method; use reqwest::Method;
use super::credential::encoded_query;
use super::MIXIN_KEY;
use crate::bilibili::analyzer::PageAnalyzer; use crate::bilibili::analyzer::PageAnalyzer;
use crate::bilibili::client::BiliClient; use crate::bilibili::client::BiliClient;
use crate::bilibili::danmaku::{DanmakuElem, DanmakuWriter, DmSegMobileReply}; use crate::bilibili::danmaku::{DanmakuElem, DanmakuWriter, DmSegMobileReply};
@@ -140,14 +142,17 @@ impl<'a> Video<'a> {
let mut res = self let mut res = self
.client .client
.request(Method::GET, "https://api.bilibili.com/x/player/wbi/playurl") .request(Method::GET, "https://api.bilibili.com/x/player/wbi/playurl")
.query(&[ .query(&encoded_query(
vec![
("avid", self.aid.as_str()), ("avid", self.aid.as_str()),
("cid", page.cid.to_string().as_str()), ("cid", page.cid.to_string().as_str()),
("qn", "127"), ("qn", "127"),
("otype", "json"), ("otype", "json"),
("fnval", "4048"), ("fnval", "4048"),
("fourk", "1"), ("fourk", "1"),
]) ],
MIXIN_KEY.load().as_ref().unwrap(),
))
.send() .send()
.await? .await?
.error_for_status()? .error_for_status()?
+16 -24
View File
@@ -31,16 +31,22 @@ async fn main() {
let bili_client = BiliClient::new(); let bili_client = BiliClient::new();
let watch_later_config = &CONFIG.watch_later; let watch_later_config = &CONFIG.watch_later;
loop { loop {
if let Err(e) = bili_client.is_login().await { 'inner: {
error!("检查登录状态时遇到错误:{e},等待下一轮执行"); match bili_client.wbi_img().await.map(|wbi_img| wbi_img.into_mixin_key()) {
time::sleep(Duration::from_secs(CONFIG.interval)).await; Ok(Some(mixin_key)) => bilibili::set_global_mixin_key(mixin_key),
continue; Ok(_) => {
error!("获取 mixin key 失败,无法进行 wbi 签名,等待下一轮执行");
break 'inner;
} }
Err(e) => {
error!("获取 mixin key 时遇到错误:{e},等待下一轮执行");
break 'inner;
}
};
if anchor != chrono::Local::now().date_naive() { if anchor != chrono::Local::now().date_naive() {
if let Err(e) = bili_client.check_refresh().await { if let Err(e) = bili_client.check_refresh().await {
error!("检查刷新 Credential 遇到错误:{e},等待下一轮执行"); error!("检查刷新 Credential 遇到错误:{e},等待下一轮执行");
time::sleep(Duration::from_secs(CONFIG.interval)).await; break 'inner;
continue;
} }
anchor = chrono::Local::now().date_naive(); anchor = chrono::Local::now().date_naive();
} }
@@ -50,28 +56,13 @@ async fn main() {
} }
} }
info!("所有收藏夹处理完毕"); info!("所有收藏夹处理完毕");
match bili_client.wbi_img().await.map(|wbi_img| wbi_img.into_mixin_key()) {
Ok(Some(mixin_key)) => {
for (collection_item, path) in &CONFIG.collection_list { for (collection_item, path) in &CONFIG.collection_list {
if let Err(e) = process_video_list( if let Err(e) =
Args::Collection { process_video_list(Args::Collection { collection_item }, &bili_client, path, &connection).await
collection_item,
mixin_key: &mixin_key,
},
&bili_client,
path,
&connection,
)
.await
{ {
error!("处理合集 {collection_item:?} 时遇到非预期的错误:{e}"); error!("处理合集 {collection_item:?} 时遇到非预期的错误:{e}");
} }
} }
}
_ => {
error!("获取 mixin key 失败,无法进行 wbi 签名,跳过本轮合集处理");
}
}
info!("所有合集处理完毕"); info!("所有合集处理完毕");
if watch_later_config.enabled { if watch_later_config.enabled {
if let Err(e) = if let Err(e) =
@@ -82,6 +73,7 @@ async fn main() {
} }
info!("稍后再看处理完毕"); info!("稍后再看处理完毕");
info!("本轮任务执行完毕,等待下一轮执行"); info!("本轮任务执行完毕,等待下一轮执行");
tokio::time::sleep(std::time::Duration::from_secs(CONFIG.interval)).await; }
time::sleep(Duration::from_secs(CONFIG.interval)).await;
} }
} }