diff --git a/src/bilibili/danmaku/danmu.rs b/src/bilibili/danmaku/danmu.rs index f5470e0..55c34e8 100644 --- a/src/bilibili/danmaku/danmu.rs +++ b/src/bilibili/danmaku/danmu.rs @@ -1,5 +1,5 @@ //! 一个弹幕实例,但是没有位置信息 -use anyhow::Result; +use anyhow::{bail, Result}; use super::canvas::CanvasConfig; #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] @@ -18,7 +18,8 @@ impl DanmuType { 4 => DanmuType::Bottom, 5 => DanmuType::Top, 6 => DanmuType::Reverse, - _ => unreachable!(), + // 高级弹幕、代码弹幕等,不支持,这里 return error,外面 unwrap_or_default 当成 Float 处理 + _ => bail!("UnSupported danmu type"), }) } } diff --git a/src/bilibili/favorite_list.rs b/src/bilibili/favorite_list.rs index 1cc624d..7fa907a 100644 --- a/src/bilibili/favorite_list.rs +++ b/src/bilibili/favorite_list.rs @@ -107,7 +107,7 @@ impl<'a> FavoriteList<'a> { }, }; if !videos["data"]["medias"].is_array() { - error!("no medias found in favorite {} page {}", self.fid, page); + warn!("no medias found in favorite {} page {}", self.fid, page); break; } let videos_info = match serde_json::from_value::>(videos["data"]["medias"].take()) { diff --git a/src/bilibili/video.rs b/src/bilibili/video.rs index 53c0752..f1cde86 100644 --- a/src/bilibili/video.rs +++ b/src/bilibili/video.rs @@ -114,16 +114,23 @@ impl<'a> Video<'a> { } async fn get_danmaku_segment(&self, page: &PageInfo, segment_idx: i32) -> Result> { - let res = self + let mut res = self .client .request(Method::GET, "http://api.bilibili.com/x/v2/dm/web/seg.so") .query(&[("type", 1), ("oid", page.cid), ("segment_index", segment_idx)]) .send() .await? - .error_for_status()? - .bytes() - .await?; - Ok(DmSegMobileReply::decode(res)?.elems) + .error_for_status()?; + let headers = std::mem::take(res.headers_mut()); + let content_type = headers.get("content-type"); + if !content_type.is_some_and(|v| v == "application/octet-stream") { + bail!( + "unexpected content type: {:?}, body: {:?}", + content_type, + res.text().await + ); + } + Ok(DmSegMobileReply::decode(res.bytes().await?)?.elems) } pub async fn get_page_analyzer(&self, page: &PageInfo) -> Result { diff --git a/src/config.rs b/src/config.rs index 69ce336..44e9f72 100644 --- a/src/config.rs +++ b/src/config.rs @@ -11,12 +11,14 @@ use crate::bilibili::{Credential, DanmakuOption, FilterOption}; pub static CONFIG: Lazy = Lazy::new(|| { let config = Config::load().unwrap_or_else(|err| { - warn!("Failed loading config: {err}"); + warn!("加载配置失败,错误为: {err},将使用默认配置..."); Config::new() }); // 放到外面,确保新的配置项被保存 + info!("配置加载完毕,覆盖刷新原有配置"); config.save().unwrap(); // 检查配置文件内容 + info!("校验配置文件内容..."); config.check(); config }); @@ -62,43 +64,50 @@ impl Config { let mut ok = true; if self.favorite_list.is_empty() { ok = false; - error!("No favorite list found, program won't do anything"); + error!("未设置需监听的收藏夹,程序空转没有意义"); } for path in self.favorite_list.values() { if !path.is_absolute() { ok = false; - error!("Path in favorite list must be absolute: {}", path.display()); + error!("收藏夹保存的路径应为绝对路径,检测到: {}", path.display()); } } if !self.upper_path.is_absolute() { ok = false; - error!("Upper face path must be absolute"); + error!("up 主头像保存的路径应为绝对路径"); } if self.video_name.is_empty() { ok = false; - error!("No video name template found"); + error!("未设置 video_name 模板"); } if self.page_name.is_empty() { ok = false; - error!("No page name template found"); + error!("未设置 page_name 模板"); } let credential = self.credential.load(); - if let Some(credential) = credential.as_deref() { - if credential.sessdata.is_empty() - || credential.bili_jct.is_empty() - || credential.buvid3.is_empty() - || credential.dedeuserid.is_empty() - || credential.ac_time_value.is_empty() - { - ok = false; - error!("Credential is incomplete"); + match credential.as_deref() { + Some(credential) => { + if credential.sessdata.is_empty() + || credential.bili_jct.is_empty() + || credential.buvid3.is_empty() + || credential.dedeuserid.is_empty() + || credential.ac_time_value.is_empty() + { + ok = false; + error!("Credential 信息不完整,请确保填写完整"); + } + } + None => { + ok = false; + error!("未设置 Credential 信息"); } - } else { - warn!("No credential found, can't access high quality video"); } if !ok { - panic!("Config in {} is invalid", CONFIG_DIR.join("config.toml").display()); + panic!( + "位于 {} 的配置文件不合法,请参考提示信息修复后继续运行", + CONFIG_DIR.join("config.toml").display() + ); } } diff --git a/src/core/command.rs b/src/core/command.rs index f532993..c39e946 100644 --- a/src/core/command.rs +++ b/src/core/command.rs @@ -24,7 +24,7 @@ use crate::core::utils::{ create_video_pages, create_videos, exist_labels, filter_unfilled_videos, handle_favorite_info, total_video_count, }; use crate::downloader::Downloader; -use crate::error::DownloadAbortError; +use crate::error::{DownloadAbortError, ProcessPageError}; /// 处理某个收藏夹,首先刷新收藏夹信息,然后下载收藏夹中未下载成功的视频 pub async fn process_favorite_list( @@ -48,7 +48,7 @@ pub async fn refresh_favorite_list( let bili_favorite_list = FavoriteList::new(bili_client, fid.to_owned()); let favorite_list_info = bili_favorite_list.get_info().await?; let favorite_model = handle_favorite_info(&favorite_list_info, path, connection).await?; - info!("Scan the favorite: {fid}."); + info!("开始扫描收藏夹: {} - {}...", favorite_model.f_id, favorite_model.name); // 每十个视频一组,避免太多的数据库操作 let video_stream = bili_favorite_list.into_video_stream().chunks(10); pin_mut!(video_stream); @@ -64,12 +64,15 @@ pub async fn refresh_favorite_list( // 将视频信息写入数据库 create_videos(&videos_info, &favorite_model, connection).await?; if should_break { - info!("Reach the last processed processed position, break.."); + info!("到达上一次处理的位置,提前中止"); break; } } let total_count = total_video_count(&favorite_model, connection).await? - total_count; - info!("Scan the favorite: {fid} done, got {got_count} videos, {total_count} new videos."); + info!( + "扫描收藏夹: {} - {} 完成, 获取了 {} 条视频, 其中有 {} 条新视频", + favorite_model.f_id, favorite_model.name, got_count, total_count + ); Ok(favorite_model) } @@ -79,14 +82,20 @@ pub async fn fetch_video_details( favorite_model: favorite::Model, connection: &DatabaseConnection, ) -> Result { - info!("start to fetch video details in favorite: {}", favorite_model.f_id); + info!( + "开始获取收藏夹 {} - {} 的视频与分页信息...", + favorite_model.f_id, favorite_model.name + ); let videos_model = filter_unfilled_videos(&favorite_model, connection).await?; for video_model in videos_model { let bili_video = Video::new(bili_client, video_model.bvid.clone()); let tags = match bili_video.get_tags().await { Ok(tags) => tags, Err(e) => { - error!("failed to get tags for video: {}, {}", &video_model.bvid, e); + error!( + "获取视频 {} - {} 的标签失败,错误为:{}", + &video_model.bvid, &video_model.name, e + ); if let Some(BiliError::RequestFailed(code, _)) = e.downcast_ref::() { if *code == -404 { let mut video_active_model: video::ActiveModel = video_model.into(); @@ -100,7 +109,10 @@ pub async fn fetch_video_details( let pages_info = match bili_video.get_pages().await { Ok(pages) => pages, Err(e) => { - error!("failed to get pages for video: {}, {}", &video_model.bvid, e); + error!( + "获取视频 {} - {} 的分页信息失败,错误为:{}", + &video_model.bvid, &video_model.name, e + ); if let Some(BiliError::RequestFailed(code, _)) = e.downcast_ref::() { if *code == -404 { let mut video_active_model: video::ActiveModel = video_model.into(); @@ -121,7 +133,10 @@ pub async fn fetch_video_details( video_active_model.save(&txn).await?; txn.commit().await?; } - info!("fetch video details in favorite: {} done.", favorite_model.f_id); + info!( + "获取收藏夹 {} - {} 的视频与分页信息完成", + favorite_model.f_id, favorite_model.name + ); Ok(favorite_model) } @@ -131,7 +146,10 @@ pub async fn download_unprocessed_videos( favorite_model: favorite::Model, connection: &DatabaseConnection, ) -> Result<()> { - info!("start to download videos in favorite: {}", favorite_model.f_id); + info!( + "开始下载收藏夹: {} - {} 中所有未处理过的视频...", + favorite_model.f_id, favorite_model.name + ); let unhandled_videos_pages = unhandled_videos_pages(&favorite_model, connection).await?; // 对于视频,允许五个同时下载(视频内还有分页、不同分页还有多种下载任务) let semaphore = Semaphore::new(5); @@ -164,7 +182,7 @@ pub async fn download_unprocessed_videos( } Err(e) => { if e.downcast_ref::().is_some() { - warn!("{e}"); + error!("下载视频时触发风控,将终止收藏夹下所有下载任务,等待下一轮执行"); break; } } @@ -177,7 +195,10 @@ pub async fn download_unprocessed_videos( if !models.is_empty() { update_videos_model(models, connection).await?; } - info!("download videos in favorite: {} done.", favorite_model.f_id); + info!( + "下载收藏夹: {} - {} 中未处理过的视频完成", + favorite_model.f_id, favorite_model.name + ); Ok(()) } @@ -252,20 +273,20 @@ pub async fn download_video_pages( results .iter() .take(4) - .zip(["poster", "video nfo", "upper face", "upper nfo"]) - .for_each(|(res, task_name)| { - if res.is_err() { - error!( - "Video {} {} failed: {}", - &video_model.bvid, - task_name, - res.as_ref().unwrap_err() - ); - } + .zip(["封面", "视频 nfo", "up 主头像", "up 主 nfo"]) + .for_each(|(res, task_name)| match res { + Ok(_) => info!( + "处理视频 {} - {} 的 {} 成功", + &video_model.bvid, &video_model.name, task_name + ), + Err(e) => error!( + "处理视频 {} - {} 的 {} 失败: {}", + &video_model.bvid, &video_model.name, task_name, e + ), }); if let Err(e) = results.into_iter().nth(4).unwrap() { if let Ok(e) = e.downcast::() { - bail!(e); + return Err(e.into()); } } let mut video_active_model: video::ActiveModel = video_model.into(); @@ -291,14 +312,14 @@ pub async fn dispatch_download_page( .map(|page_model| download_page(bili_client, video_model, page_model, &child_semaphore, downloader)) .collect::>(); let mut models = Vec::with_capacity(10); - let mut should_error = false; + let (mut should_error, mut is_break) = (false, false); while let Some(res) = tasks.next().await { match res { Ok(model) => { if let Set(status) = model.download_status { let status = PageStatus::new(status); if status.should_run().iter().any(|v| *v) { - // 有一个分页没下载完成,就应该将视频本身标记为未完成 + // 有一个分页没变成终止状态(即下载成功或者重试次数达到限制),就应该向上层传递 Error should_error = true; } } @@ -306,7 +327,7 @@ pub async fn dispatch_download_page( } Err(e) => { if e.downcast_ref::().is_some() { - warn!("{e}"); + is_break = true; break; } } @@ -319,7 +340,19 @@ pub async fn dispatch_download_page( update_pages_model(models, connection).await?; } if should_error { - bail!("Some pages failed to download"); + if is_break { + error!( + "下载视频 {} - {} 的分页时触发风控,将异常向上传递...", + &video_model.bvid, &video_model.name + ); + bail!(DownloadAbortError()); + } else { + error!( + "下载视频 {} - {} 的分页时出现了错误,将在下一轮尝试重新处理", + &video_model.bvid, &video_model.name + ); + bail!(ProcessPageError()); + } } Ok(()) } @@ -418,22 +451,21 @@ pub async fn download_page( status.update_status(&results); results .iter() - .zip(["poster", "video", "nfo", "danmaku"]) - .for_each(|(res, task_name)| { - if res.is_err() { - error!( - "Video {} page {} {} failed: {}", - &video_model.bvid, - page_model.pid, - task_name, - res.as_ref().unwrap_err() - ); - } + .zip(["封面", "视频", "视频 nfo", "弹幕"]) + .for_each(|(res, task_name)| match res { + Ok(_) => info!( + "处理视频 {} - {} 第 {} 页的 {} 成功", + &video_model.bvid, &video_model.name, page_model.pid, task_name + ), + Err(e) => error!( + "处理视频 {} - {} 第 {} 页的 {} 失败: {}", + &video_model.bvid, &video_model.name, page_model.pid, task_name, e + ), }); // 查看下载视频的状态,该状态会影响上层是否 break if let Err(e) = results.into_iter().nth(1).unwrap() { if let Ok(e) = e.downcast::() { - bail!(e); + return Err(e.into()); } } let mut page_active_model: page::ActiveModel = page_model.into(); diff --git a/src/core/status.rs b/src/core/status.rs index 129c5cf..eb771da 100644 --- a/src/core/status.rs +++ b/src/core/status.rs @@ -150,7 +150,7 @@ impl PageStatus { pub fn update_status(&mut self, result: &[Result<()>]) { assert!(result.len() == 4, "PageStatus should have 4 status"); - self.0.update_status(&result) + self.0.update_status(result) } } diff --git a/src/error.rs b/src/error.rs index 04f46ed..5d415c1 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,5 +1,9 @@ use thiserror::Error; #[derive(Error, Debug)] -#[error("Bilibili api request too frequently, abort all tasks and try again later")] +#[error("Request too frequently")] pub struct DownloadAbortError(); + +#[derive(Error, Debug)] +#[error("Process page error")] +pub struct ProcessPageError(); diff --git a/src/main.rs b/src/main.rs index cec9183..63f396d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,20 +26,19 @@ async fn main() -> ! { loop { if anchor != chrono::Local::now().date_naive() { if let Err(e) = bili_client.check_refresh().await { - error!("Error: {e}"); + error!("检查刷新 Credential 遇到错误:{e},等待下一轮执行"); tokio::time::sleep(std::time::Duration::from_secs(CONFIG.interval)).await; continue; } anchor = chrono::Local::now().date_naive(); } for (fid, path) in &CONFIG.favorite_list { - let res = process_favorite_list(&bili_client, fid, path, &connection).await; - if let Err(e) = res { - error!("Error: {e}"); + if let Err(e) = process_favorite_list(&bili_client, fid, path, &connection).await { + // 可预期的错误都被内部处理了,这里漏出来应该是大问题 + error!("处理收藏夹 {fid} 时遇到非预期的错误:{e}"); } } - info!("All favorite lists have been processed, wait for next round."); - + info!("所有收藏夹处理完毕,等待下一轮执行"); tokio::time::sleep(std::time::Duration::from_secs(CONFIG.interval)).await; } }