mirror of
https://github.com/amtoaer/bili-sync.git
synced 2026-09-08 00:47:22 +08:00
feat: 为下载视频接口加入 wbi 签名 (#143)
This commit is contained in:
@@ -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),
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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?;
|
||||||
|
|||||||
@@ -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 { .. })));
|
||||||
|
|||||||
@@ -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()?
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user