feat: 尽量将用户可见日志替换成中文,修复部分问题

This commit is contained in:
amtoaer
2024-04-11 00:05:37 +08:00
parent 2600d5fe5a
commit fe0fa5f3f2
8 changed files with 124 additions and 72 deletions

View File

@@ -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"),
})
}
}

View File

@@ -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::<Vec<VideoInfo>>(videos["data"]["medias"].take()) {

View File

@@ -114,16 +114,23 @@ impl<'a> Video<'a> {
}
async fn get_danmaku_segment(&self, page: &PageInfo, segment_idx: i32) -> Result<Vec<DanmakuElem>> {
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<PageAnalyzer> {

View File

@@ -11,12 +11,14 @@ use crate::bilibili::{Credential, DanmakuOption, FilterOption};
pub static CONFIG: Lazy<Config> = 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()
);
}
}

View File

@@ -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<favorite::Model> {
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::<BiliError>() {
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::<BiliError>() {
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::<DownloadAbortError>().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::<DownloadAbortError>() {
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::<FuturesUnordered<_>>();
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::<DownloadAbortError>().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::<DownloadAbortError>() {
bail!(e);
return Err(e.into());
}
}
let mut page_active_model: page::ActiveModel = page_model.into();

View File

@@ -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)
}
}

View File

@@ -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();

View File

@@ -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;
}
}