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

This commit is contained in:
amtoaer
2024-04-02 00:00:17 +08:00
parent f5bc882122
commit beb3634e33
8 changed files with 120 additions and 42 deletions

1
Cargo.lock generated
View File

@@ -425,6 +425,7 @@ dependencies = [
"serde",
"serde_json",
"strum 0.26.2",
"thiserror",
"tokio",
"toml",
]

View File

@@ -36,6 +36,7 @@ sea-orm = { version = "0.12", features = [
serde = { version = "1.0.197", features = ["derive"] }
serde_json = "1.0"
strum = { version = "0.26", features = ["derive"] }
thiserror = "1.0.58"
tokio = { version = "1", features = ["full"] }
toml = "0.8.12"

View File

@@ -1,9 +1,10 @@
use std::sync::Arc;
use anyhow::{anyhow, bail, Result};
use log::error;
use serde::{Deserialize, Serialize};
use crate::bilibili::error::BiliError;
pub struct PageAnalyzer {
info: serde_json::Value,
}
@@ -138,17 +139,26 @@ impl PageAnalyzer {
fn streams(&mut self, filter_option: &FilterOption) -> Result<Vec<Stream>> {
if self.is_flv_stream() {
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() {
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() {
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();
@@ -156,15 +166,7 @@ impl PageAnalyzer {
let audios_data = self.info["dash"]["audio"].take();
let flac_data = self.info["dash"]["flac"].take();
let dolby_data = self.info["dash"]["dolby"].take();
for video_data in videos_data
.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()
{
for video_data in videos_data.as_array().ok_or(BiliError::RiskControlOccurred)?.iter() {
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)
.ok_or(anyhow!("invalid video stream quality"))?;

View File

@@ -9,6 +9,7 @@ use rsa::sha2::Sha256;
use rsa::{Oaep, RsaPublicKey};
use serde::{Deserialize, Serialize};
use super::error::BiliError;
use crate::bilibili::Client;
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
@@ -41,8 +42,16 @@ impl Credential {
)
.send()
.await?
.error_for_status()?
.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()));
}
res["data"]["refresh"].as_bool().ok_or(anyhow!("check refresh failed"))
}
@@ -82,18 +91,13 @@ JNrRuoEUXpabUzGB8QIDAQAB
)
.header(header::COOKIE, "Domain=.bilibili.com")
.send()
.await?;
if !res.status().is_success() {
return match res.status().as_u16() {
404 => Err(anyhow!("correspond path is wrong or expired")),
_ => Err(anyhow!("get csrf failed")),
};
}
.await?
.error_for_status()?;
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> {
let res = client
let mut res = client
.request(
Method::POST,
"https://passport.bilibili.com/x/passport-login/web/cookie/refresh",
@@ -108,8 +112,19 @@ JNrRuoEUXpabUzGB8QIDAQAB
("source", "main_web"),
])
.send()
.await?;
let set_cookies = res.headers().get_all(header::SET_COOKIE);
.await?
.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 {
buvid3: self.buvid3.clone(),
..Default::default()
@@ -132,11 +147,10 @@ JNrRuoEUXpabUzGB8QIDAQAB
_ => unreachable!(),
}
}
let json = res.json::<serde_json::Value>().await?;
if !json["data"]["refresh_token"].is_string() {
if !res["data"]["refresh_token"].is_string() {
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)
}
@@ -154,10 +168,15 @@ JNrRuoEUXpabUzGB8QIDAQAB
])
.send()
.await?
.error_for_status()?
.json::<serde_json::Value>()
.await?;
if res["code"] != 0 {
bail!("confirm refresh failed: {}", res["message"]);
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(())
}

9
src/bilibili/error.rs Normal file
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),
}

View File

@@ -3,8 +3,10 @@ use async_stream::stream;
use chrono::serde::ts_seconds;
use chrono::{DateTime, Utc};
use futures::Stream;
use log::error;
use serde_json::Value;
use crate::bilibili::error::BiliError;
use crate::bilibili::BiliClient;
pub struct FavoriteList<'a> {
client: &'a BiliClient,
@@ -53,8 +55,16 @@ impl<'a> FavoriteList<'a> {
.query(&[("media_id", &self.fid)])
.send()
.await?
.error_for_status()?
.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()));
}
Ok(serde_json::from_value(res["data"].take())?)
}
@@ -72,10 +82,15 @@ impl<'a> FavoriteList<'a> {
])
.send()
.await?
.error_for_status()?
.json::<serde_json::Value>()
.await?;
if res["code"] != 0 {
bail!("get favorite videos failed: {}", res["message"]);
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(res)
}
@@ -85,11 +100,19 @@ impl<'a> FavoriteList<'a> {
stream! {
let mut page = 1;
loop {
let Ok(mut videos) = self.get_videos(page).await else{
break;
let mut videos = match self.get_videos(page).await {
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{
break;
let videos_info = match serde_json::from_value::<Vec<VideoInfo>>(videos["data"]["medias"].take()) {
Ok(v) => v,
Err(e) => {
error!("failed to parse videos of page {}: {}", page, e);
break;
},
};
for video_info in videos_info.into_iter(){
yield video_info;

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 client::{BiliClient, Client};
pub use credential::Credential;
pub use favorite_list::{FavoriteList, FavoriteListInfo, VideoInfo};
pub use video::{PageInfo, Video};
mod analyzer;
mod client;
mod credential;
mod error;
mod favorite_list;
mod video;

View File

@@ -3,6 +3,7 @@ use reqwest::Method;
use crate::bilibili::analyzer::PageAnalyzer;
use crate::bilibili::client::BiliClient;
use crate::bilibili::error::BiliError;
static MASK_CODE: u64 = 2251799813685247;
static XOR_CODE: u64 = 23442827791579;
@@ -55,8 +56,16 @@ impl<'a> Video<'a> {
.query(&[("aid", &self.aid), ("bvid", &self.bvid)])
.send()
.await?
.error_for_status()?
.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()));
}
Ok(serde_json::from_value(res["data"].take())?)
}
@@ -67,8 +76,16 @@ impl<'a> Video<'a> {
.query(&[("aid", &self.aid), ("bvid", &self.bvid)])
.send()
.await?
.error_for_status()?
.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()));
}
Ok(serde_json::from_value(res["data"].take())?)
}
@@ -86,10 +103,15 @@ impl<'a> Video<'a> {
])
.send()
.await?
.error_for_status()?
.json::<serde_json::Value>()
.await?;
if res["code"] != 0 {
bail!("get page analyzer failed: {}", res["message"]);
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(PageAnalyzer::new(res["data"].take()))
}