feat: 在接口层级返回特定错误

This commit is contained in:
amtoaer
2024-04-02 00:00:44 +08:00
parent f5bc882122
commit beb3634e33
8 changed files with 120 additions and 42 deletions
Generated
+1
View File
@@ -425,6 +425,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"strum 0.26.2", "strum 0.26.2",
"thiserror",
"tokio", "tokio",
"toml", "toml",
] ]
+1
View File
@@ -36,6 +36,7 @@ sea-orm = { version = "0.12", features = [
serde = { version = "1.0.197", features = ["derive"] } serde = { version = "1.0.197", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
strum = { version = "0.26", features = ["derive"] } strum = { version = "0.26", features = ["derive"] }
thiserror = "1.0.58"
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
toml = "0.8.12" toml = "0.8.12"
+15 -13
View File
@@ -1,9 +1,10 @@
use std::sync::Arc; use std::sync::Arc;
use anyhow::{anyhow, bail, Result}; use anyhow::{anyhow, bail, Result};
use log::error;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::bilibili::error::BiliError;
pub struct PageAnalyzer { pub struct PageAnalyzer {
info: serde_json::Value, info: serde_json::Value,
} }
@@ -138,17 +139,26 @@ impl PageAnalyzer {
fn streams(&mut self, filter_option: &FilterOption) -> Result<Vec<Stream>> { fn streams(&mut self, filter_option: &FilterOption) -> Result<Vec<Stream>> {
if self.is_flv_stream() { if self.is_flv_stream() {
return Ok(vec![Stream::Flv( return Ok(vec![Stream::Flv(
self.info["durl"][0]["url"].as_str().unwrap().to_string(), self.info["durl"][0]["url"]
.as_str()
.ok_or(anyhow!("invalid flv stream"))?
.to_string(),
)]); )]);
} }
if self.is_html5_mp4_stream() { if self.is_html5_mp4_stream() {
return Ok(vec![Stream::Html5Mp4( return Ok(vec![Stream::Html5Mp4(
self.info["durl"][0]["url"].as_str().unwrap().to_string(), self.info["durl"][0]["url"]
.as_str()
.ok_or(anyhow!("invalid html5 mp4 stream"))?
.to_string(),
)]); )]);
} }
if self.is_episode_try_mp4_stream() { if self.is_episode_try_mp4_stream() {
return Ok(vec![Stream::EpositeTryMp4( return Ok(vec![Stream::EpositeTryMp4(
self.info["durl"][0]["url"].as_str().unwrap().to_string(), self.info["durl"][0]["url"]
.as_str()
.ok_or(anyhow!("invalid episode try mp4 stream"))?
.to_string(),
)]); )]);
} }
let mut streams: Vec<Stream> = Vec::new(); let mut streams: Vec<Stream> = Vec::new();
@@ -156,15 +166,7 @@ impl PageAnalyzer {
let audios_data = self.info["dash"]["audio"].take(); let audios_data = self.info["dash"]["audio"].take();
let flac_data = self.info["dash"]["flac"].take(); let flac_data = self.info["dash"]["flac"].take();
let dolby_data = self.info["dash"]["dolby"].take(); let dolby_data = self.info["dash"]["dolby"].take();
for video_data in videos_data for video_data in videos_data.as_array().ok_or(BiliError::RiskControlOccurred)?.iter() {
.as_array()
.ok_or_else(|| -> Result<serde_json::Value> {
error!("video data is not an array: {:?}", self.info);
Err(anyhow!("invalid video data"))
})
.unwrap_or(&Vec::new())
.iter()
{
let video_stream_url = video_data["baseUrl"].as_str().unwrap().to_string(); let video_stream_url = video_data["baseUrl"].as_str().unwrap().to_string();
let video_stream_quality = VideoQuality::from_repr(video_data["id"].as_u64().unwrap() as usize) let video_stream_quality = VideoQuality::from_repr(video_data["id"].as_u64().unwrap() as usize)
.ok_or(anyhow!("invalid video stream quality"))?; .ok_or(anyhow!("invalid video stream quality"))?;
+34 -15
View File
@@ -9,6 +9,7 @@ use rsa::sha2::Sha256;
use rsa::{Oaep, RsaPublicKey}; use rsa::{Oaep, RsaPublicKey};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::error::BiliError;
use crate::bilibili::Client; use crate::bilibili::Client;
#[derive(Default, Debug, Clone, Serialize, Deserialize)] #[derive(Default, Debug, Clone, Serialize, Deserialize)]
@@ -41,8 +42,16 @@ impl Credential {
) )
.send() .send()
.await? .await?
.error_for_status()?
.json::<serde_json::Value>() .json::<serde_json::Value>()
.await?; .await?;
let (code, msg) = match (res["code"].as_u64(), res["message"].as_str()) {
(Some(code), Some(msg)) => (code, msg),
_ => bail!("no code or message found"),
};
if code != 0 {
bail!(BiliError::RequestFailed(code, msg.to_owned()));
}
res["data"]["refresh"].as_bool().ok_or(anyhow!("check refresh failed")) res["data"]["refresh"].as_bool().ok_or(anyhow!("check refresh failed"))
} }
@@ -82,18 +91,13 @@ JNrRuoEUXpabUzGB8QIDAQAB
) )
.header(header::COOKIE, "Domain=.bilibili.com") .header(header::COOKIE, "Domain=.bilibili.com")
.send() .send()
.await?; .await?
if !res.status().is_success() { .error_for_status()?;
return match res.status().as_u16() {
404 => Err(anyhow!("correspond path is wrong or expired")),
_ => Err(anyhow!("get csrf failed")),
};
}
regex_find(r#"<div id="1-name">(.+?)</div>"#, res.text().await?.as_str()) regex_find(r#"<div id="1-name">(.+?)</div>"#, res.text().await?.as_str())
} }
async fn get_new_credential(&self, client: &Client, csrf: &str) -> Result<Credential> { async fn get_new_credential(&self, client: &Client, csrf: &str) -> Result<Credential> {
let res = client let mut res = client
.request( .request(
Method::POST, Method::POST,
"https://passport.bilibili.com/x/passport-login/web/cookie/refresh", "https://passport.bilibili.com/x/passport-login/web/cookie/refresh",
@@ -108,8 +112,19 @@ JNrRuoEUXpabUzGB8QIDAQAB
("source", "main_web"), ("source", "main_web"),
]) ])
.send() .send()
.await?; .await?
let set_cookies = res.headers().get_all(header::SET_COOKIE); .error_for_status()?;
// 必须在 .json 前取出 headers,否则 res 会被消耗
let headers = std::mem::take(res.headers_mut());
let res = res.json::<serde_json::Value>().await?;
let (code, msg) = match (res["code"].as_u64(), res["message"].as_str()) {
(Some(code), Some(msg)) => (code, msg),
_ => bail!("no code or message found"),
};
if code != 0 {
bail!(BiliError::RequestFailed(code, msg.to_owned()));
}
let set_cookies = headers.get_all(header::SET_COOKIE);
let mut credential = Credential { let mut credential = Credential {
buvid3: self.buvid3.clone(), buvid3: self.buvid3.clone(),
..Default::default() ..Default::default()
@@ -132,11 +147,10 @@ JNrRuoEUXpabUzGB8QIDAQAB
_ => unreachable!(), _ => unreachable!(),
} }
} }
let json = res.json::<serde_json::Value>().await?; if !res["data"]["refresh_token"].is_string() {
if !json["data"]["refresh_token"].is_string() {
bail!("refresh_token not found"); bail!("refresh_token not found");
} }
credential.ac_time_value = json["data"]["refresh_token"].as_str().unwrap().to_string(); credential.ac_time_value = res["data"]["refresh_token"].as_str().unwrap().to_string();
Ok(credential) Ok(credential)
} }
@@ -154,10 +168,15 @@ JNrRuoEUXpabUzGB8QIDAQAB
]) ])
.send() .send()
.await? .await?
.error_for_status()?
.json::<serde_json::Value>() .json::<serde_json::Value>()
.await?; .await?;
if res["code"] != 0 { let (code, msg) = match (res["code"].as_u64(), res["message"].as_str()) {
bail!("confirm refresh failed: {}", res["message"]); (Some(code), Some(msg)) => (code, msg),
_ => bail!("no code or message found"),
};
if code != 0 {
bail!(BiliError::RequestFailed(code, msg.to_owned()));
} }
Ok(()) Ok(())
} }
+9
View File
@@ -0,0 +1,9 @@
use thiserror::Error;
#[derive(Error, Debug)]
pub enum BiliError {
#[error("risk control occurred")]
RiskControlOccurred,
#[error("request failed, status code: {0}, message: {1}")]
RequestFailed(u64, String),
}
+29 -6
View File
@@ -3,8 +3,10 @@ use async_stream::stream;
use chrono::serde::ts_seconds; use chrono::serde::ts_seconds;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use futures::Stream; use futures::Stream;
use log::error;
use serde_json::Value; use serde_json::Value;
use crate::bilibili::error::BiliError;
use crate::bilibili::BiliClient; use crate::bilibili::BiliClient;
pub struct FavoriteList<'a> { pub struct FavoriteList<'a> {
client: &'a BiliClient, client: &'a BiliClient,
@@ -53,8 +55,16 @@ impl<'a> FavoriteList<'a> {
.query(&[("media_id", &self.fid)]) .query(&[("media_id", &self.fid)])
.send() .send()
.await? .await?
.error_for_status()?
.json::<serde_json::Value>() .json::<serde_json::Value>()
.await?; .await?;
let (code, msg) = match (res["code"].as_u64(), res["message"].as_str()) {
(Some(code), Some(msg)) => (code, msg),
_ => bail!("no code or message found"),
};
if code != 0 {
bail!(BiliError::RequestFailed(code, msg.to_owned()));
}
Ok(serde_json::from_value(res["data"].take())?) Ok(serde_json::from_value(res["data"].take())?)
} }
@@ -72,10 +82,15 @@ impl<'a> FavoriteList<'a> {
]) ])
.send() .send()
.await? .await?
.error_for_status()?
.json::<serde_json::Value>() .json::<serde_json::Value>()
.await?; .await?;
if res["code"] != 0 { let (code, msg) = match (res["code"].as_u64(), res["message"].as_str()) {
bail!("get favorite videos failed: {}", res["message"]); (Some(code), Some(msg)) => (code, msg),
_ => bail!("no code or message found"),
};
if code != 0 {
bail!(BiliError::RequestFailed(code, msg.to_owned()));
} }
Ok(res) Ok(res)
} }
@@ -85,11 +100,19 @@ impl<'a> FavoriteList<'a> {
stream! { stream! {
let mut page = 1; let mut page = 1;
loop { loop {
let Ok(mut videos) = self.get_videos(page).await else{ let mut videos = match self.get_videos(page).await {
break; Ok(v) => v,
Err(e) => {
error!("failed to get videos of page {}: {}", page, e);
break;
},
}; };
let Ok(videos_info) = serde_json::from_value::<Vec<VideoInfo>>(videos["data"]["medias"].take()) else{ let videos_info = match serde_json::from_value::<Vec<VideoInfo>>(videos["data"]["medias"].take()) {
break; Ok(v) => v,
Err(e) => {
error!("failed to parse videos of page {}: {}", page, e);
break;
},
}; };
for video_info in videos_info.into_iter(){ for video_info in videos_info.into_iter(){
yield video_info; yield video_info;
+7 -6
View File
@@ -1,11 +1,12 @@
mod analyzer;
mod client;
mod credential;
mod favorite_list;
mod video;
pub use analyzer::{AudioQuality, BestStream, FilterOption, PageAnalyzer, VideoCodecs, VideoQuality}; pub use analyzer::{AudioQuality, BestStream, FilterOption, PageAnalyzer, VideoCodecs, VideoQuality};
pub use client::{BiliClient, Client}; pub use client::{BiliClient, Client};
pub use credential::Credential; pub use credential::Credential;
pub use favorite_list::{FavoriteList, FavoriteListInfo, VideoInfo}; pub use favorite_list::{FavoriteList, FavoriteListInfo, VideoInfo};
pub use video::{PageInfo, Video}; pub use video::{PageInfo, Video};
mod analyzer;
mod client;
mod credential;
mod error;
mod favorite_list;
mod video;
+24 -2
View File
@@ -3,6 +3,7 @@ use reqwest::Method;
use crate::bilibili::analyzer::PageAnalyzer; use crate::bilibili::analyzer::PageAnalyzer;
use crate::bilibili::client::BiliClient; use crate::bilibili::client::BiliClient;
use crate::bilibili::error::BiliError;
static MASK_CODE: u64 = 2251799813685247; static MASK_CODE: u64 = 2251799813685247;
static XOR_CODE: u64 = 23442827791579; static XOR_CODE: u64 = 23442827791579;
@@ -55,8 +56,16 @@ impl<'a> Video<'a> {
.query(&[("aid", &self.aid), ("bvid", &self.bvid)]) .query(&[("aid", &self.aid), ("bvid", &self.bvid)])
.send() .send()
.await? .await?
.error_for_status()?
.json::<serde_json::Value>() .json::<serde_json::Value>()
.await?; .await?;
let (code, msg) = match (res["code"].as_u64(), res["message"].as_str()) {
(Some(code), Some(msg)) => (code, msg),
_ => bail!("no code or message found"),
};
if code != 0 {
bail!(BiliError::RequestFailed(code, msg.to_owned()));
}
Ok(serde_json::from_value(res["data"].take())?) Ok(serde_json::from_value(res["data"].take())?)
} }
@@ -67,8 +76,16 @@ impl<'a> Video<'a> {
.query(&[("aid", &self.aid), ("bvid", &self.bvid)]) .query(&[("aid", &self.aid), ("bvid", &self.bvid)])
.send() .send()
.await? .await?
.error_for_status()?
.json::<serde_json::Value>() .json::<serde_json::Value>()
.await?; .await?;
let (code, msg) = match (res["code"].as_u64(), res["message"].as_str()) {
(Some(code), Some(msg)) => (code, msg),
_ => bail!("no code or message found"),
};
if code != 0 {
bail!(BiliError::RequestFailed(code, msg.to_owned()));
}
Ok(serde_json::from_value(res["data"].take())?) Ok(serde_json::from_value(res["data"].take())?)
} }
@@ -86,10 +103,15 @@ impl<'a> Video<'a> {
]) ])
.send() .send()
.await? .await?
.error_for_status()?
.json::<serde_json::Value>() .json::<serde_json::Value>()
.await?; .await?;
if res["code"] != 0 { let (code, msg) = match (res["code"].as_u64(), res["message"].as_str()) {
bail!("get page analyzer failed: {}", res["message"]); (Some(code), Some(msg)) => (code, msg),
_ => bail!("no code or message found"),
};
if code != 0 {
bail!(BiliError::RequestFailed(code, msg.to_owned()));
} }
Ok(PageAnalyzer::new(res["data"].take())) Ok(PageAnalyzer::new(res["data"].take()))
} }