diff --git a/Cargo.lock b/Cargo.lock index 8f2b00e..a5be630 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -425,6 +425,7 @@ dependencies = [ "serde", "serde_json", "strum 0.26.2", + "thiserror", "tokio", "toml", ] diff --git a/Cargo.toml b/Cargo.toml index 93ae51b..639198d 100644 --- a/Cargo.toml +++ b/Cargo.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" diff --git a/src/bilibili/analyzer.rs b/src/bilibili/analyzer.rs index e655021..fc7ff14 100644 --- a/src/bilibili/analyzer.rs +++ b/src/bilibili/analyzer.rs @@ -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> { 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 = 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 { - 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"))?; diff --git a/src/bilibili/credential.rs b/src/bilibili/credential.rs index 6151d7d..18a137d 100644 --- a/src/bilibili/credential.rs +++ b/src/bilibili/credential.rs @@ -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::() .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#"
(.+?)
"#, res.text().await?.as_str()) } async fn get_new_credential(&self, client: &Client, csrf: &str) -> Result { - 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::().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::().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::() .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(()) } diff --git a/src/bilibili/error.rs b/src/bilibili/error.rs new file mode 100644 index 0000000..c34e7d8 --- /dev/null +++ b/src/bilibili/error.rs @@ -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), +} diff --git a/src/bilibili/favorite_list.rs b/src/bilibili/favorite_list.rs index 8704884..4ffc07a 100644 --- a/src/bilibili/favorite_list.rs +++ b/src/bilibili/favorite_list.rs @@ -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::() .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::() .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::>(videos["data"]["medias"].take()) else{ - break; + let videos_info = match serde_json::from_value::>(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; diff --git a/src/bilibili/mod.rs b/src/bilibili/mod.rs index f6294f3..dd18206 100644 --- a/src/bilibili/mod.rs +++ b/src/bilibili/mod.rs @@ -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; diff --git a/src/bilibili/video.rs b/src/bilibili/video.rs index bbbade1..76b93da 100644 --- a/src/bilibili/video.rs +++ b/src/bilibili/video.rs @@ -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::() .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::() .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::() .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())) }