feat: 在接口层级返回特定错误
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -425,6 +425,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum 0.26.2",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"toml",
|
||||
]
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"))?;
|
||||
|
||||
@@ -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
9
src/bilibili/error.rs
Normal 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),
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user