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
+5 -1
View File
@@ -9,7 +9,7 @@ use validator::Validate;
use crate::bilibili::{Credential, DanmakuOption, FilterOption}; use crate::bilibili::{Credential, DanmakuOption, FilterOption};
use crate::config::LegacyConfig; use crate::config::LegacyConfig;
use crate::config::default::{default_auth_token, default_bind_address, default_time_format}; 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}; use crate::utils::model::{load_db_config, save_db_config};
pub static CONFIG_DIR: LazyLock<PathBuf> = pub static CONFIG_DIR: LazyLock<PathBuf> =
@@ -22,6 +22,8 @@ pub struct Config {
pub credential: Credential, pub credential: Credential,
pub filter_option: FilterOption, pub filter_option: FilterOption,
pub danmaku_option: DanmakuOption, pub danmaku_option: DanmakuOption,
#[serde(default)]
pub skip_option: SkipOption,
pub video_name: String, pub video_name: String,
pub page_name: String, pub page_name: String,
pub interval: u64, pub interval: u64,
@@ -94,6 +96,7 @@ impl Default for Config {
credential: Credential::default(), credential: Credential::default(),
filter_option: FilterOption::default(), filter_option: FilterOption::default(),
danmaku_option: DanmakuOption::default(), danmaku_option: DanmakuOption::default(),
skip_option: SkipOption::default(),
video_name: "{{title}}".to_owned(), video_name: "{{title}}".to_owned(),
page_name: "{{bvid}}".to_owned(), page_name: "{{bvid}}".to_owned(),
interval: 1200, interval: 1200,
@@ -115,6 +118,7 @@ impl From<LegacyConfig> for Config {
credential: legacy.credential, credential: legacy.credential,
filter_option: legacy.filter_option, filter_option: legacy.filter_option,
danmaku_option: legacy.danmaku_option, danmaku_option: legacy.danmaku_option,
skip_option: SkipOption::default(),
video_name: legacy.video_name, video_name: legacy.video_name,
page_name: legacy.page_name, page_name: legacy.page_name,
interval: legacy.interval, interval: legacy.interval,
+9
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 { pub trait PathSafeTemplate {
fn path_safe_register(&mut self, name: &'static str, template: impl Into<String>) -> Result<()>; 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>; fn path_safe_render(&self, name: &'static str, data: &serde_json::Value) -> Result<String>;
+1 -1
View File
@@ -10,7 +10,7 @@ mod versioned_config;
pub use crate::config::args::{ARGS, version}; pub use crate::config::args::{ARGS, version};
pub use crate::config::current::{CONFIG_DIR, Config}; pub use crate::config::current::{CONFIG_DIR, Config};
pub use crate::config::handlebar::TEMPLATE; 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::legacy::LegacyConfig;
pub use crate::config::versioned_cache::VersionedCache; pub use crate::config::versioned_cache::VersionedCache;
pub use crate::config::versioned_config::VersionedConfig; pub use crate::config::versioned_config::VersionedConfig;
+36 -10
View File
@@ -14,7 +14,7 @@ use tokio::sync::Semaphore;
use crate::adapter::{VideoSource, VideoSourceEnum}; use crate::adapter::{VideoSource, VideoSourceEnum};
use crate::bilibili::{BestStream, BiliClient, BiliError, Dimension, PageInfo, Video, VideoInfo}; 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::downloader::Downloader;
use crate::error::{DownloadAbortError, ExecutionStatus, ProcessPageError}; use crate::error::{DownloadAbortError, ExecutionStatus, ProcessPageError};
use crate::utils::format_arg::{page_format_args, video_format_args}; 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(); video_source.log_download_video_start();
let semaphore = Semaphore::new(VersionedConfig::get().load().concurrent_limit.video); let semaphore = Semaphore::new(VersionedConfig::get().load().concurrent_limit.video);
let downloader = Downloader::new(bili_client.client.clone()); 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 unhandled_videos_pages = filter_unhandled_video_pages(video_source.filter_expr(), connection).await?;
let mut assigned_upper = HashSet::new(); let mut assigned_upper = HashSet::new();
let tasks = unhandled_videos_pages let tasks = unhandled_videos_pages
@@ -179,6 +181,7 @@ pub async fn download_unprocessed_videos(
&semaphore, &semaphore,
&downloader, &downloader,
should_download_upper, should_download_upper,
skip_option,
) )
}) })
.collect::<FuturesUnordered<_>>(); .collect::<FuturesUnordered<_>>();
@@ -218,6 +221,7 @@ pub async fn download_video_pages(
semaphore: &Semaphore, semaphore: &Semaphore,
downloader: &Downloader, downloader: &Downloader,
should_download_upper: bool, should_download_upper: bool,
skip_option: &SkipOption,
) -> Result<video::ActiveModel> { ) -> Result<video::ActiveModel> {
let _permit = semaphore.acquire().await.context("acquire semaphore failed")?; let _permit = semaphore.acquire().await.context("acquire semaphore failed")?;
let mut status = VideoStatus::from(video_model.download_status); 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!( let (res_1, res_2, res_3, res_4, res_5) = tokio::join!(
// 下载视频封面 // 下载视频封面
fetch_video_poster( fetch_video_poster(
separate_status[0] && !is_single_page, separate_status[0] && !is_single_page && !skip_option.no_poster,
&video_model, &video_model,
downloader, downloader,
base_path.join("poster.jpg"), base_path.join("poster.jpg"),
@@ -247,20 +251,20 @@ pub async fn download_video_pages(
), ),
// 生成视频信息的 nfo // 生成视频信息的 nfo
generate_video_nfo( generate_video_nfo(
separate_status[1] && !is_single_page, separate_status[1] && !is_single_page && !skip_option.no_video_nfo,
&video_model, &video_model,
base_path.join("tvshow.nfo"), base_path.join("tvshow.nfo"),
), ),
// 下载 Up 主头像 // 下载 Up 主头像
fetch_upper_face( fetch_upper_face(
separate_status[2] && should_download_upper, separate_status[2] && should_download_upper && !skip_option.no_upper,
&video_model, &video_model,
downloader, downloader,
base_upper_path.join("folder.jpg"), base_upper_path.join("folder.jpg"),
), ),
// 生成 Up 主信息的 nfo // 生成 Up 主信息的 nfo
generate_upper_nfo( generate_upper_nfo(
separate_status[3] && should_download_upper, separate_status[3] && should_download_upper && !skip_option.no_upper,
&video_model, &video_model,
base_upper_path.join("person.nfo"), base_upper_path.join("person.nfo"),
), ),
@@ -272,7 +276,8 @@ pub async fn download_video_pages(
pages, pages,
connection, connection,
downloader, downloader,
&base_path &base_path,
skip_option,
) )
); );
let results = [res_1, res_2, res_3, res_4, res_5] let results = [res_1, res_2, res_3, res_4, res_5]
@@ -309,6 +314,7 @@ pub async fn download_video_pages(
} }
/// 分发并执行分页下载任务,当且仅当所有分页成功下载或达到最大重试次数时返回 Ok,否则根据失败原因返回对应的错误 /// 分发并执行分页下载任务,当且仅当所有分页成功下载或达到最大重试次数时返回 Ok,否则根据失败原因返回对应的错误
#[allow(clippy::too_many_arguments)]
pub async fn dispatch_download_page( pub async fn dispatch_download_page(
should_run: bool, should_run: bool,
bili_client: &BiliClient, bili_client: &BiliClient,
@@ -317,6 +323,7 @@ pub async fn dispatch_download_page(
connection: &DatabaseConnection, connection: &DatabaseConnection,
downloader: &Downloader, downloader: &Downloader,
base_path: &Path, base_path: &Path,
skip_option: &SkipOption,
) -> Result<ExecutionStatus> { ) -> Result<ExecutionStatus> {
if !should_run { if !should_run {
return Ok(ExecutionStatus::Skipped); return Ok(ExecutionStatus::Skipped);
@@ -332,6 +339,7 @@ pub async fn dispatch_download_page(
&child_semaphore, &child_semaphore,
downloader, downloader,
base_path, base_path,
skip_option,
) )
}) })
.collect::<FuturesUnordered<_>>(); .collect::<FuturesUnordered<_>>();
@@ -382,6 +390,7 @@ pub async fn download_page(
semaphore: &Semaphore, semaphore: &Semaphore,
downloader: &Downloader, downloader: &Downloader,
base_path: &Path, base_path: &Path,
skip_option: &SkipOption,
) -> Result<page::ActiveModel> { ) -> Result<page::ActiveModel> {
let _permit = semaphore.acquire().await.context("acquire semaphore failed")?; let _permit = semaphore.acquire().await.context("acquire semaphore failed")?;
let mut status = PageStatus::from(page_model.download_status); 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!( let (res_1, res_2, res_3, res_4, res_5) = tokio::join!(
// 下载分页封面 // 下载分页封面
fetch_page_poster( fetch_page_poster(
separate_status[0], separate_status[0] && !skip_option.no_poster,
video_model, video_model,
&page_model, &page_model,
downloader, downloader,
@@ -454,11 +463,28 @@ pub async fn download_page(
&video_path &video_path
), ),
// 生成分页视频信息的 nfo // 生成分页视频信息的 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] let results = [res_1, res_2, res_3, res_4, res_5]
.into_iter() .into_iter()
+9
View File
@@ -246,6 +246,14 @@ export interface DanmakuOption {
time_offset: number; 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 { export interface RateLimit {
limit: number; limit: number;
duration: number; duration: number;
@@ -270,6 +278,7 @@ export interface Config {
credential: Credential; credential: Credential;
filter_option: FilterOption; filter_option: FilterOption;
danmaku_option: DanmakuOption; danmaku_option: DanmakuOption;
skip_option: SkipOption;
video_name: string; video_name: string;
page_name: string; page_name: string;
interval: number; interval: number;
+30 -1
View File
@@ -145,7 +145,7 @@
<Tabs.List class="grid w-full grid-cols-5"> <Tabs.List class="grid w-full grid-cols-5">
<Tabs.Trigger value="basic">基本设置</Tabs.Trigger> <Tabs.Trigger value="basic">基本设置</Tabs.Trigger>
<Tabs.Trigger value="auth">B站认证</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="danmaku">弹幕渲染</Tabs.Trigger>
<Tabs.Trigger value="advanced">高级设置</Tabs.Trigger> <Tabs.Trigger value="advanced">高级设置</Tabs.Trigger>
</Tabs.List> </Tabs.List>
@@ -431,6 +431,8 @@
<Separator /> <Separator />
<div class="space-y-4"> <div class="space-y-4">
<Label>特殊流排除选项</Label>
<p class="text-muted-foreground text-sm">排除某些类型的特殊流</p>
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<Switch id="no-dolby-video" bind:checked={formData.filter_option.no_dolby_video} /> <Switch id="no-dolby-video" bind:checked={formData.filter_option.no_dolby_video} />
<Label for="no-dolby-video">排除杜比视界视频</Label> <Label for="no-dolby-video">排除杜比视界视频</Label>
@@ -448,6 +450,33 @@
<Label for="no-hires">排除Hi-RES音频</Label> <Label for="no-hires">排除Hi-RES音频</Label>
</div> </div>
</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> </Tabs.Content>
<!-- 弹幕设置 --> <!-- 弹幕设置 -->