feat: 支持跳过视频的某些处理部分 (#492)

This commit is contained in:
ᴀᴍᴛᴏᴀᴇʀ
2025-10-11 20:45:44 +08:00
committed by GitHub
parent ed54ca13b8
commit 02c42861ab
6 changed files with 90 additions and 13 deletions

View File

@@ -9,7 +9,7 @@ use validator::Validate;
use crate::bilibili::{Credential, DanmakuOption, FilterOption};
use crate::config::LegacyConfig;
use crate::config::default::{default_auth_token, default_bind_address, default_time_format};
use crate::config::item::{ConcurrentLimit, NFOTimeType};
use crate::config::item::{ConcurrentLimit, NFOTimeType, SkipOption};
use crate::utils::model::{load_db_config, save_db_config};
pub static CONFIG_DIR: LazyLock<PathBuf> =
@@ -22,6 +22,8 @@ pub struct Config {
pub credential: Credential,
pub filter_option: FilterOption,
pub danmaku_option: DanmakuOption,
#[serde(default)]
pub skip_option: SkipOption,
pub video_name: String,
pub page_name: String,
pub interval: u64,
@@ -94,6 +96,7 @@ impl Default for Config {
credential: Credential::default(),
filter_option: FilterOption::default(),
danmaku_option: DanmakuOption::default(),
skip_option: SkipOption::default(),
video_name: "{{title}}".to_owned(),
page_name: "{{bvid}}".to_owned(),
interval: 1200,
@@ -115,6 +118,7 @@ impl From<LegacyConfig> for Config {
credential: legacy.credential,
filter_option: legacy.filter_option,
danmaku_option: legacy.danmaku_option,
skip_option: SkipOption::default(),
video_name: legacy.video_name,
page_name: legacy.page_name,
interval: legacy.interval,

View File

@@ -69,6 +69,15 @@ impl Default for ConcurrentLimit {
}
}
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct SkipOption {
pub no_poster: bool,
pub no_video_nfo: bool,
pub no_upper: bool,
pub no_danmaku: bool,
pub no_subtitle: bool,
}
pub trait PathSafeTemplate {
fn path_safe_register(&mut self, name: &'static str, template: impl Into<String>) -> Result<()>;
fn path_safe_render(&self, name: &'static str, data: &serde_json::Value) -> Result<String>;

View File

@@ -10,7 +10,7 @@ mod versioned_config;
pub use crate::config::args::{ARGS, version};
pub use crate::config::current::{CONFIG_DIR, Config};
pub use crate::config::handlebar::TEMPLATE;
pub use crate::config::item::{NFOTimeType, PathSafeTemplate, RateLimit};
pub use crate::config::item::{NFOTimeType, PathSafeTemplate, RateLimit, SkipOption};
pub use crate::config::legacy::LegacyConfig;
pub use crate::config::versioned_cache::VersionedCache;
pub use crate::config::versioned_config::VersionedConfig;

View File

@@ -14,7 +14,7 @@ use tokio::sync::Semaphore;
use crate::adapter::{VideoSource, VideoSourceEnum};
use crate::bilibili::{BestStream, BiliClient, BiliError, Dimension, PageInfo, Video, VideoInfo};
use crate::config::{ARGS, PathSafeTemplate, TEMPLATE, VersionedConfig};
use crate::config::{ARGS, PathSafeTemplate, SkipOption, TEMPLATE, VersionedConfig};
use crate::downloader::Downloader;
use crate::error::{DownloadAbortError, ExecutionStatus, ProcessPageError};
use crate::utils::format_arg::{page_format_args, video_format_args};
@@ -163,6 +163,8 @@ pub async fn download_unprocessed_videos(
video_source.log_download_video_start();
let semaphore = Semaphore::new(VersionedConfig::get().load().concurrent_limit.video);
let downloader = Downloader::new(bili_client.client.clone());
// 提前获取 skip_option保证单个视频源在整个下载过程中配置不变
let skip_option = &VersionedConfig::get().load().skip_option;
let unhandled_videos_pages = filter_unhandled_video_pages(video_source.filter_expr(), connection).await?;
let mut assigned_upper = HashSet::new();
let tasks = unhandled_videos_pages
@@ -179,6 +181,7 @@ pub async fn download_unprocessed_videos(
&semaphore,
&downloader,
should_download_upper,
skip_option,
)
})
.collect::<FuturesUnordered<_>>();
@@ -218,6 +221,7 @@ pub async fn download_video_pages(
semaphore: &Semaphore,
downloader: &Downloader,
should_download_upper: bool,
skip_option: &SkipOption,
) -> Result<video::ActiveModel> {
let _permit = semaphore.acquire().await.context("acquire semaphore failed")?;
let mut status = VideoStatus::from(video_model.download_status);
@@ -239,7 +243,7 @@ pub async fn download_video_pages(
let (res_1, res_2, res_3, res_4, res_5) = tokio::join!(
// 下载视频封面
fetch_video_poster(
separate_status[0] && !is_single_page,
separate_status[0] && !is_single_page && !skip_option.no_poster,
&video_model,
downloader,
base_path.join("poster.jpg"),
@@ -247,20 +251,20 @@ pub async fn download_video_pages(
),
// 生成视频信息的 nfo
generate_video_nfo(
separate_status[1] && !is_single_page,
separate_status[1] && !is_single_page && !skip_option.no_video_nfo,
&video_model,
base_path.join("tvshow.nfo"),
),
// 下载 Up 主头像
fetch_upper_face(
separate_status[2] && should_download_upper,
separate_status[2] && should_download_upper && !skip_option.no_upper,
&video_model,
downloader,
base_upper_path.join("folder.jpg"),
),
// 生成 Up 主信息的 nfo
generate_upper_nfo(
separate_status[3] && should_download_upper,
separate_status[3] && should_download_upper && !skip_option.no_upper,
&video_model,
base_upper_path.join("person.nfo"),
),
@@ -272,7 +276,8 @@ pub async fn download_video_pages(
pages,
connection,
downloader,
&base_path
&base_path,
skip_option,
)
);
let results = [res_1, res_2, res_3, res_4, res_5]
@@ -309,6 +314,7 @@ pub async fn download_video_pages(
}
/// 分发并执行分页下载任务,当且仅当所有分页成功下载或达到最大重试次数时返回 Ok否则根据失败原因返回对应的错误
#[allow(clippy::too_many_arguments)]
pub async fn dispatch_download_page(
should_run: bool,
bili_client: &BiliClient,
@@ -317,6 +323,7 @@ pub async fn dispatch_download_page(
connection: &DatabaseConnection,
downloader: &Downloader,
base_path: &Path,
skip_option: &SkipOption,
) -> Result<ExecutionStatus> {
if !should_run {
return Ok(ExecutionStatus::Skipped);
@@ -332,6 +339,7 @@ pub async fn dispatch_download_page(
&child_semaphore,
downloader,
base_path,
skip_option,
)
})
.collect::<FuturesUnordered<_>>();
@@ -382,6 +390,7 @@ pub async fn download_page(
semaphore: &Semaphore,
downloader: &Downloader,
base_path: &Path,
skip_option: &SkipOption,
) -> Result<page::ActiveModel> {
let _permit = semaphore.acquire().await.context("acquire semaphore failed")?;
let mut status = PageStatus::from(page_model.download_status);
@@ -437,7 +446,7 @@ pub async fn download_page(
let (res_1, res_2, res_3, res_4, res_5) = tokio::join!(
// 下载分页封面
fetch_page_poster(
separate_status[0],
separate_status[0] && !skip_option.no_poster,
video_model,
&page_model,
downloader,
@@ -454,11 +463,28 @@ pub async fn download_page(
&video_path
),
// 生成分页视频信息的 nfo
generate_page_nfo(separate_status[2], video_model, &page_model, nfo_path),
generate_page_nfo(
separate_status[2] && !skip_option.no_video_nfo,
video_model,
&page_model,
nfo_path
),
// 下载分页弹幕
fetch_page_danmaku(separate_status[3], bili_client, video_model, &page_info, danmaku_path),
fetch_page_danmaku(
separate_status[3] && !skip_option.no_danmaku,
bili_client,
video_model,
&page_info,
danmaku_path
),
// 下载分页字幕
fetch_page_subtitle(separate_status[4], bili_client, video_model, &page_info, &subtitle_path)
fetch_page_subtitle(
separate_status[4] && !skip_option.no_subtitle,
bili_client,
video_model,
&page_info,
&subtitle_path
)
);
let results = [res_1, res_2, res_3, res_4, res_5]
.into_iter()

View File

@@ -246,6 +246,14 @@ export interface DanmakuOption {
time_offset: number;
}
export interface SkipOption {
no_poster: boolean;
no_video_nfo: boolean;
no_upper: boolean;
no_danmaku: boolean;
no_subtitle: boolean;
}
export interface RateLimit {
limit: number;
duration: number;
@@ -270,6 +278,7 @@ export interface Config {
credential: Credential;
filter_option: FilterOption;
danmaku_option: DanmakuOption;
skip_option: SkipOption;
video_name: string;
page_name: string;
interval: number;

View File

@@ -145,7 +145,7 @@
<Tabs.List class="grid w-full grid-cols-5">
<Tabs.Trigger value="basic">基本设置</Tabs.Trigger>
<Tabs.Trigger value="auth">B站认证</Tabs.Trigger>
<Tabs.Trigger value="filter">视频质量</Tabs.Trigger>
<Tabs.Trigger value="filter">视频处理</Tabs.Trigger>
<Tabs.Trigger value="danmaku">弹幕渲染</Tabs.Trigger>
<Tabs.Trigger value="advanced">高级设置</Tabs.Trigger>
</Tabs.List>
@@ -431,6 +431,8 @@
<Separator />
<div class="space-y-4">
<Label>特殊流排除选项</Label>
<p class="text-muted-foreground text-sm">排除某些类型的特殊流</p>
<div class="flex items-center space-x-2">
<Switch id="no-dolby-video" bind:checked={formData.filter_option.no_dolby_video} />
<Label for="no-dolby-video">排除杜比视界视频</Label>
@@ -448,6 +450,33 @@
<Label for="no-hires">排除Hi-RES音频</Label>
</div>
</div>
<Separator />
<div class="space-y-4">
<Label>处理跳过选项</Label>
<p class="text-muted-foreground text-sm">在视频处理部分跳过某些执行环节</p>
<div class="flex items-center space-x-2">
<Switch id="no-dolby-video" bind:checked={formData.skip_option.no_poster} />
<Label for="no-dolby-video">跳过视频封面</Label>
</div>
<div class="flex items-center space-x-2">
<Switch id="no-dolby-audio" bind:checked={formData.skip_option.no_video_nfo} />
<Label for="no-dolby-audio">跳过视频 NFO</Label>
</div>
<div class="flex items-center space-x-2">
<Switch id="no-hdr" bind:checked={formData.skip_option.no_upper} />
<Label for="no-hdr">跳过 Up 主头像、信息</Label>
</div>
<div class="flex items-center space-x-2">
<Switch id="no-hires" bind:checked={formData.skip_option.no_danmaku} />
<Label for="no-hires">跳过弹幕</Label>
</div>
<div class="flex items-center space-x-2">
<Switch id="no-hires" bind:checked={formData.skip_option.no_subtitle} />
<Label for="no-hires">跳过字幕</Label>
</div>
</div>
</Tabs.Content>
<!-- 弹幕设置 -->