From 3a7e30b2cf007ee5aa14c78f729d45cb37b39d4c Mon Sep 17 00:00:00 2001 From: lanyeeee Date: Tue, 26 Aug 2025 05:24:40 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=90=8E=E7=AB=AF=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=B5=8C=E5=85=A5=E7=AB=A0=E8=8A=82=E5=85=83=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/config.rs | 2 + src-tauri/src/downloader/download_progress.rs | 22 +- src-tauri/src/downloader/download_task.rs | 310 +++++++++++++++--- .../downloader/tasks/embed_chapter_task.rs | 14 + src-tauri/src/downloader/tasks/mod.rs | 1 + src-tauri/src/types/player_info.rs | 20 ++ src-tauri/src/utils.rs | 12 +- src/bindings.ts | 5 +- 8 files changed, 339 insertions(+), 47 deletions(-) create mode 100644 src-tauri/src/downloader/tasks/embed_chapter_task.rs diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 84cdfc9..2496b25 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -20,6 +20,7 @@ pub struct Config { pub download_video: bool, pub download_audio: bool, pub auto_merge: bool, + pub embed_chapter: bool, pub download_xml_danmaku: bool, pub download_ass_danmaku: bool, pub download_json_danmaku: bool, @@ -104,6 +105,7 @@ impl Config { download_video: true, download_audio: true, auto_merge: true, + embed_chapter: true, download_xml_danmaku: true, download_ass_danmaku: true, download_json_danmaku: true, diff --git a/src-tauri/src/downloader/download_progress.rs b/src-tauri/src/downloader/download_progress.rs index 8df41ee..6b17a5b 100644 --- a/src-tauri/src/downloader/download_progress.rs +++ b/src-tauri/src/downloader/download_progress.rs @@ -13,8 +13,8 @@ use crate::{ config::Config, downloader::tasks::{ audio_task::AudioTask, cover_task::CoverTask, danmaku_task::DanmakuTask, - json_task::JsonTask, merge_task::MergeTask, nfo_task::NfoTask, subtitle_task::SubtitleTask, - video_task::VideoTask, + embed_chapter_task::EmbedChapterTask, json_task::JsonTask, merge_task::MergeTask, + nfo_task::NfoTask, subtitle_task::SubtitleTask, video_task::VideoTask, }, extensions::AppHandleExt, types::{ @@ -53,6 +53,7 @@ pub struct DownloadProgress { pub video_task: VideoTask, pub audio_task: AudioTask, pub merge_task: MergeTask, + pub embed_chapter_task: EmbedChapterTask, pub subtitle_task: SubtitleTask, pub danmaku_task: DanmakuTask, pub cover_task: CoverTask, @@ -125,6 +126,7 @@ impl DownloadProgress { video_task: tasks.video, audio_task: tasks.audio, merge_task: tasks.merge, + embed_chapter_task: tasks.embed_chapter, danmaku_task: tasks.danmaku, subtitle_task: tasks.subtitle, cover_task: tasks.cover, @@ -176,6 +178,7 @@ impl DownloadProgress { video_task: tasks.video, audio_task: tasks.audio, merge_task: tasks.merge, + embed_chapter_task: tasks.embed_chapter, danmaku_task: tasks.danmaku, subtitle_task: tasks.subtitle, cover_task: tasks.cover, @@ -320,6 +323,7 @@ impl DownloadProgress { self.video_task.is_completed() && self.audio_task.is_completed() && self.merge_task.is_completed() + && self.embed_chapter_task.is_completed() && self.danmaku_task.is_completed() && self.subtitle_task.is_completed() && self.cover_task.is_completed() @@ -331,6 +335,7 @@ impl DownloadProgress { self.video_task.mark_uncompleted(); self.audio_task.mark_uncompleted(); self.merge_task.completed = false; + self.embed_chapter_task.completed = false; self.danmaku_task.completed = false; self.subtitle_task.completed = false; self.cover_task.completed = false; @@ -384,6 +389,7 @@ fn create_normal_progresses_for_single( video_task: tasks.video, audio_task: tasks.audio, merge_task: tasks.merge, + embed_chapter_task: tasks.embed_chapter, danmaku_task: tasks.danmaku, subtitle_task: tasks.subtitle, cover_task: tasks.cover, @@ -424,6 +430,7 @@ fn create_normal_progresses_for_single( video_task: tasks.video, audio_task: tasks.audio, merge_task: tasks.merge, + embed_chapter_task: tasks.embed_chapter, danmaku_task: tasks.danmaku, subtitle_task: tasks.subtitle, cover_task: tasks.cover, @@ -464,6 +471,7 @@ fn create_normal_progresses_for_single( video_task: tasks.video.clone(), audio_task: tasks.audio.clone(), merge_task: tasks.merge.clone(), + embed_chapter_task: tasks.embed_chapter.clone(), danmaku_task: tasks.danmaku.clone(), subtitle_task: tasks.subtitle.clone(), cover_task: tasks.cover.clone(), @@ -536,6 +544,7 @@ fn create_normal_progresses_for_season( video_task: tasks.video, audio_task: tasks.audio, merge_task: tasks.merge, + embed_chapter_task: tasks.embed_chapter, danmaku_task: tasks.danmaku, subtitle_task: tasks.subtitle, cover_task: tasks.cover, @@ -576,6 +585,7 @@ fn create_normal_progresses_for_season( video_task: tasks.video, audio_task: tasks.audio, merge_task: tasks.merge, + embed_chapter_task: tasks.embed_chapter, danmaku_task: tasks.danmaku, subtitle_task: tasks.subtitle, cover_task: tasks.cover, @@ -617,6 +627,7 @@ fn create_normal_progresses_for_season( video_task: tasks.video.clone(), audio_task: tasks.audio.clone(), merge_task: tasks.merge.clone(), + embed_chapter_task: tasks.embed_chapter.clone(), danmaku_task: tasks.danmaku.clone(), subtitle_task: tasks.subtitle.clone(), cover_task: tasks.cover.clone(), @@ -639,6 +650,7 @@ struct Tasks { video: VideoTask, audio: AudioTask, merge: MergeTask, + embed_chapter: EmbedChapterTask, danmaku: DanmakuTask, subtitle: SubtitleTask, cover: CoverTask, @@ -672,6 +684,11 @@ impl Tasks { completed: false, }; + let embed_chapter = EmbedChapterTask { + selected: config.embed_chapter, + completed: false, + }; + let danmaku = DanmakuTask { xml_selected: config.download_xml_danmaku, ass_selected: config.download_ass_danmaku, @@ -704,6 +721,7 @@ impl Tasks { video, audio, merge, + embed_chapter, danmaku, subtitle, cover, diff --git a/src-tauri/src/downloader/download_task.rs b/src-tauri/src/downloader/download_task.rs index cd2bec1..dcb700b 100644 --- a/src-tauri/src/downloader/download_task.rs +++ b/src-tauri/src/downloader/download_task.rs @@ -1,6 +1,6 @@ use std::{ fs::{File, OpenOptions}, - io::{Seek, Write}, + io::{Seek, Write as IoWrite}, sync::Arc, time::{Duration, SystemTime, UNIX_EPOCH}, }; @@ -17,7 +17,6 @@ use tokio::{ }; use crate::{ - bili_client::BiliClient, danmaku_xml_to_ass::xml_to_ass, downloader::episode_type::EpisodeType, events::DownloadEvent, @@ -27,6 +26,7 @@ use crate::{ create_download_task_params::CreateDownloadTaskParams, get_bangumi_info_params::GetBangumiInfoParams, get_cheese_info_params::GetCheeseInfoParams, get_normal_info_params::GetNormalInfoParams, normal_info::NormalInfo, + player_info::PlayerInfo, }, utils::{self, ToXml}, }; @@ -288,6 +288,9 @@ impl DownloadTask { episode_dir.display() ))?; + let mut player_info = None; + let mut episode_info = None; + if !progress.video_task.is_completed() && progress.video_task.content_length != 0 { // 如果视频任务被选中且未完成且有要下载的内容,则下载视频 self.download_video(&progress) @@ -304,11 +307,29 @@ impl DownloadTask { tracing::debug!("{ids_string} `{filename}`音频下载完成"); } - if !progress.merge_task.is_completed() { + let merge_completed = progress.merge_task.is_completed(); + let embed_completed = progress.embed_chapter_task.is_completed(); + + if !merge_completed && !embed_completed { + // 如果合并任务和嵌入章节任务都未完成,则调用merge_and_embed,将两个任务通过一个ffmpeg命令完成 + self.merge_and_embed(&progress, &mut player_info) + .await + .context(format!( + "{ids_string} `{filename}`合并视频和音频失败并+嵌入章节元数据失败" + ))?; + tracing::debug!("{ids_string} `{filename}`视频和音频合并+嵌入章节元数据完成"); + } else if !merge_completed { + // 如果合并任务未完成,嵌入章节任务已完成,则只合并 self.merge_video_audio(&progress) .await .context(format!("{ids_string} `{filename}`合并视频和音频失败"))?; tracing::debug!("{ids_string} `{filename}`视频和音频合并完成"); + } else if !embed_completed { + // 如果嵌入章节任务未完成,合并任务已完成,则只嵌入 + self.embed_chapter(&progress, &mut player_info) + .await + .context(format!("{ids_string} `{filename}`嵌入章节元数据失败"))?; + tracing::debug!("{ids_string} `{filename}`嵌入章节元数据完成"); } if !progress.danmaku_task.is_completed() { @@ -319,7 +340,7 @@ impl DownloadTask { } if !progress.subtitle_task.is_completed() { - self.download_subtitle(&progress) + self.download_subtitle(&progress, &mut player_info) .await .context(format!("{ids_string} `{filename}`下载字幕失败"))?; tracing::debug!("{ids_string} `{filename}`字幕下载完成"); @@ -332,8 +353,6 @@ impl DownloadTask { tracing::debug!("{ids_string} `{filename}`封面下载完成"); } - let mut episode_info = None; - if !progress.nfo_task.is_completed() { self.download_nfo(&progress, &mut episode_info) .await @@ -599,11 +618,7 @@ impl DownloadTask { let output_path = episode_dir.join(format!("{filename}-merged.mp4")); - let ffmpeg_program = std::env::current_exe() - .context("获取当前可执行文件路径失败")? - .parent() - .context("获取当前可执行文件所在目录失败")? - .join("com.lanyeeee.bilibili-video-downloader-ffmpeg"); + let ffmpeg_program = utils::get_ffmpeg_program().context("获取FFmpeg程序路径失败")?; let (tx, rx) = tokio::sync::oneshot::channel(); let video_path_clone = video_path.clone(); @@ -665,6 +680,202 @@ impl DownloadTask { Ok(()) } + async fn embed_chapter( + &self, + progress: &DownloadProgress, + player_info: &mut Option, + ) -> anyhow::Result<()> { + let (episode_dir, filename) = (&progress.episode_dir, &progress.filename); + + let video_path = episode_dir.join(format!("{filename}.mp4")); + if !video_path.exists() { + self.update_progress(|p| p.embed_chapter_task.completed = true); + return Ok(()); + } + + let ffmpeg_program = utils::get_ffmpeg_program().context("获取FFmpeg程序路径失败")?; + let output_path = episode_dir.join(format!("{filename}-embed.mp4")); + + let player_info = player_info.get_or_init(&self.app, progress).await?; + + let metadata_content = player_info.generate_chapter_metadata(); + let metadata_path = episode_dir.join(format!("{filename}.FFMETA.ini")); + + std::fs::write(&metadata_path, metadata_content) + .context(format!("保存章节元数据到`{}`失败", metadata_path.display()))?; + + let (tx, rx) = tokio::sync::oneshot::channel(); + let video_path_clone = video_path.clone(); + let metadata_path_clone = metadata_path.clone(); + let output_path_clone = output_path.clone(); + + tauri::async_runtime::spawn_blocking(move || { + let mut command = std::process::Command::new(ffmpeg_program); + + command + .arg("-i") + .arg(video_path_clone) + .arg("-i") + .arg(metadata_path_clone) + .arg("-map_metadata") + .arg("1") + .arg("-c") + .arg("copy") + .arg(output_path_clone) + .arg("-y"); + + #[cfg(target_os = "windows")] + { + // 隐藏窗口 + use std::os::windows::process::CommandExt; + command.creation_flags(0x0800_0000); + } + + let output = command.output(); + + let _ = tx.send(output); + }); + + let output = rx.await??; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + let stdout = String::from_utf8_lossy(&output.stdout); + let err = anyhow!(format!("STDOUT: {stdout}")) + .context(format!("STDERR: {stderr}")) + .context("原因可能是视频或音频文件损坏,建议[重来]试试"); + return Err(err); + } + + std::fs::remove_file(&video_path) + .context(format!("删除视频文件`{}`失败", video_path.display()))?; + std::fs::rename(&output_path, &video_path).context(format!( + "将`{}`重命名为`{}`失败", + output_path.display(), + video_path.display() + ))?; + std::fs::remove_file(&metadata_path).context(format!( + "删除章节元数据文件`{}`失败", + metadata_path.display() + ))?; + + self.update_progress(|p| p.embed_chapter_task.completed = true); + + Ok(()) + } + + async fn merge_and_embed( + &self, + progress: &DownloadProgress, + player_info: &mut Option, + ) -> anyhow::Result<()> { + let (episode_dir, filename) = (&progress.episode_dir, &progress.filename); + + let video_path = episode_dir.join(format!("{filename}.mp4")); + if !video_path.exists() { + self.update_progress(|p| { + p.merge_task.completed = true; + p.embed_chapter_task.completed = true; + }); + + return Ok(()); + } + + let audio_path = episode_dir.join(format!("{filename}.m4a")); + if !audio_path.exists() { + // 如果音频文件不存在,则只嵌入章节元数据 + self.embed_chapter(progress, player_info) + .await + .context("嵌入章节元数据失败")?; + + self.update_progress(|p| p.merge_task.completed = true); + return Ok(()); + } + + let player_info = player_info.get_or_init(&self.app, progress).await?; + + let metadata_content = player_info.generate_chapter_metadata(); + let metadata_path = episode_dir.join(format!("{filename}.FFMETA.ini")); + + std::fs::write(&metadata_path, metadata_content) + .context(format!("保存章节元数据到`{}`失败", metadata_path.display()))?; + + let output_path = episode_dir.join(format!("{filename}-merged.mp4")); + + let ffmpeg_program = utils::get_ffmpeg_program().context("获取FFmpeg程序路径失败")?; + + let (tx, rx) = tokio::sync::oneshot::channel(); + let video_path_clone = video_path.clone(); + let audio_path_clone = audio_path.clone(); + let metadata_path_clone = metadata_path.clone(); + let output_path_clone = output_path.clone(); + + tokio::spawn(async move { + let mut command = std::process::Command::new(ffmpeg_program); + + command + .arg("-i") + .arg(video_path_clone) + .arg("-i") + .arg(audio_path_clone) + .arg("-i") + .arg(metadata_path_clone) + .arg("-map_metadata") + .arg("2") + .arg("-c") + .arg("copy") + .arg("-map") + .arg("0:v:0") + .arg("-map") + .arg("1:a:0") + .arg(output_path_clone) + .arg("-y"); + + #[cfg(target_os = "windows")] + { + // 隐藏窗口 + use std::os::windows::process::CommandExt; + command.creation_flags(0x0800_0000); + } + + let output = command.output(); + + let _ = tx.send(output); + }); + + let output = rx.await??; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + let stdout = String::from_utf8_lossy(&output.stdout); + let err = anyhow!(format!("STDOUT: {stdout}")) + .context(format!("STDERR: {stderr}")) + .context("原因可能是视频或音频文件损坏,建议[重来]试试"); + return Err(err); + } + + std::fs::remove_file(&video_path) + .context(format!("删除视频文件`{}`失败", video_path.display()))?; + std::fs::remove_file(&audio_path) + .context(format!("删除音频文件`{}`失败", audio_path.display()))?; + std::fs::rename(&output_path, &video_path).context(format!( + "将`{}`重命名为`{}`失败", + output_path.display(), + video_path.display() + ))?; + std::fs::remove_file(&metadata_path).context(format!( + "删除章节元数据文件`{}`失败", + metadata_path.display() + ))?; + + self.update_progress(|p| { + p.merge_task.completed = true; + p.embed_chapter_task.completed = true; + }); + + Ok(()) + } + async fn download_danmaku(&self, progress: &DownloadProgress) -> anyhow::Result<()> { let (aid, cid, duration) = (progress.aid, progress.cid, progress.duration); let danmaku_task = &progress.danmaku_task; @@ -705,24 +916,20 @@ impl DownloadTask { Ok(()) } - async fn download_subtitle(&self, progress: &DownloadProgress) -> anyhow::Result<()> { + async fn download_subtitle( + &self, + progress: &DownloadProgress, + player_info: &mut Option, + ) -> anyhow::Result<()> { use std::fmt::Write; let (episode_dir, filename) = (&progress.episode_dir, &progress.filename); - let (aid, cid) = { - let progress = self.progress.read(); - (progress.aid, progress.cid) - }; + let player_info = player_info.get_or_init(&self.app, progress).await?; let bili_client = self.app.get_bili_client(); - let player_info = bili_client - .get_player_info(aid, cid) - .await - .context("获取播放器信息失败")?; - let subtitle = &player_info.subtitle; - for subtitle_detail in &subtitle.subtitles { + for subtitle_detail in &player_info.subtitle.subtitles { let url = format!("http:{}", subtitle_detail.subtitle_url); let subtitle = bili_client .get_subtitle(&url) @@ -775,18 +982,15 @@ impl DownloadTask { episode_info: &mut Option, ) -> anyhow::Result<()> { let (episode_dir, filename) = (&progress.episode_dir, &progress.filename); - let (aid, ep_id, episode_type) = (progress.aid, progress.ep_id, progress.episode_type); + + let episode_info = episode_info.get_or_init(&self.app, progress).await?; let bili_client = self.app.get_bili_client(); - let episode_info = episode_info - .get_or_init(&bili_client, aid, ep_id, episode_type) - .await?; - match episode_info { EpisodeInfo::Normal(info) => { let tags = bili_client - .get_tags(aid) + .get_tags(progress.aid) .await .context("获取视频标签失败")?; let movie_nfo = info @@ -885,13 +1089,8 @@ impl DownloadTask { episode_info: &mut Option, ) -> anyhow::Result<()> { let (episode_dir, filename) = (&progress.episode_dir, &progress.filename); - let (aid, ep_id, episode_type) = (progress.aid, progress.ep_id, progress.episode_type); - let bili_client = self.app.get_bili_client(); - - let episode_info = episode_info - .get_or_init(&bili_client, aid, ep_id, episode_type) - .await?; + let episode_info = episode_info.get_or_init(&self.app, progress).await?; let json_path = episode_dir.join(format!("{filename}-元数据.json")); let json_string = match episode_info { @@ -1156,25 +1355,24 @@ enum EpisodeInfo { trait GetOrInitEpisodeInfo { async fn get_or_init<'a>( &'a mut self, - bili_client: &BiliClient, - aid: i64, - ep_id: Option, - episode_type: EpisodeType, + app: &AppHandle, + progress: &DownloadProgress, ) -> anyhow::Result<&'a mut EpisodeInfo>; } impl GetOrInitEpisodeInfo for Option { async fn get_or_init<'a>( &'a mut self, - bili_client: &BiliClient, - aid: i64, - ep_id: Option, - episode_type: EpisodeType, + app: &AppHandle, + progress: &DownloadProgress, ) -> anyhow::Result<&'a mut EpisodeInfo> { if let Some(info) = self { return Ok(info); } + let bili_client = app.get_bili_client(); + let (aid, ep_id, episode_type) = (progress.aid, progress.ep_id, progress.episode_type); + let new_info = match episode_type { EpisodeType::Normal => { let info = bili_client @@ -1204,3 +1402,31 @@ impl GetOrInitEpisodeInfo for Option { Ok(self.insert(new_info)) } } + +trait GetOrInitPlayerInfo { + async fn get_or_init<'a>( + &'a mut self, + app: &AppHandle, + progress: &DownloadProgress, + ) -> anyhow::Result<&'a mut PlayerInfo>; +} + +impl GetOrInitPlayerInfo for Option { + async fn get_or_init<'a>( + &'a mut self, + app: &AppHandle, + progress: &DownloadProgress, + ) -> anyhow::Result<&'a mut PlayerInfo> { + if let Some(info) = self { + return Ok(info); + } + + let bili_client = app.get_bili_client(); + let info = bili_client + .get_player_info(progress.aid, progress.cid) + .await + .context("获取播放器信息失败")?; + + Ok(self.insert(info)) + } +} diff --git a/src-tauri/src/downloader/tasks/embed_chapter_task.rs b/src-tauri/src/downloader/tasks/embed_chapter_task.rs new file mode 100644 index 0000000..5e5cf31 --- /dev/null +++ b/src-tauri/src/downloader/tasks/embed_chapter_task.rs @@ -0,0 +1,14 @@ +use serde::{Deserialize, Serialize}; +use specta::Type; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)] +pub struct EmbedChapterTask { + pub selected: bool, + pub completed: bool, +} + +impl EmbedChapterTask { + pub fn is_completed(&self) -> bool { + !self.selected || self.completed + } +} diff --git a/src-tauri/src/downloader/tasks/mod.rs b/src-tauri/src/downloader/tasks/mod.rs index d8acc56..ed66dad 100644 --- a/src-tauri/src/downloader/tasks/mod.rs +++ b/src-tauri/src/downloader/tasks/mod.rs @@ -1,6 +1,7 @@ pub mod audio_task; pub mod cover_task; pub mod danmaku_task; +pub mod embed_chapter_task; pub mod json_task; pub mod merge_task; pub mod nfo_task; diff --git a/src-tauri/src/types/player_info.rs b/src-tauri/src/types/player_info.rs index 7f76731..307bd93 100644 --- a/src-tauri/src/types/player_info.rs +++ b/src-tauri/src/types/player_info.rs @@ -44,6 +44,26 @@ pub struct PlayerInfo { pub is_upower_exclusive_with_qa: bool, } +impl PlayerInfo { + pub fn generate_chapter_metadata(&self) -> String { + use std::fmt::Write; + + let mut metadata_content = ";FFMETADATA1\n".to_string(); + + for view_point in &self.view_points { + let start = view_point.from; + let end = view_point.to; + let title = &view_point.content; + let _ = writeln!( + &mut metadata_content, + "[CHAPTER]\nTIMEBASE=1/1\nSTART={start}\nEND={end}\ntitle={title}\n" + ); + } + + metadata_content + } +} + #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)] pub struct IpInfo { pub ip: String, diff --git a/src-tauri/src/utils.rs b/src-tauri/src/utils.rs index 7983de6..c75e45a 100644 --- a/src-tauri/src/utils.rs +++ b/src-tauri/src/utils.rs @@ -1,7 +1,7 @@ use std::{ fs::File, io::{BufReader, Read}, - path::Path, + path::{Path, PathBuf}, }; use anyhow::{anyhow, Context}; @@ -176,3 +176,13 @@ pub fn seconds_to_srt_time(seconds: f64) -> String { let h = total_m / 60; format!("{h:02}:{m:02}:{s:02},{ms:03}") } + +pub fn get_ffmpeg_program() -> anyhow::Result { + let ffmpeg_program = std::env::current_exe() + .context("获取当前可执行文件路径失败")? + .parent() + .context("获取当前可执行文件所在目录失败")? + .join("com.lanyeeee.bilibili-video-downloader-ffmpeg"); + + Ok(ffmpeg_program) +} diff --git a/src/bindings.ts b/src/bindings.ts index 697a937..c2b48fd 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -239,7 +239,7 @@ export type CntInfo = { collect: number; play: number; thumb_up: number; share: export type CntInfoInMedia = { collect: number; play: number; danmaku: number; vt: number; play_switch: number; reply: number; view_text_1: string } export type CodecType = "Unknown" | "Audio" | "AVC" | "HEVC" | "AV1" export type CommandError = { err_title: string; err_message: string } -export type Config = { download_dir: string; enable_file_logger: boolean; sessdata: string; prefer_video_quality: PreferVideoQuality; prefer_codec_type: PreferCodecType; prefer_audio_quality: PreferAudioQuality; download_video: boolean; download_audio: boolean; auto_merge: boolean; download_xml_danmaku: boolean; download_ass_danmaku: boolean; download_json_danmaku: boolean; download_subtitle: boolean; download_cover: boolean; download_nfo: boolean; download_json: boolean; dir_fmt: string; dir_fmt_for_part: string; time_fmt: string; proxy_mode: ProxyMode; proxy_host: string; proxy_port: number; task_concurrency: number; task_download_interval_sec: number; chunk_concurrency: number; chunk_download_interval_sec: number; danmaku_config: CanvasConfig } +export type Config = { download_dir: string; enable_file_logger: boolean; sessdata: string; prefer_video_quality: PreferVideoQuality; prefer_codec_type: PreferCodecType; prefer_audio_quality: PreferAudioQuality; download_video: boolean; download_audio: boolean; auto_merge: boolean; embed_chapter: boolean; download_xml_danmaku: boolean; download_ass_danmaku: boolean; download_json_danmaku: boolean; download_subtitle: boolean; download_cover: boolean; download_nfo: boolean; download_json: boolean; dir_fmt: string; dir_fmt_for_part: string; time_fmt: string; proxy_mode: ProxyMode; proxy_host: string; proxy_port: number; task_concurrency: number; task_download_interval_sec: number; chunk_concurrency: number; chunk_download_interval_sec: number; danmaku_config: CanvasConfig } export type Consulting = { consulting_flag: boolean; consulting_url: string } export type ContentAttr = { text: string; bg_color: string; bg_color_night: string; img: string; multi_img: MultiImg } export type ContentList = { bold: boolean; content: string; number: string } @@ -255,9 +255,10 @@ export type Dimension = { width: number; height: number; rotate: number } export type DimensionInBangumi = { height: number; rotate: number; width: number } export type DimensionInWatchLater = { width: number; height: number; rotate: number } export type DownloadEvent = { event: "Speed"; data: { speed: string } } | { event: "TaskCreate"; data: { state: DownloadTaskState; progress: DownloadProgress } } | { event: "TaskStateUpdate"; data: { task_id: string; state: DownloadTaskState } } | { event: "TaskSleeping"; data: { task_id: string; remaining_sec: number } } | { event: "TaskDelete"; data: { task_id: string } } | { event: "ProgressPreparing"; data: { task_id: string } } | { event: "ProgressUpdate"; data: { progress: DownloadProgress } } -export type DownloadProgress = { task_id: string; episode_type: EpisodeType; aid: number; bvid: string | null; cid: number; ep_id: number | null; duration: number; pub_ts: number; collection_title: string; part_title: string | null; part_order: number | null; episode_title: string; episode_order: number; up_name: string | null; up_uid: number | null; up_avatar: string | null; episode_dir: string; filename: string; video_task: VideoTask; audio_task: AudioTask; merge_task: MergeTask; subtitle_task: SubtitleTask; danmaku_task: DanmakuTask; cover_task: CoverTask; nfo_task: NfoTask; json_task: JsonTask; create_ts: number; completed_ts: number | null } +export type DownloadProgress = { task_id: string; episode_type: EpisodeType; aid: number; bvid: string | null; cid: number; ep_id: number | null; duration: number; pub_ts: number; collection_title: string; part_title: string | null; part_order: number | null; episode_title: string; episode_order: number; up_name: string | null; up_uid: number | null; up_avatar: string | null; episode_dir: string; filename: string; video_task: VideoTask; audio_task: AudioTask; merge_task: MergeTask; embed_chapter_task: EmbedChapterTask; subtitle_task: SubtitleTask; danmaku_task: DanmakuTask; cover_task: CoverTask; nfo_task: NfoTask; json_task: JsonTask; create_ts: number; completed_ts: number | null } export type DownloadTaskState = "Pending" | "Downloading" | "Paused" | "Completed" | "Failed" export type Ed = { end: number; start: number } +export type EmbedChapterTask = { selected: boolean; completed: boolean } export type EpInBangumi = { aid: number; badge: string; badge_info: BadgeInfoInBangumi; badge_type: number | null; bvid: string | null; cid: number; cover: string; dimension: DimensionInBangumi | null; duration: number | null; enable_vt: boolean; ep_id: number; from: string | null; id: number; is_view_hide: boolean; link: string; link_type: string | null; long_title: string | null; pub_time: number; pv: number; release_date: string | null; rights: RightsInBangumiEp | null; section_type: number; share_copy: string | null; share_url: string | null; short_link: string | null; showDrmLoginDialog: boolean; show_title: string | null; skip: Skip | null; status: number; subtitle: string | null; title: string; vid: string | null; icon_font: IconFont | null } export type EpInBangumiFollow = { season_id: number; media_id: number; season_type: number; season_type_name: string; title: string; cover: string; total_count: number; is_finish: number; is_started: number; is_play: number; badge: string; badge_type: number; rights: RightsInBangumiFollow; stat: StatInBangumiFollow; new_ep: NewEpInBangumiFollow; rating: RatingInBangumiFollow | null; square_cover: string; season_status: number; season_title: string; badge_ep: string; media_attr: number; season_attr: number; evaluate: string; areas: AreaInBangumiFollow[]; subtitle: string; first_ep: number; can_watch: number; release_date_show: string | null; series: SeriesInBangumiFollow; publish: PublishInBangumiFollow; mode: number; section: SectionInBangumiFollow[]; url: string; badge_info: BadgeInfoInBangumiFollow; renewal_time: string | null; first_ep_info: FirstEpInfo; formal_ep_count: number | null; short_url: string; badge_infos: BadgeInfos | null; season_version: string | null; horizontal_cover_16_9: string | null; horizontal_cover_16_10: string | null; subtitle_14: string | null; viewable_crowd_type: number; producers?: Producer[]; summary: string; styles?: string[]; follow_status: number; is_new: number; progress: string; both_follow: boolean; subtitle_25: string | null } export type EpInCheese = { aid: number; catalogue_index: number; cid: number; cover: string; duration: number; ep_status: number; episode_can_view: boolean; from: string; id: number; index: number; label: string | null; page: number; play: number; play_way: number; playable: boolean; release_date: number; show_vt: boolean; status: number; subtitle: string; title: string; watched: boolean; watchedHistory: number }