mirror of
https://github.com/amtoaer/bili-sync.git
synced 2026-09-05 23:49:53 +08:00
feat: 支持跳过视频的某些处理部分 (#492)
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
<!-- 弹幕设置 -->
|
<!-- 弹幕设置 -->
|
||||||
|
|||||||
Reference in New Issue
Block a user