mirror of
https://github.com/lanyeeee/bilibili-video-downloader.git
synced 2026-09-08 17:16:52 +08:00
feat: 后端支持配置下载文件已存在时的行为
This commit is contained in:
@@ -42,6 +42,7 @@ pub struct Config {
|
|||||||
pub chunk_concurrency: usize,
|
pub chunk_concurrency: usize,
|
||||||
pub chunk_download_interval_sec: u64,
|
pub chunk_download_interval_sec: u64,
|
||||||
pub danmaku_config: CanvasConfig,
|
pub danmaku_config: CanvasConfig,
|
||||||
|
pub file_exist_action: FileExistAction,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
@@ -151,6 +152,7 @@ impl Config {
|
|||||||
chunk_concurrency: 16,
|
chunk_concurrency: 16,
|
||||||
chunk_download_interval_sec: 0,
|
chunk_download_interval_sec: 0,
|
||||||
danmaku_config: CanvasConfig::default(),
|
danmaku_config: CanvasConfig::default(),
|
||||||
|
file_exist_action: FileExistAction::Overwrite,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -162,3 +164,10 @@ pub enum ProxyMode {
|
|||||||
System,
|
System,
|
||||||
Custom,
|
Custom,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Type)]
|
||||||
|
pub enum FileExistAction {
|
||||||
|
#[default]
|
||||||
|
Overwrite,
|
||||||
|
Skip,
|
||||||
|
}
|
||||||
|
|||||||
@@ -330,12 +330,12 @@ impl DownloadProgress {
|
|||||||
pub fn mark_uncompleted(&mut self) {
|
pub fn mark_uncompleted(&mut self) {
|
||||||
self.video_task.mark_uncompleted();
|
self.video_task.mark_uncompleted();
|
||||||
self.audio_task.mark_uncompleted();
|
self.audio_task.mark_uncompleted();
|
||||||
self.video_process_task.completed = false;
|
self.video_process_task.mark_uncompleted();
|
||||||
self.danmaku_task.completed = false;
|
self.danmaku_task.mark_uncompleted();
|
||||||
self.subtitle_task.completed = false;
|
self.subtitle_task.mark_uncompleted();
|
||||||
self.cover_task.completed = false;
|
self.cover_task.mark_uncompleted();
|
||||||
self.nfo_task.completed = false;
|
self.nfo_task.mark_uncompleted();
|
||||||
self.json_task.completed = false;
|
self.json_task.mark_uncompleted();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_ids_string(&self) -> String {
|
pub fn get_ids_string(&self) -> String {
|
||||||
@@ -656,6 +656,7 @@ impl Tasks {
|
|||||||
content_length: 0,
|
content_length: 0,
|
||||||
chunks: Vec::new(),
|
chunks: Vec::new(),
|
||||||
completed: false,
|
completed: false,
|
||||||
|
skipped: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let audio = AudioTask {
|
let audio = AudioTask {
|
||||||
@@ -665,6 +666,7 @@ impl Tasks {
|
|||||||
content_length: 0,
|
content_length: 0,
|
||||||
chunks: Vec::new(),
|
chunks: Vec::new(),
|
||||||
completed: false,
|
completed: false,
|
||||||
|
skipped: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let video_process = VideoProcessTask {
|
let video_process = VideoProcessTask {
|
||||||
@@ -679,6 +681,7 @@ impl Tasks {
|
|||||||
ass_selected: config.download_ass_danmaku,
|
ass_selected: config.download_ass_danmaku,
|
||||||
json_selected: config.download_json_danmaku,
|
json_selected: config.download_json_danmaku,
|
||||||
completed: false,
|
completed: false,
|
||||||
|
skipped: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let subtitle = SubtitleTask {
|
let subtitle = SubtitleTask {
|
||||||
@@ -695,6 +698,7 @@ impl Tasks {
|
|||||||
let nfo = NfoTask {
|
let nfo = NfoTask {
|
||||||
selected: config.download_nfo,
|
selected: config.download_nfo,
|
||||||
completed: false,
|
completed: false,
|
||||||
|
skipped: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let json = JsonTask {
|
let json = JsonTask {
|
||||||
|
|||||||
@@ -292,7 +292,7 @@ impl DownloadTask {
|
|||||||
.process(self, &progress)
|
.process(self, &progress)
|
||||||
.await
|
.await
|
||||||
.context(format!("{ids_string} `{filename}`下载视频文件失败"))?;
|
.context(format!("{ids_string} `{filename}`下载视频文件失败"))?;
|
||||||
tracing::debug!("{ids_string} `{filename}`视频下载完成");
|
tracing::debug!("{ids_string} `{filename}`视频下载任务完成");
|
||||||
}
|
}
|
||||||
|
|
||||||
if !audio_task.is_completed() && audio_task.content_length != 0 {
|
if !audio_task.is_completed() && audio_task.content_length != 0 {
|
||||||
@@ -300,7 +300,7 @@ impl DownloadTask {
|
|||||||
.process(self, &progress)
|
.process(self, &progress)
|
||||||
.await
|
.await
|
||||||
.context(format!("{ids_string} `{filename}`下载音频文件失败"))?;
|
.context(format!("{ids_string} `{filename}`下载音频文件失败"))?;
|
||||||
tracing::debug!("{ids_string} `{filename}`音频下载完成");
|
tracing::debug!("{ids_string} `{filename}`音频下载任务完成");
|
||||||
}
|
}
|
||||||
|
|
||||||
if !video_process_task.is_completed() {
|
if !video_process_task.is_completed() {
|
||||||
@@ -308,7 +308,7 @@ impl DownloadTask {
|
|||||||
.process(self, &progress, &mut player_info)
|
.process(self, &progress, &mut player_info)
|
||||||
.await
|
.await
|
||||||
.context(format!("{ids_string} `{filename}`视频处理失败"))?;
|
.context(format!("{ids_string} `{filename}`视频处理失败"))?;
|
||||||
tracing::debug!("{ids_string} `{filename}`视频处理完成");
|
tracing::debug!("{ids_string} `{filename}`视频处理任务完成");
|
||||||
}
|
}
|
||||||
|
|
||||||
if !danmaku_task.is_completed() {
|
if !danmaku_task.is_completed() {
|
||||||
@@ -316,7 +316,7 @@ impl DownloadTask {
|
|||||||
.process(self, &progress)
|
.process(self, &progress)
|
||||||
.await
|
.await
|
||||||
.context(format!("{ids_string} `{filename}`下载弹幕失败"))?;
|
.context(format!("{ids_string} `{filename}`下载弹幕失败"))?;
|
||||||
tracing::debug!("{ids_string} `{filename}`弹幕下载完成");
|
tracing::debug!("{ids_string} `{filename}`弹幕下载任务完成");
|
||||||
}
|
}
|
||||||
|
|
||||||
if !subtitle_task.is_completed() {
|
if !subtitle_task.is_completed() {
|
||||||
@@ -324,7 +324,7 @@ impl DownloadTask {
|
|||||||
.process(self, &progress, &mut player_info)
|
.process(self, &progress, &mut player_info)
|
||||||
.await
|
.await
|
||||||
.context(format!("{ids_string} `{filename}`下载字幕失败"))?;
|
.context(format!("{ids_string} `{filename}`下载字幕失败"))?;
|
||||||
tracing::debug!("{ids_string} `{filename}`字幕下载完成");
|
tracing::debug!("{ids_string} `{filename}`字幕下载任务完成");
|
||||||
}
|
}
|
||||||
|
|
||||||
if !cover_task.is_completed() {
|
if !cover_task.is_completed() {
|
||||||
@@ -332,7 +332,7 @@ impl DownloadTask {
|
|||||||
.process(self, &progress)
|
.process(self, &progress)
|
||||||
.await
|
.await
|
||||||
.context(format!("{ids_string} `{filename}`下载封面失败"))?;
|
.context(format!("{ids_string} `{filename}`下载封面失败"))?;
|
||||||
tracing::debug!("{ids_string} `{filename}`封面下载完成");
|
tracing::debug!("{ids_string} `{filename}`封面下载任务完成");
|
||||||
}
|
}
|
||||||
|
|
||||||
if !nfo_task.is_completed() {
|
if !nfo_task.is_completed() {
|
||||||
@@ -340,7 +340,7 @@ impl DownloadTask {
|
|||||||
.process(self, &progress, &mut episode_info)
|
.process(self, &progress, &mut episode_info)
|
||||||
.await
|
.await
|
||||||
.context(format!("{ids_string} `{filename}`下载NFO失败"))?;
|
.context(format!("{ids_string} `{filename}`下载NFO失败"))?;
|
||||||
tracing::debug!("{ids_string} `{filename}`NFO下载完成");
|
tracing::debug!("{ids_string} `{filename}`NFO下载任务完成");
|
||||||
}
|
}
|
||||||
|
|
||||||
if !json_task.is_completed() {
|
if !json_task.is_completed() {
|
||||||
@@ -348,7 +348,7 @@ impl DownloadTask {
|
|||||||
.process(self, &progress, &mut episode_info)
|
.process(self, &progress, &mut episode_info)
|
||||||
.await
|
.await
|
||||||
.context(format!("{ids_string} `{filename}`下载JSON元数据失败"))?;
|
.context(format!("{ids_string} `{filename}`下载JSON元数据失败"))?;
|
||||||
tracing::debug!("{ids_string} `{filename}`JSON元数据下载完成");
|
tracing::debug!("{ids_string} `{filename}`JSON元数据下载任务完成");
|
||||||
}
|
}
|
||||||
|
|
||||||
let completed_ts = SystemTime::now()
|
let completed_ts = SystemTime::now()
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use tauri::AppHandle;
|
|||||||
use tokio::task::JoinSet;
|
use tokio::task::JoinSet;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
config::FileExistAction,
|
||||||
downloader::{
|
downloader::{
|
||||||
download_chunk_task::DownloadChunkTask, download_progress::DownloadProgress,
|
download_chunk_task::DownloadChunkTask, download_progress::DownloadProgress,
|
||||||
download_task::DownloadTask, media_chunk::MediaChunk,
|
download_task::DownloadTask, media_chunk::MediaChunk,
|
||||||
@@ -28,6 +29,7 @@ use crate::{
|
|||||||
const CHUNK_SIZE: u64 = 2 * 1024 * 1024; // 2MB
|
const CHUNK_SIZE: u64 = 2 * 1024 * 1024; // 2MB
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||||
|
#[serde(default)]
|
||||||
pub struct AudioTask {
|
pub struct AudioTask {
|
||||||
pub selected: bool,
|
pub selected: bool,
|
||||||
pub url: String,
|
pub url: String,
|
||||||
@@ -35,6 +37,7 @@ pub struct AudioTask {
|
|||||||
pub content_length: u64,
|
pub content_length: u64,
|
||||||
pub chunks: Vec<MediaChunk>,
|
pub chunks: Vec<MediaChunk>,
|
||||||
pub completed: bool,
|
pub completed: bool,
|
||||||
|
pub skipped: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AudioTask {
|
impl AudioTask {
|
||||||
@@ -286,6 +289,7 @@ impl AudioTask {
|
|||||||
self.chunks.iter_mut().for_each(|chunk| {
|
self.chunks.iter_mut().for_each(|chunk| {
|
||||||
chunk.completed = false;
|
chunk.completed = false;
|
||||||
});
|
});
|
||||||
|
self.skipped = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_completed(&self) -> bool {
|
pub fn is_completed(&self) -> bool {
|
||||||
@@ -298,10 +302,6 @@ impl AudioTask {
|
|||||||
progress: &DownloadProgress,
|
progress: &DownloadProgress,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let (episode_dir, filename) = (&progress.episode_dir, &progress.filename);
|
let (episode_dir, filename) = (&progress.episode_dir, &progress.filename);
|
||||||
|
|
||||||
let temp_file_path = episode_dir.join(format!(
|
|
||||||
"{filename}.m4a.com.lanyeeee.bilibili-video-downloader"
|
|
||||||
));
|
|
||||||
let (audio_task, episode_title, ids_string) = {
|
let (audio_task, episode_title, ids_string) = {
|
||||||
(
|
(
|
||||||
progress.audio_task.clone(),
|
progress.audio_task.clone(),
|
||||||
@@ -310,6 +310,21 @@ impl AudioTask {
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let m4a_path = episode_dir.join(format!("{filename}.m4a"));
|
||||||
|
let file_exist_action = download_task.app.get_config().read().file_exist_action;
|
||||||
|
if file_exist_action == FileExistAction::Skip && m4a_path.exists() {
|
||||||
|
tracing::debug!("{ids_string} `{filename}`音频文件已存在,跳过下载");
|
||||||
|
download_task.update_progress(|p| {
|
||||||
|
p.audio_task.skipped = true;
|
||||||
|
p.audio_task.completed = true;
|
||||||
|
});
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let temp_file_path = episode_dir.join(format!(
|
||||||
|
"{filename}.m4a.com.lanyeeee.bilibili-video-downloader"
|
||||||
|
));
|
||||||
|
|
||||||
let file = if temp_file_path.exists() {
|
let file = if temp_file_path.exists() {
|
||||||
// 如果文件已存在,则打开它
|
// 如果文件已存在,则打开它
|
||||||
OpenOptions::new()
|
OpenOptions::new()
|
||||||
@@ -389,7 +404,6 @@ impl AudioTask {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 重命名临时文件
|
// 重命名临时文件
|
||||||
let m4a_path = episode_dir.join(format!("{filename}.m4a"));
|
|
||||||
if m4a_path.exists() {
|
if m4a_path.exists() {
|
||||||
std::fs::remove_file(&m4a_path)
|
std::fs::remove_file(&m4a_path)
|
||||||
.context(format!("删除已存在的音频文件`{}`失败", m4a_path.display()))?;
|
.context(format!("删除已存在的音频文件`{}`失败", m4a_path.display()))?;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||||
|
#[serde(default)]
|
||||||
pub struct CoverTask {
|
pub struct CoverTask {
|
||||||
pub selected: bool,
|
pub selected: bool,
|
||||||
pub url: String,
|
pub url: String,
|
||||||
@@ -17,6 +18,10 @@ pub struct CoverTask {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl CoverTask {
|
impl CoverTask {
|
||||||
|
pub fn mark_uncompleted(&mut self) {
|
||||||
|
self.completed = false;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_completed(&self) -> bool {
|
pub fn is_completed(&self) -> bool {
|
||||||
!self.selected || self.completed
|
!self.selected || self.completed
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use specta::Type;
|
use specta::Type;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
config::FileExistAction,
|
||||||
danmaku_xml_to_ass::xml_to_ass,
|
danmaku_xml_to_ass::xml_to_ass,
|
||||||
downloader::{download_progress::DownloadProgress, download_task::DownloadTask},
|
downloader::{download_progress::DownloadProgress, download_task::DownloadTask},
|
||||||
extensions::AppHandleExt,
|
extensions::AppHandleExt,
|
||||||
@@ -12,15 +13,22 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||||
|
#[serde(default)]
|
||||||
#[allow(clippy::struct_excessive_bools)]
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
pub struct DanmakuTask {
|
pub struct DanmakuTask {
|
||||||
pub xml_selected: bool,
|
pub xml_selected: bool,
|
||||||
pub ass_selected: bool,
|
pub ass_selected: bool,
|
||||||
pub json_selected: bool,
|
pub json_selected: bool,
|
||||||
pub completed: bool,
|
pub completed: bool,
|
||||||
|
pub skipped: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DanmakuTask {
|
impl DanmakuTask {
|
||||||
|
pub fn mark_uncompleted(&mut self) {
|
||||||
|
self.completed = false;
|
||||||
|
self.skipped = false;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_completed(&self) -> bool {
|
pub fn is_completed(&self) -> bool {
|
||||||
!self.xml_selected && !self.ass_selected && !self.json_selected || self.completed
|
!self.xml_selected && !self.ass_selected && !self.json_selected || self.completed
|
||||||
}
|
}
|
||||||
@@ -32,6 +40,27 @@ impl DanmakuTask {
|
|||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let danmaku_task = &progress.danmaku_task;
|
let danmaku_task = &progress.danmaku_task;
|
||||||
let (episode_dir, filename) = (&progress.episode_dir, &progress.filename);
|
let (episode_dir, filename) = (&progress.episode_dir, &progress.filename);
|
||||||
|
let ids_string = progress.get_ids_string();
|
||||||
|
|
||||||
|
let xml_path = episode_dir.join(format!("{filename}.弹幕.xml"));
|
||||||
|
let ass_path = episode_dir.join(format!("{filename}.弹幕.ass"));
|
||||||
|
let json_path = episode_dir.join(format!("{filename}.弹幕.json"));
|
||||||
|
|
||||||
|
let file_exist_action = download_task.app.get_config().read().file_exist_action;
|
||||||
|
if file_exist_action == FileExistAction::Skip {
|
||||||
|
let skip_xml = !danmaku_task.xml_selected || xml_path.exists();
|
||||||
|
let skip_ass = !danmaku_task.ass_selected || ass_path.exists();
|
||||||
|
let skip_json = !danmaku_task.json_selected || json_path.exists();
|
||||||
|
|
||||||
|
if skip_xml && skip_ass && skip_json {
|
||||||
|
tracing::debug!("{ids_string} `{filename}`弹幕文件已存在,跳过下载");
|
||||||
|
download_task.update_progress(|p| {
|
||||||
|
p.danmaku_task.skipped = true;
|
||||||
|
p.danmaku_task.completed = true;
|
||||||
|
});
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let bili_client = download_task.app.get_bili_client();
|
let bili_client = download_task.app.get_bili_client();
|
||||||
let replies = bili_client
|
let replies = bili_client
|
||||||
@@ -44,14 +73,12 @@ impl DanmakuTask {
|
|||||||
.context("将弹幕转换为XML失败")?;
|
.context("将弹幕转换为XML失败")?;
|
||||||
|
|
||||||
if danmaku_task.xml_selected {
|
if danmaku_task.xml_selected {
|
||||||
let xml_path = episode_dir.join(format!("{filename}.弹幕.xml"));
|
|
||||||
std::fs::write(&xml_path, &xml)
|
std::fs::write(&xml_path, &xml)
|
||||||
.context(format!("保存弹幕XML到`{}`失败", xml_path.display()))?;
|
.context(format!("保存弹幕XML到`{}`失败", xml_path.display()))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if danmaku_task.ass_selected {
|
if danmaku_task.ass_selected {
|
||||||
let config = download_task.app.get_config().read().danmaku_config.clone();
|
let config = download_task.app.get_config().read().danmaku_config.clone();
|
||||||
let ass_path = episode_dir.join(format!("{filename}.弹幕.ass"));
|
|
||||||
let ass_file = File::create(&ass_path)
|
let ass_file = File::create(&ass_path)
|
||||||
.context(format!("创建弹幕ASS文件`{}`失败", ass_path.display()))?;
|
.context(format!("创建弹幕ASS文件`{}`失败", ass_path.display()))?;
|
||||||
let title = filename.to_string();
|
let title = filename.to_string();
|
||||||
@@ -59,7 +86,6 @@ impl DanmakuTask {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if danmaku_task.json_selected {
|
if danmaku_task.json_selected {
|
||||||
let json_path = episode_dir.join(format!("{filename}.弹幕.json"));
|
|
||||||
let json_string = serde_json::to_string(&replies).context("将弹幕转换为JSON失败")?;
|
let json_string = serde_json::to_string(&replies).context("将弹幕转换为JSON失败")?;
|
||||||
std::fs::write(&json_path, json_string)
|
std::fs::write(&json_path, json_string)
|
||||||
.context(format!("保存弹幕JSON到`{}`失败", json_path.display()))?;
|
.context(format!("保存弹幕JSON到`{}`失败", json_path.display()))?;
|
||||||
|
|||||||
@@ -11,12 +11,17 @@ use crate::downloader::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||||
|
#[serde(default)]
|
||||||
pub struct JsonTask {
|
pub struct JsonTask {
|
||||||
pub selected: bool,
|
pub selected: bool,
|
||||||
pub completed: bool,
|
pub completed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl JsonTask {
|
impl JsonTask {
|
||||||
|
pub fn mark_uncompleted(&mut self) {
|
||||||
|
self.completed = false;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_completed(&self) -> bool {
|
pub fn is_completed(&self) -> bool {
|
||||||
!self.selected || self.completed
|
!self.selected || self.completed
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use specta::Type;
|
|||||||
use yaserde::{YaDeserialize, YaSerialize};
|
use yaserde::{YaDeserialize, YaSerialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
config::FileExistAction,
|
||||||
downloader::{
|
downloader::{
|
||||||
download_progress::DownloadProgress,
|
download_progress::DownloadProgress,
|
||||||
download_task::DownloadTask,
|
download_task::DownloadTask,
|
||||||
@@ -19,12 +20,19 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||||
|
#[serde(default)]
|
||||||
pub struct NfoTask {
|
pub struct NfoTask {
|
||||||
pub selected: bool,
|
pub selected: bool,
|
||||||
pub completed: bool,
|
pub completed: bool,
|
||||||
|
pub skipped: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NfoTask {
|
impl NfoTask {
|
||||||
|
pub fn mark_uncompleted(&mut self) {
|
||||||
|
self.completed = false;
|
||||||
|
self.skipped = false;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_completed(&self) -> bool {
|
pub fn is_completed(&self) -> bool {
|
||||||
!self.selected || self.completed
|
!self.selected || self.completed
|
||||||
}
|
}
|
||||||
@@ -35,105 +43,188 @@ impl NfoTask {
|
|||||||
progress: &DownloadProgress,
|
progress: &DownloadProgress,
|
||||||
episode_info: &mut Option<EpisodeInfo>,
|
episode_info: &mut Option<EpisodeInfo>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let (episode_dir, filename) = (&progress.episode_dir, &progress.filename);
|
|
||||||
|
|
||||||
let episode_info = episode_info
|
let episode_info = episode_info
|
||||||
.get_or_init(&download_task.app, progress)
|
.get_or_init(&download_task.app, progress)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let bili_client = download_task.app.get_bili_client();
|
|
||||||
|
|
||||||
match episode_info {
|
match episode_info {
|
||||||
EpisodeInfo::Normal(info) => {
|
EpisodeInfo::Normal(info) => {
|
||||||
let tags = bili_client
|
self.process_normal(download_task, progress, info).await?;
|
||||||
.get_tags(progress.aid)
|
|
||||||
.await
|
|
||||||
.context("获取视频标签失败")?;
|
|
||||||
let movie_nfo = info
|
|
||||||
.to_movie_nfo(tags)
|
|
||||||
.context("将普通视频信息转换为movie NFO失败")?;
|
|
||||||
let nfo_path = episode_dir.join(format!("{filename}.nfo"));
|
|
||||||
std::fs::write(&nfo_path, movie_nfo)
|
|
||||||
.context(format!("保存普通视频NFO到`{}`失败", nfo_path.display()))?;
|
|
||||||
|
|
||||||
if let Some(ugc_season) = &info.ugc_season {
|
|
||||||
let collection_cover = &ugc_season.cover;
|
|
||||||
let (cover_data, ext) = bili_client
|
|
||||||
.get_cover_data_and_ext(collection_cover)
|
|
||||||
.await
|
|
||||||
.context("获取普通视频合集封面失败")?;
|
|
||||||
let cover_path = episode_dir.join(format!("poster.{ext}"));
|
|
||||||
std::fs::write(&cover_path, cover_data).context(format!(
|
|
||||||
"保存普通视频合集封面到`{}`失败",
|
|
||||||
cover_path.display()
|
|
||||||
))?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
EpisodeInfo::Bangumi(info, ep_id) => {
|
EpisodeInfo::Bangumi(info, ep_id) => {
|
||||||
let tvshow_nfo = info
|
self.process_bangumi(download_task, progress, info, ep_id)
|
||||||
.to_tvshow_nfo()
|
.await?;
|
||||||
.context("将番剧信息转换为tvshow NFO失败")?;
|
|
||||||
let tvshow_nfo_path = episode_dir.join("tvshow.nfo");
|
|
||||||
std::fs::write(&tvshow_nfo_path, tvshow_nfo)
|
|
||||||
.context(format!("保存番剧NFO到`{}`失败", tvshow_nfo_path.display()))?;
|
|
||||||
|
|
||||||
let episode_details_nfo = info
|
|
||||||
.to_episode_details_nfo(*ep_id)
|
|
||||||
.context("将番剧信息转换为episodedetail NFO失败")?;
|
|
||||||
let episode_details_nfo_path = episode_dir.join(format!("{filename}.nfo"));
|
|
||||||
std::fs::write(&episode_details_nfo_path, episode_details_nfo).context(format!(
|
|
||||||
"保存番剧NFO到`{}`失败",
|
|
||||||
episode_details_nfo_path.display()
|
|
||||||
))?;
|
|
||||||
|
|
||||||
let poster_url = &info.cover;
|
|
||||||
let (poster_data, ext) = bili_client
|
|
||||||
.get_cover_data_and_ext(poster_url)
|
|
||||||
.await
|
|
||||||
.context("获取番剧封面失败")?;
|
|
||||||
let poster_path = episode_dir.join(format!("poster.{ext}"));
|
|
||||||
std::fs::write(&poster_path, poster_data)
|
|
||||||
.context(format!("保存番剧封面到`{}`失败", poster_path.display()))?;
|
|
||||||
|
|
||||||
let fanart_url = &info.bkg_cover;
|
|
||||||
if !fanart_url.is_empty() {
|
|
||||||
let (fanart_data, ext) = bili_client
|
|
||||||
.get_cover_data_and_ext(fanart_url)
|
|
||||||
.await
|
|
||||||
.context("获取番剧封面失败")?;
|
|
||||||
let fanart_path = episode_dir.join(format!("fanart.{ext}"));
|
|
||||||
std::fs::write(&fanart_path, fanart_data)
|
|
||||||
.context(format!("保存番剧封面到`{}`失败", fanart_path.display()))?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
EpisodeInfo::Cheese(info, ep_id) => {
|
EpisodeInfo::Cheese(info, ep_id) => {
|
||||||
let tvshow_nfo = info
|
self.process_cheese(download_task, progress, info, ep_id)
|
||||||
.to_tvshow_nfo()
|
.await?;
|
||||||
.context("将课程信息转换为tvshow NFO失败")?;
|
|
||||||
let tvshow_nfo_path = episode_dir.join("tvshow.nfo");
|
|
||||||
std::fs::write(&tvshow_nfo_path, tvshow_nfo)
|
|
||||||
.context(format!("保存课程NFO到`{}`失败", tvshow_nfo_path.display()))?;
|
|
||||||
|
|
||||||
let episode_details_nfo = info
|
|
||||||
.to_episode_details_nfo(*ep_id)
|
|
||||||
.context("将课程信息转换为episodedetail NFO失败")?;
|
|
||||||
let episode_details_nfo_path = episode_dir.join(format!("{filename}.nfo"));
|
|
||||||
std::fs::write(&episode_details_nfo_path, episode_details_nfo).context(format!(
|
|
||||||
"保存课程NFO到`{}`失败",
|
|
||||||
episode_details_nfo_path.display()
|
|
||||||
))?;
|
|
||||||
|
|
||||||
let poster_url = &info.cover;
|
|
||||||
let (poster_data, ext) = bili_client
|
|
||||||
.get_cover_data_and_ext(poster_url)
|
|
||||||
.await
|
|
||||||
.context("获取课程封面失败")?;
|
|
||||||
let poster_path = episode_dir.join(format!("poster.{ext}"));
|
|
||||||
std::fs::write(&poster_path, poster_data)
|
|
||||||
.context(format!("保存课程封面到`{}`失败", poster_path.display()))?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn process_normal(
|
||||||
|
&self,
|
||||||
|
download_task: &Arc<DownloadTask>,
|
||||||
|
progress: &DownloadProgress,
|
||||||
|
info: &NormalInfo,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let (episode_dir, filename) = (&progress.episode_dir, &progress.filename);
|
||||||
|
let ids_string = progress.get_ids_string();
|
||||||
|
let nfo_path = episode_dir.join(format!("{filename}.nfo"));
|
||||||
|
|
||||||
|
let file_exist_action = download_task.app.get_config().read().file_exist_action;
|
||||||
|
if file_exist_action == FileExistAction::Skip && nfo_path.exists() {
|
||||||
|
tracing::debug!("{ids_string} `{filename}`NFO文件已存在,跳过下载");
|
||||||
|
download_task.update_progress(|p| {
|
||||||
|
p.nfo_task.skipped = true;
|
||||||
|
p.nfo_task.completed = true;
|
||||||
|
});
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let bili_client = download_task.app.get_bili_client();
|
||||||
|
|
||||||
|
let tags = bili_client
|
||||||
|
.get_tags(progress.aid)
|
||||||
|
.await
|
||||||
|
.context("获取视频标签失败")?;
|
||||||
|
let movie_nfo = info
|
||||||
|
.to_movie_nfo(tags)
|
||||||
|
.context("将普通视频信息转换为movie NFO失败")?;
|
||||||
|
std::fs::write(&nfo_path, movie_nfo)
|
||||||
|
.context(format!("保存普通视频NFO到`{}`失败", nfo_path.display()))?;
|
||||||
|
|
||||||
|
if let Some(ugc_season) = &info.ugc_season {
|
||||||
|
let collection_cover = &ugc_season.cover;
|
||||||
|
let (cover_data, ext) = bili_client
|
||||||
|
.get_cover_data_and_ext(collection_cover)
|
||||||
|
.await
|
||||||
|
.context("获取普通视频合集封面失败")?;
|
||||||
|
let cover_path = episode_dir.join(format!("poster.{ext}"));
|
||||||
|
std::fs::write(&cover_path, cover_data).context(format!(
|
||||||
|
"保存普通视频合集封面到`{}`失败",
|
||||||
|
cover_path.display()
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
download_task.update_progress(|p| p.nfo_task.completed = true);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn process_bangumi(
|
||||||
|
&self,
|
||||||
|
download_task: &Arc<DownloadTask>,
|
||||||
|
progress: &DownloadProgress,
|
||||||
|
info: &BangumiInfo,
|
||||||
|
ep_id: &i64,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let (episode_dir, filename) = (&progress.episode_dir, &progress.filename);
|
||||||
|
let ids_string = progress.get_ids_string();
|
||||||
|
let episode_details_nfo_path = episode_dir.join(format!("{filename}.nfo"));
|
||||||
|
|
||||||
|
let file_exist_action = download_task.app.get_config().read().file_exist_action;
|
||||||
|
if file_exist_action == FileExistAction::Skip && episode_details_nfo_path.exists() {
|
||||||
|
tracing::debug!("{ids_string} `{filename}`NFO文件已存在,跳过下载");
|
||||||
|
download_task.update_progress(|p| {
|
||||||
|
p.nfo_task.skipped = true;
|
||||||
|
p.nfo_task.completed = true;
|
||||||
|
});
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let bili_client = download_task.app.get_bili_client();
|
||||||
|
|
||||||
|
let tvshow_nfo = info
|
||||||
|
.to_tvshow_nfo()
|
||||||
|
.context("将番剧信息转换为tvshow NFO失败")?;
|
||||||
|
let tvshow_nfo_path = episode_dir.join("tvshow.nfo");
|
||||||
|
std::fs::write(&tvshow_nfo_path, tvshow_nfo)
|
||||||
|
.context(format!("保存番剧NFO到`{}`失败", tvshow_nfo_path.display()))?;
|
||||||
|
|
||||||
|
let episode_details_nfo = info
|
||||||
|
.to_episode_details_nfo(*ep_id)
|
||||||
|
.context("将番剧信息转换为episodedetail NFO失败")?;
|
||||||
|
let episode_details_nfo_path = episode_dir.join(format!("{filename}.nfo"));
|
||||||
|
std::fs::write(&episode_details_nfo_path, episode_details_nfo).context(format!(
|
||||||
|
"保存番剧NFO到`{}`失败",
|
||||||
|
episode_details_nfo_path.display()
|
||||||
|
))?;
|
||||||
|
|
||||||
|
let poster_url = &info.cover;
|
||||||
|
let (poster_data, ext) = bili_client
|
||||||
|
.get_cover_data_and_ext(poster_url)
|
||||||
|
.await
|
||||||
|
.context("获取番剧封面失败")?;
|
||||||
|
let poster_path = episode_dir.join(format!("poster.{ext}"));
|
||||||
|
std::fs::write(&poster_path, poster_data)
|
||||||
|
.context(format!("保存番剧封面到`{}`失败", poster_path.display()))?;
|
||||||
|
|
||||||
|
let fanart_url = &info.bkg_cover;
|
||||||
|
if !fanart_url.is_empty() {
|
||||||
|
let (fanart_data, ext) = bili_client
|
||||||
|
.get_cover_data_and_ext(fanart_url)
|
||||||
|
.await
|
||||||
|
.context("获取番剧封面失败")?;
|
||||||
|
let fanart_path = episode_dir.join(format!("fanart.{ext}"));
|
||||||
|
std::fs::write(&fanart_path, fanart_data)
|
||||||
|
.context(format!("保存番剧封面到`{}`失败", fanart_path.display()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
download_task.update_progress(|p| p.nfo_task.completed = true);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn process_cheese(
|
||||||
|
&self,
|
||||||
|
download_task: &Arc<DownloadTask>,
|
||||||
|
progress: &DownloadProgress,
|
||||||
|
info: &CheeseInfo,
|
||||||
|
ep_id: &i64,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let (episode_dir, filename) = (&progress.episode_dir, &progress.filename);
|
||||||
|
let ids_string = progress.get_ids_string();
|
||||||
|
let episode_details_nfo_path = episode_dir.join(format!("{filename}.nfo"));
|
||||||
|
|
||||||
|
let file_exist_action = download_task.app.get_config().read().file_exist_action;
|
||||||
|
if file_exist_action == FileExistAction::Skip && episode_details_nfo_path.exists() {
|
||||||
|
tracing::debug!("{ids_string} `{filename}`NFO文件已存在,跳过下载");
|
||||||
|
download_task.update_progress(|p| {
|
||||||
|
p.nfo_task.skipped = true;
|
||||||
|
p.nfo_task.completed = true;
|
||||||
|
});
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let bili_client = download_task.app.get_bili_client();
|
||||||
|
|
||||||
|
let tvshow_nfo = info
|
||||||
|
.to_tvshow_nfo()
|
||||||
|
.context("将课程信息转换为tvshow NFO失败")?;
|
||||||
|
let tvshow_nfo_path = episode_dir.join("tvshow.nfo");
|
||||||
|
std::fs::write(&tvshow_nfo_path, tvshow_nfo)
|
||||||
|
.context(format!("保存课程NFO到`{}`失败", tvshow_nfo_path.display()))?;
|
||||||
|
|
||||||
|
let episode_details_nfo = info
|
||||||
|
.to_episode_details_nfo(*ep_id)
|
||||||
|
.context("将课程信息转换为episodedetail NFO失败")?;
|
||||||
|
std::fs::write(&episode_details_nfo_path, episode_details_nfo).context(format!(
|
||||||
|
"保存课程NFO到`{}`失败",
|
||||||
|
episode_details_nfo_path.display()
|
||||||
|
))?;
|
||||||
|
|
||||||
|
let poster_url = &info.cover;
|
||||||
|
let (poster_data, ext) = bili_client
|
||||||
|
.get_cover_data_and_ext(poster_url)
|
||||||
|
.await
|
||||||
|
.context("获取课程封面失败")?;
|
||||||
|
let poster_path = episode_dir.join(format!("poster.{ext}"));
|
||||||
|
std::fs::write(&poster_path, poster_data)
|
||||||
|
.context(format!("保存课程封面到`{}`失败", poster_path.display()))?;
|
||||||
|
|
||||||
download_task.update_progress(|p| p.nfo_task.completed = true);
|
download_task.update_progress(|p| p.nfo_task.completed = true);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -12,12 +12,17 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||||
|
#[serde(default)]
|
||||||
pub struct SubtitleTask {
|
pub struct SubtitleTask {
|
||||||
pub selected: bool,
|
pub selected: bool,
|
||||||
pub completed: bool,
|
pub completed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SubtitleTask {
|
impl SubtitleTask {
|
||||||
|
pub fn mark_uncompleted(&mut self) {
|
||||||
|
self.completed = false;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_completed(&self) -> bool {
|
pub fn is_completed(&self) -> bool {
|
||||||
!self.selected || self.completed
|
!self.selected || self.completed
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||||
|
#[serde(default)]
|
||||||
#[allow(clippy::struct_excessive_bools)]
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
pub struct VideoProcessTask {
|
pub struct VideoProcessTask {
|
||||||
pub merge_selected: bool,
|
pub merge_selected: bool,
|
||||||
@@ -26,6 +27,10 @@ pub struct VideoProcessTask {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl VideoProcessTask {
|
impl VideoProcessTask {
|
||||||
|
pub fn mark_uncompleted(&mut self) {
|
||||||
|
self.completed = false;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_completed(&self) -> bool {
|
pub fn is_completed(&self) -> bool {
|
||||||
!self.merge_selected && !self.embed_chapter_selected && !self.embed_skip_selected
|
!self.merge_selected && !self.embed_chapter_selected && !self.embed_skip_selected
|
||||||
|| self.completed
|
|| self.completed
|
||||||
@@ -44,7 +49,6 @@ impl VideoProcessTask {
|
|||||||
.await
|
.await
|
||||||
.context("自动合并+嵌入章节元数据失败")?;
|
.context("自动合并+嵌入章节元数据失败")?;
|
||||||
} else if self.merge_selected {
|
} else if self.merge_selected {
|
||||||
println!("merge1");
|
|
||||||
self.merge(download_task, progress)
|
self.merge(download_task, progress)
|
||||||
.await
|
.await
|
||||||
.context("自动合并失败")?;
|
.context("自动合并失败")?;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use tauri::AppHandle;
|
|||||||
use tokio::task::JoinSet;
|
use tokio::task::JoinSet;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
config::FileExistAction,
|
||||||
downloader::{
|
downloader::{
|
||||||
download_chunk_task::DownloadChunkTask, download_progress::DownloadProgress,
|
download_chunk_task::DownloadChunkTask, download_progress::DownloadProgress,
|
||||||
download_task::DownloadTask, media_chunk::MediaChunk,
|
download_task::DownloadTask, media_chunk::MediaChunk,
|
||||||
@@ -28,6 +29,7 @@ use crate::{
|
|||||||
const CHUNK_SIZE: u64 = 2 * 1024 * 1024; // 2MB
|
const CHUNK_SIZE: u64 = 2 * 1024 * 1024; // 2MB
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||||
|
#[serde(default)]
|
||||||
pub struct VideoTask {
|
pub struct VideoTask {
|
||||||
pub selected: bool,
|
pub selected: bool,
|
||||||
pub url: String,
|
pub url: String,
|
||||||
@@ -36,6 +38,7 @@ pub struct VideoTask {
|
|||||||
pub content_length: u64,
|
pub content_length: u64,
|
||||||
pub chunks: Vec<MediaChunk>,
|
pub chunks: Vec<MediaChunk>,
|
||||||
pub completed: bool,
|
pub completed: bool,
|
||||||
|
pub skipped: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VideoTask {
|
impl VideoTask {
|
||||||
@@ -309,23 +312,20 @@ impl VideoTask {
|
|||||||
self.chunks.iter_mut().for_each(|chunk| {
|
self.chunks.iter_mut().for_each(|chunk| {
|
||||||
chunk.completed = false;
|
chunk.completed = false;
|
||||||
});
|
});
|
||||||
|
self.skipped = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_completed(&self) -> bool {
|
pub fn is_completed(&self) -> bool {
|
||||||
!self.selected || self.completed
|
!self.selected || self.completed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_lines)]
|
||||||
pub async fn process(
|
pub async fn process(
|
||||||
&self,
|
&self,
|
||||||
download_task: &Arc<DownloadTask>,
|
download_task: &Arc<DownloadTask>,
|
||||||
progress: &DownloadProgress,
|
progress: &DownloadProgress,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let (episode_dir, filename) = (&progress.episode_dir, &progress.filename);
|
let (episode_dir, filename) = (&progress.episode_dir, &progress.filename);
|
||||||
|
|
||||||
let temp_file_path = episode_dir.join(format!(
|
|
||||||
"{filename}.mp4.com.lanyeeee.bilibili-video-downloader"
|
|
||||||
));
|
|
||||||
|
|
||||||
let (video_task, episode_title, ids_string) = {
|
let (video_task, episode_title, ids_string) = {
|
||||||
let progress = download_task.progress.read();
|
let progress = download_task.progress.read();
|
||||||
(
|
(
|
||||||
@@ -335,6 +335,21 @@ impl VideoTask {
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mp4_path = episode_dir.join(format!("{filename}.mp4"));
|
||||||
|
let file_exist_action = download_task.app.get_config().read().file_exist_action;
|
||||||
|
if file_exist_action == FileExistAction::Skip && mp4_path.exists() {
|
||||||
|
tracing::debug!("{ids_string} `{filename}`视频文件已存在,跳过下载");
|
||||||
|
download_task.update_progress(|p| {
|
||||||
|
p.video_task.skipped = true;
|
||||||
|
p.video_task.completed = true;
|
||||||
|
});
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let temp_file_path = episode_dir.join(format!(
|
||||||
|
"{filename}.mp4.com.lanyeeee.bilibili-video-downloader"
|
||||||
|
));
|
||||||
|
|
||||||
let file = if temp_file_path.exists() {
|
let file = if temp_file_path.exists() {
|
||||||
// 如果临时文件已存在,则打开它
|
// 如果临时文件已存在,则打开它
|
||||||
OpenOptions::new()
|
OpenOptions::new()
|
||||||
@@ -416,7 +431,6 @@ impl VideoTask {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 重命名临时文件
|
// 重命名临时文件
|
||||||
let mp4_path = episode_dir.join(format!("{filename}.mp4"));
|
|
||||||
if mp4_path.exists() {
|
if mp4_path.exists() {
|
||||||
std::fs::remove_file(&mp4_path)
|
std::fs::remove_file(&mp4_path)
|
||||||
.context(format!("删除已存在的视频文件`{}`失败", mp4_path.display()))?;
|
.context(format!("删除已存在的视频文件`{}`失败", mp4_path.display()))?;
|
||||||
|
|||||||
+6
-5
@@ -185,7 +185,7 @@ export type AreaInBangumi = { id: number; name: string }
|
|||||||
export type AreaInBangumiFollow = { id: number; name: string }
|
export type AreaInBangumiFollow = { id: number; name: string }
|
||||||
export type ArgueInfo = { argue_msg: string; argue_type: number; argue_link: string }
|
export type ArgueInfo = { argue_msg: string; argue_type: number; argue_link: string }
|
||||||
export type AudioQuality = "Unknown" | "64K" | "132K" | "192K" | "Dolby" | "HiRes"
|
export type AudioQuality = "Unknown" | "64K" | "132K" | "192K" | "Dolby" | "HiRes"
|
||||||
export type AudioTask = { selected: boolean; url: string; audio_quality: AudioQuality; content_length: number; chunks: MediaChunk[]; completed: boolean }
|
export type AudioTask = { selected: boolean; url: string; audio_quality: AudioQuality; content_length: number; chunks: MediaChunk[]; completed: boolean; skipped: boolean }
|
||||||
export type Author = { mid: number; name: string; face: string }
|
export type Author = { mid: number; name: string; face: string }
|
||||||
export type BadgeInfoInBangumi = { bg_color: string; bg_color_night: string; text: string }
|
export type BadgeInfoInBangumi = { bg_color: string; bg_color_night: string; text: string }
|
||||||
export type BadgeInfoInBangumiFollow = { text: string | null; bg_color: string; bg_color_night: string; img: string | null; multi_img: MultiImg }
|
export type BadgeInfoInBangumiFollow = { text: string | null; bg_color: string; bg_color_night: string; img: string | null; multi_img: MultiImg }
|
||||||
@@ -255,7 +255,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 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 CodecType = "Unknown" | "Audio" | "AVC" | "HEVC" | "AV1"
|
||||||
export type CommandError = { err_title: string; err_message: string }
|
export type CommandError = { err_title: string; err_message: string }
|
||||||
export type Config = { download_dir: string; enable_file_logger: boolean; sessdata: string; video_quality_priority: VideoQuality[]; codec_type_priority: CodecType[]; audio_quality_priority: AudioQuality[]; download_video: boolean; download_audio: boolean; auto_merge: boolean; embed_chapter: boolean; embed_skip: 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; video_quality_priority: VideoQuality[]; codec_type_priority: CodecType[]; audio_quality_priority: AudioQuality[]; download_video: boolean; download_audio: boolean; auto_merge: boolean; embed_chapter: boolean; embed_skip: 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; file_exist_action: FileExistAction }
|
||||||
export type Consulting = { consulting_flag: boolean; consulting_url: string }
|
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 ContentAttr = { text: string; bg_color: string; bg_color_night: string; img: string; multi_img: MultiImg }
|
||||||
export type ContentList = { bold: boolean; content: string; number: string }
|
export type ContentList = { bold: boolean; content: string; number: string }
|
||||||
@@ -265,7 +265,7 @@ export type CreateBangumiDownloadTaskParams = { ep_ids: number[]; info: BangumiI
|
|||||||
export type CreateCheeseDownloadTaskParams = { ep_ids: number[]; info: CheeseInfo }
|
export type CreateCheeseDownloadTaskParams = { ep_ids: number[]; info: CheeseInfo }
|
||||||
export type CreateDownloadTaskParams = { Normal: CreateNormalDownloadTaskParams } | { Bangumi: CreateBangumiDownloadTaskParams } | { Cheese: CreateCheeseDownloadTaskParams }
|
export type CreateDownloadTaskParams = { Normal: CreateNormalDownloadTaskParams } | { Bangumi: CreateBangumiDownloadTaskParams } | { Cheese: CreateCheeseDownloadTaskParams }
|
||||||
export type CreateNormalDownloadTaskParams = { info: NormalInfo; aid_cid_pairs: ([number, number | null])[] }
|
export type CreateNormalDownloadTaskParams = { info: NormalInfo; aid_cid_pairs: ([number, number | null])[] }
|
||||||
export type DanmakuTask = { xml_selected: boolean; ass_selected: boolean; json_selected: boolean; completed: boolean }
|
export type DanmakuTask = { xml_selected: boolean; ass_selected: boolean; json_selected: boolean; completed: boolean; skipped: boolean }
|
||||||
export type DescV2 = { raw_text: string; type: number; biz_id: number }
|
export type DescV2 = { raw_text: string; type: number; biz_id: number }
|
||||||
export type DeviceType = "All" | "PC" | "Mobile" | "Pad" | "TV"
|
export type DeviceType = "All" | "PC" | "Mobile" | "Pad" | "TV"
|
||||||
export type Dimension = { width: number; height: number; rotate: number }
|
export type Dimension = { width: number; height: number; rotate: number }
|
||||||
@@ -289,6 +289,7 @@ export type Faq1Item = { answer: string; question: string }
|
|||||||
export type FavFolders = { count: number; list: Folder[] }
|
export type FavFolders = { count: number; list: Folder[] }
|
||||||
export type FavInfo = { info: Info; medias: MediaInFav[] | null; has_more: boolean; ttl: number }
|
export type FavInfo = { info: Info; medias: MediaInFav[] | null; has_more: boolean; ttl: number }
|
||||||
export type FavSearchResult = FavInfo
|
export type FavSearchResult = FavInfo
|
||||||
|
export type FileExistAction = "Overwrite" | "Skip"
|
||||||
export type FirstEpInfo = { id: number; cover: string; title: string; long_title: string | null; pub_time: string; duration: number }
|
export type FirstEpInfo = { id: number; cover: string; title: string; long_title: string | null; pub_time: string; duration: number }
|
||||||
export type Folder = { id: number; fid: number; mid: number; attr: number; title: string; fav_state: number; media_count: number }
|
export type Folder = { id: number; fid: number; mid: number; attr: number; title: string; fav_state: number; media_count: number }
|
||||||
export type GetBangumiFollowInfoParams = { vmid: number;
|
export type GetBangumiFollowInfoParams = { vmid: number;
|
||||||
@@ -324,7 +325,7 @@ export type MultiImg = { color: string; medium_remind: string }
|
|||||||
export type NewEp = { desc: string; id: number; is_new: number; title: string }
|
export type NewEp = { desc: string; id: number; is_new: number; title: string }
|
||||||
export type NewEpInBangumiFollow = { id: number | null; index_show: string | null; cover: string | null; title: string | null; long_title: string | null; pub_time: string | null; duration: number | null }
|
export type NewEpInBangumiFollow = { id: number | null; index_show: string | null; cover: string | null; title: string | null; long_title: string | null; pub_time: string | null; duration: number | null }
|
||||||
export type NewEpInSeason = { cover: string; id: number; index_show: string }
|
export type NewEpInSeason = { cover: string; id: number; index_show: string }
|
||||||
export type NfoTask = { selected: boolean; completed: boolean }
|
export type NfoTask = { selected: boolean; completed: boolean; skipped: boolean }
|
||||||
export type NormalInfo = { bvid: string; aid: number; videos: number; tid: number; tid_v2: number; tname: string; tname_v2: string; copyright: number; pic: string; title: string; pubdate: number; ctime: number; desc: string; desc_v2: DescV2[] | null; state: number; duration: number; rights: Rights; owner: OwnerInNormal; stat: StatInNormal; argue_info: ArgueInfo; dynamic: string; cid: number; dimension: Dimension; teenage_mode: number; is_chargeable_season: boolean; is_story: boolean; is_upower_exclusive: boolean; is_upower_play: boolean; is_upower_preview: boolean; enable_vt: number; vt_display: string; is_upower_exclusive_with_qa: boolean; no_cache: boolean; pages: PageInNormal[]; subtitle: SubtitleInNormal; staff: Staff[] | null; ugc_season: UgcSeason | null; is_season_display: boolean; user_garb: UserGarb; honor_reply: HonorReply; like_icon: string; need_jump_bv: boolean; disable_show_up_info: boolean; is_story_play: number; is_view_self: boolean }
|
export type NormalInfo = { bvid: string; aid: number; videos: number; tid: number; tid_v2: number; tname: string; tname_v2: string; copyright: number; pic: string; title: string; pubdate: number; ctime: number; desc: string; desc_v2: DescV2[] | null; state: number; duration: number; rights: Rights; owner: OwnerInNormal; stat: StatInNormal; argue_info: ArgueInfo; dynamic: string; cid: number; dimension: Dimension; teenage_mode: number; is_chargeable_season: boolean; is_story: boolean; is_upower_exclusive: boolean; is_upower_play: boolean; is_upower_preview: boolean; enable_vt: number; vt_display: string; is_upower_exclusive_with_qa: boolean; no_cache: boolean; pages: PageInNormal[]; subtitle: SubtitleInNormal; staff: Staff[] | null; ugc_season: UgcSeason | null; is_season_display: boolean; user_garb: UserGarb; honor_reply: HonorReply; like_icon: string; need_jump_bv: boolean; disable_show_up_info: boolean; is_story_play: number; is_view_self: boolean }
|
||||||
export type NormalSearchResult = NormalInfo
|
export type NormalSearchResult = NormalInfo
|
||||||
export type Official = { role: number; title: string; desc: string; type: number }
|
export type Official = { role: number; title: string; desc: string; type: number }
|
||||||
@@ -404,7 +405,7 @@ export type UserVideoList = { vlist: EpInUserVideo[] }
|
|||||||
export type UserVideoSearchResult = UserVideoInfo
|
export type UserVideoSearchResult = UserVideoInfo
|
||||||
export type VideoProcessTask = { merge_selected: boolean; embed_chapter_selected: boolean; embed_skip_selected: boolean; completed: boolean }
|
export type VideoProcessTask = { merge_selected: boolean; embed_chapter_selected: boolean; embed_skip_selected: boolean; completed: boolean }
|
||||||
export type VideoQuality = "Unknown" | "240P" | "360P" | "480P" | "720P" | "720P60" | "1080P" | "AiRepair" | "1080P+" | "1080P60" | "4K" | "HDR" | "Dolby" | "8K"
|
export type VideoQuality = "Unknown" | "240P" | "360P" | "480P" | "720P" | "720P60" | "1080P" | "AiRepair" | "1080P+" | "1080P60" | "4K" | "HDR" | "Dolby" | "8K"
|
||||||
export type VideoTask = { selected: boolean; url: string; video_quality: VideoQuality; codec_type: CodecType; content_length: number; chunks: MediaChunk[]; completed: boolean }
|
export type VideoTask = { selected: boolean; url: string; video_quality: VideoQuality; codec_type: CodecType; content_length: number; chunks: MediaChunk[]; completed: boolean; skipped: boolean }
|
||||||
export type VipInUserInfo = { type: number; status: number; due_date: number; vip_pay_type: number; theme_type: number; label: LabelInUserInfo; avatar_subscript: number; nickname_color: string; role: number; avatar_subscript_url: string; tv_vip_status: number; tv_vip_pay_type: number; tv_due_date: number }
|
export type VipInUserInfo = { type: number; status: number; due_date: number; vip_pay_type: number; theme_type: number; label: LabelInUserInfo; avatar_subscript: number; nickname_color: string; role: number; avatar_subscript_url: string; tv_vip_status: number; tv_vip_pay_type: number; tv_due_date: number }
|
||||||
export type VipLabel = { path: string; text: string; label_theme: string; text_color: string; bg_style: number; bg_color: string; border_color: string; use_img_label: boolean; img_label_uri_hans: string; img_label_uri_hant: string; img_label_uri_hans_static: string; img_label_uri_hant_static: string }
|
export type VipLabel = { path: string; text: string; label_theme: string; text_color: string; bg_style: number; bg_color: string; border_color: string; use_img_label: boolean; img_label_uri_hans: string; img_label_uri_hant: string; img_label_uri_hans_static: string; img_label_uri_hant_static: string }
|
||||||
export type VipOrPay = { text: string; bg_color: string; bg_color_night: string; img: string; multi_img: MultiImg }
|
export type VipOrPay = { text: string; bg_color: string; bg_color_night: string; img: string; multi_img: MultiImg }
|
||||||
|
|||||||
Reference in New Issue
Block a user