feat: 尽量将用户可见日志替换成中文,修复部分问题
This commit is contained in:
@@ -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"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
11
src/main.rs
11
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user