Compare commits

...
18 changed files with 264 additions and 222 deletions
+1
View File
@@ -327,6 +327,7 @@ pub mod v1 {
pub part_title: Option<String>, pub part_title: Option<String>,
pub part_order: Option<i64>, pub part_order: Option<i64>,
pub episode_title: String, pub episode_title: String,
pub episode_title_in_collection: Option<String>,
pub episode_order: i64, pub episode_order: i64,
pub up_name: Option<String>, pub up_name: Option<String>,
pub up_uid: Option<i64>, pub up_uid: Option<i64>,
+16
View File
@@ -315,6 +315,7 @@ dependencies = [
"tauri-plugin-dialog", "tauri-plugin-dialog",
"tauri-plugin-opener", "tauri-plugin-opener",
"tauri-plugin-os", "tauri-plugin-os",
"tauri-plugin-window-state",
"tauri-specta", "tauri-specta",
"tokio", "tokio",
"tracing", "tracing",
@@ -4463,6 +4464,21 @@ dependencies = [
"thiserror 2.0.12", "thiserror 2.0.12",
] ]
[[package]]
name = "tauri-plugin-window-state"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d5f6fe3291bfa609c7e0b0ee3bedac294d94c7018934086ce782c1d0f2a468e"
dependencies = [
"bitflags 2.9.1",
"log",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.12",
]
[[package]] [[package]]
name = "tauri-runtime" name = "tauri-runtime"
version = "2.7.0" version = "2.7.0"
+1
View File
@@ -22,6 +22,7 @@ tauri = { version = "2", features = [] }
tauri-plugin-opener = "2" tauri-plugin-opener = "2"
tauri-plugin-os = "2" tauri-plugin-os = "2"
tauri-plugin-dialog = "2" tauri-plugin-dialog = "2"
tauri-plugin-window-state = "2"
dlopen2 = { version = "0.8.2" } dlopen2 = { version = "0.8.2" }
bilibili-video-downloader-plugin-api = { path = "../src-plugin/plugin-api" } bilibili-video-downloader-plugin-api = { path = "../src-plugin/plugin-api" }
+2 -1
View File
@@ -351,7 +351,8 @@ impl BiliClient {
.read() .read()
.get("https://api.bilibili.com/x/space/wbi/arc/search") .get("https://api.bilibili.com/x/space/wbi/arc/search")
.query(&params) .query(&params)
.header("cookie", self.get_cookie()); // 不带DedeUserID会随机触发412错误
.header("cookie", format!("DedeUserID=1; {}", self.get_cookie()));
let http_resp = request.send().await?; let http_resp = request.send().await?;
// 检查http响应状态码 // 检查http响应状态码
let status = http_resp.status(); let status = http_resp.status();
+7 -65
View File
@@ -24,10 +24,7 @@ use crate::{
}, },
}; };
use super::{ use super::{download_progress::DownloadProgress, download_task::DownloadTask};
download_progress::DownloadProgress, download_task::DownloadTask,
download_task_state::DownloadTaskState,
};
pub struct DownloadManager { pub struct DownloadManager {
pub app: AppHandle, pub app: AppHandle,
@@ -91,7 +88,7 @@ impl DownloadManager {
let old_task = tasks.insert(new_task.task_id.clone(), new_task); let old_task = tasks.insert(new_task.task_id.clone(), new_task);
if let Some(old_task) = old_task { if let Some(old_task) = old_task {
// 如果同一个ID的下载任务已经存在,则取消旧的任务 // 如果同一个ID的下载任务已经存在,则取消旧的任务
let _ = old_task.cancel_sender.send(()); old_task.cancel();
} }
} }
@@ -120,8 +117,7 @@ impl DownloadManager {
tracing::error!(err_title, message); tracing::error!(err_title, message);
continue; continue;
}; };
task.set_state(DownloadTaskState::Paused); task.pause();
tracing::debug!("已将ID对应的下载任务状态设置为`Paused`");
} }
} }
@@ -139,8 +135,7 @@ impl DownloadManager {
tracing::error!(err_title, message); tracing::error!(err_title, message);
continue; continue;
}; };
task.set_state(DownloadTaskState::Pending); task.resume();
tracing::debug!("已将ID对应的下载任务状态设置为`Pending`");
} }
} }
@@ -158,27 +153,13 @@ impl DownloadManager {
tracing::error!(err_title, message); tracing::error!(err_title, message);
continue; continue;
}; };
// TODO: 应该先发删除新号再删文件 if let Err(err) = task.delete() {
// 因为发信号失败会把任务重新塞回去
// 目前先删文件会导致发信号失败时出现 任务还在但文件没了的情况
if let Err(err) = self.delete_progress_file(task_id) {
let err_title = "删除下载任务失败"; let err_title = "删除下载任务失败";
let message = err.to_message(); let message = err.to_message();
tracing::error!(err_title, message); tracing::error!(err_title, message);
tasks.insert(task_id.clone(), task); tasks.insert(task_id.clone(), task);
continue; continue;
} }
if let Err(err) = task.delete_sender.send(()).map_err(eyre::Report::from) {
let err = err.wrap_err("通知ID对应的下载任务删除失败");
let err_title = "删除下载任务失败";
let message = err.to_message();
tracing::error!(err_title, message);
tasks.insert(task_id.clone(), task);
continue;
}
tracing::debug!("已通知ID对应的下载任务删除");
} }
} }
@@ -197,15 +178,12 @@ impl DownloadManager {
continue; continue;
}; };
if let Err(err) = task.restart_sender.send(()).map_err(eyre::Report::from) { if let Err(err) = task.restart() {
let err_title = "重来下载任务失败"; let err_title = "重来下载任务失败";
let err = err.wrap_err("通知ID对应的下载任务重来失败");
let message = err.to_message(); let message = err.to_message();
tracing::error!(err_title, message); tracing::error!(err_title, message);
continue; continue;
} }
tracing::debug!("已通知ID对应的下载任务重来");
} }
} }
@@ -222,37 +200,11 @@ impl DownloadManager {
return; return;
}; };
// TODO: 把这块代码变成DownloadProgress的mark_restart函数 if let Err(err) = task.restart_with_params(params) {
{
let mut progress = task.progress.write();
progress.video_task.selected = params.video_task_selected;
progress.audio_task.selected = params.audio_task_selected;
progress.video_process_task.merge_selected = params.merge_selected;
progress.video_process_task.embed_chapter_selected = params.embed_chapter_selected;
progress.video_process_task.embed_skip_selected = params.embed_skip_selected;
progress.subtitle_task.selected = params.subtitle_task_selected;
progress.danmaku_task.xml_selected = params.xml_danmaku_selected;
progress.danmaku_task.ass_selected = params.ass_danmaku_selected;
progress.danmaku_task.json_selected = params.json_danmaku_selected;
progress.cover_task.selected = params.cover_task_selected;
progress.nfo_task.selected = params.nfo_task_selected;
progress.json_task.selected = params.json_task_selected;
progress.video_task.video_quality = params.video_quality;
progress.video_task.codec_type = params.codec_type;
progress.audio_task.audio_quality = params.audio_quality;
}
if let Err(err) = task.restart_sender.send(()).map_err(eyre::Report::from) {
let err_title = "重来下载任务失败"; let err_title = "重来下载任务失败";
let err = err.wrap_err("通知ID对应的下载任务重来失败");
let message = err.to_message(); let message = err.to_message();
tracing::error!(err_title, message); tracing::error!(err_title, message);
return;
} }
tracing::debug!("已通知ID对应的下载任务重来");
} }
async fn emit_download_speed_loop(app: AppHandle, byte_per_sec: Arc<AtomicU64>) { async fn emit_download_speed_loop(app: AppHandle, byte_per_sec: Arc<AtomicU64>) {
@@ -274,14 +226,4 @@ impl DownloadManager {
let task_dir = app_data_dir.join(".下载任务"); let task_dir = app_data_dir.join(".下载任务");
Ok(task_dir) Ok(task_dir)
} }
#[instrument(level = "error", skip_all, fields(task_id = task_id))]
fn delete_progress_file(&self, task_id: &str) -> eyre::Result<()> {
let task_dir = self.get_task_dir()?;
let task_file = task_dir.join(format!("{task_id}.json"));
if task_file.exists() {
std::fs::remove_file(task_file)?;
}
Ok(())
}
} }
+31 -3
View File
@@ -32,6 +32,7 @@ use crate::{
bangumi_info::BangumiInfo, bangumi_info::BangumiInfo,
cheese_info::CheeseInfo, cheese_info::CheeseInfo,
codec_type::CodecType, codec_type::CodecType,
get_normal_info_params::GetNormalInfoParams,
normal_info::{NormalInfo, UgcSeason}, normal_info::{NormalInfo, UgcSeason},
video_quality::VideoQuality, video_quality::VideoQuality,
}, },
@@ -54,6 +55,7 @@ pub struct DownloadProgress {
pub part_title: Option<String>, pub part_title: Option<String>,
pub part_order: Option<i64>, pub part_order: Option<i64>,
pub episode_title: String, pub episode_title: String,
pub episode_title_in_collection: Option<String>,
pub episode_order: i64, pub episode_order: i64,
pub up_name: Option<String>, pub up_name: Option<String>,
pub up_uid: Option<i64>, pub up_uid: Option<i64>,
@@ -129,6 +131,7 @@ impl DownloadProgress {
part_title: None, part_title: None,
part_order: None, part_order: None,
episode_title: episode.show_title.clone().unwrap_or(episode.title.clone()), episode_title: episode.show_title.clone().unwrap_or(episode.title.clone()),
episode_title_in_collection: None,
episode_order, episode_order,
up_name, up_name,
up_uid, up_uid,
@@ -179,6 +182,7 @@ impl DownloadProgress {
part_title: None, part_title: None,
part_order: None, part_order: None,
episode_title: episode.title.clone(), episode_title: episode.title.clone(),
episode_title_in_collection: None,
episode_order: episode.index, episode_order: episode.index,
up_name: Some(info.up_info.uname.clone()), up_name: Some(info.up_info.uname.clone()),
up_uid: Some(info.up_info.mid), up_uid: Some(info.up_info.mid),
@@ -338,6 +342,18 @@ impl DownloadProgress {
let audio_selected = self.audio_task.selected; let audio_selected = self.audio_task.selected;
let audio_completed = self.audio_task.completed; let audio_completed = self.audio_task.completed;
// 通过合集批量创建的任务时,合集接口只提供视频在合集内的标题
// 所以在创建任务阶段,episode_title暂时用它占位
// 在真正开始下载前,需要重新获取视频的真实标题
if self.episode_type == EpisodeType::Normal && self.episode_title_in_collection.is_some() {
let bili_client = app.get_bili_client();
let info = bili_client
.get_normal_info(GetNormalInfoParams::Aid(self.aid))
.await
.wrap_err("获取视频信息失败")?;
self.episode_title = info.title;
}
if (!video_selected && !audio_selected) || (video_completed && audio_completed) { if (!video_selected && !audio_selected) || (video_completed && audio_completed) {
// 如果视频和音频都没有选中,或者都已经完成,则更新需要格式化的字段就返回 // 如果视频和音频都没有选中,或者都已经完成,则更新需要格式化的字段就返回
self.update_fmt_fields(app) self.update_fmt_fields(app)
@@ -352,6 +368,7 @@ impl DownloadProgress {
let Some(bvid) = &self.bvid else { let Some(bvid) = &self.bvid else {
return Err(eyre!("progress中的bvid为None,无法获取视频链接")); return Err(eyre!("progress中的bvid为None,无法获取视频链接"));
}; };
let media_url = bili_client let media_url = bili_client
.get_normal_url(bvid, self.cid) .get_normal_url(bvid, self.cid)
.await .await
@@ -442,6 +459,7 @@ impl DownloadProgress {
pub_ts: self.pub_ts, pub_ts: self.pub_ts,
collection_title: self.collection_title.clone(), collection_title: self.collection_title.clone(),
episode_title: self.episode_title.clone(), episode_title: self.episode_title.clone(),
episode_title_in_collection: self.episode_title_in_collection.clone(),
episode_order: self.episode_order, episode_order: self.episode_order,
part_title: self.part_title.clone(), part_title: self.part_title.clone(),
part_order: self.part_order, part_order: self.part_order,
@@ -526,6 +544,7 @@ fn create_normal_progresses_for_single(
part_title: Some(page.part.clone()), part_title: Some(page.part.clone()),
part_order: Some(page.page), part_order: Some(page.page),
episode_title: info.title.clone(), episode_title: info.title.clone(),
episode_title_in_collection: None,
episode_order: 1, episode_order: 1,
up_name: Some(info.owner.name.clone()), up_name: Some(info.owner.name.clone()),
up_uid: Some(info.owner.mid), up_uid: Some(info.owner.mid),
@@ -564,6 +583,7 @@ fn create_normal_progresses_for_single(
part_title: None, part_title: None,
part_order: None, part_order: None,
episode_title: info.title.clone(), episode_title: info.title.clone(),
episode_title_in_collection: None,
episode_order: 1, episode_order: 1,
up_name: Some(info.owner.name.clone()), up_name: Some(info.owner.name.clone()),
up_uid: Some(info.owner.mid), up_uid: Some(info.owner.mid),
@@ -602,6 +622,7 @@ fn create_normal_progresses_for_single(
part_title: Some(page.part.clone()), part_title: Some(page.part.clone()),
part_order: Some(page.page), part_order: Some(page.page),
episode_title: info.title.clone(), episode_title: info.title.clone(),
episode_title_in_collection: None,
episode_order: 1, episode_order: 1,
up_name: Some(info.owner.name.clone()), up_name: Some(info.owner.name.clone()),
up_uid: Some(info.owner.mid), up_uid: Some(info.owner.mid),
@@ -653,6 +674,10 @@ fn create_normal_progresses_for_season(
let tasks = Tasks::new(config, &ep.arc.pic); let tasks = Tasks::new(config, &ep.arc.pic);
// 合集接口返回的只有在合集内的标题,视频标题先用它占位,到prepare时再按aid获取真实标题
let episode_title = ep.title.clone();
let episode_title_in_collection = Some(ep.title.clone());
let create_ts = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); let create_ts = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
if let Some(cid) = cid { if let Some(cid) = cid {
@@ -672,7 +697,8 @@ fn create_normal_progresses_for_season(
collection_title: ugc_season.title.clone(), collection_title: ugc_season.title.clone(),
part_title: Some(page.part.clone()), part_title: Some(page.part.clone()),
part_order: Some(page.page), part_order: Some(page.page),
episode_title: ep.title.clone(), episode_title: episode_title.clone(),
episode_title_in_collection: episode_title_in_collection.clone(),
episode_order, episode_order,
up_name: Some(info.owner.name.clone()), up_name: Some(info.owner.name.clone()),
up_uid: Some(info.owner.mid), up_uid: Some(info.owner.mid),
@@ -710,7 +736,8 @@ fn create_normal_progresses_for_season(
collection_title: ugc_season.title.clone(), collection_title: ugc_season.title.clone(),
part_title: None, part_title: None,
part_order: None, part_order: None,
episode_title: ep.title.clone(), episode_title: episode_title.clone(),
episode_title_in_collection: episode_title_in_collection.clone(),
episode_order, episode_order,
up_name: Some(info.owner.name.clone()), up_name: Some(info.owner.name.clone()),
up_uid: Some(info.owner.mid), up_uid: Some(info.owner.mid),
@@ -749,7 +776,8 @@ fn create_normal_progresses_for_season(
collection_title: ugc_season.title.clone(), collection_title: ugc_season.title.clone(),
part_title: Some(page.part.clone()), part_title: Some(page.part.clone()),
part_order: Some(page.page), part_order: Some(page.page),
episode_title: ep.title.clone(), episode_title: episode_title.clone(),
episode_title_in_collection: episode_title_in_collection.clone(),
episode_order, episode_order,
up_name: Some(info.owner.name.clone()), up_name: Some(info.owner.name.clone()),
up_uid: Some(info.owner.mid), up_uid: Some(info.owner.mid),
+87 -3
View File
@@ -2,7 +2,7 @@
use eyre::WrapErr; use eyre::WrapErr;
use parking_lot::RwLock; use parking_lot::RwLock;
use tauri::AppHandle; use tauri::{AppHandle, Manager};
use tauri_specta::Event; use tauri_specta::Event;
use tokio::{ use tokio::{
sync::{SemaphorePermit, watch}, sync::{SemaphorePermit, watch},
@@ -14,7 +14,10 @@ use crate::{
downloader::episode_type::EpisodeType, downloader::episode_type::EpisodeType,
events::DownloadEvent, events::DownloadEvent,
extensions::{AppHandleExt, EyreReportToMessage}, extensions::{AppHandleExt, EyreReportToMessage},
types::create_download_task_params::CreateDownloadTaskParams, types::{
create_download_task_params::CreateDownloadTaskParams,
restart_download_task_params::RestartDownloadTaskParams,
},
}; };
use super::{download_progress::DownloadProgress, download_task_state::DownloadTaskState}; use super::{download_progress::DownloadProgress, download_task_state::DownloadTaskState};
@@ -127,6 +130,7 @@ impl DownloadTask {
ep_id = progress.ep_id, ep_id = progress.ep_id,
collection_title = progress.collection_title, collection_title = progress.collection_title,
episode_title = progress.episode_title, episode_title = progress.episode_title,
episode_title_in_collection = progress.episode_title_in_collection,
episode_order = progress.episode_order, episode_order = progress.episode_order,
part_title = progress.part_title, part_title = progress.part_title,
part_order = progress.part_order, part_order = progress.part_order,
@@ -199,6 +203,70 @@ impl DownloadTask {
task task
} }
#[instrument(level = "error", skip_all, fields(task_id = self.task_id))]
pub fn pause(&self) {
self.set_state(DownloadTaskState::Paused);
tracing::debug!("已将下载任务状态设置为`Paused`");
}
#[instrument(level = "error", skip_all, fields(task_id = self.task_id))]
pub fn resume(&self) {
self.set_state(DownloadTaskState::Pending);
tracing::debug!("已将下载任务状态设置为`Pending`");
}
#[instrument(level = "error", skip_all, fields(task_id = self.task_id))]
pub fn cancel(&self) {
let _ = self.cancel_sender.send(());
}
#[instrument(level = "error", skip_all, fields(task_id = self.task_id))]
pub fn delete(&self) -> eyre::Result<()> {
self.delete_progress_file()?;
self.delete_sender
.send(())
.map_err(eyre::Report::from)
.wrap_err("通知ID对应的下载任务删除失败")?;
tracing::debug!("已通知ID对应的下载任务删除");
Ok(())
}
#[instrument(level = "error", skip_all, fields(task_id = self.task_id))]
pub fn restart(&self) -> eyre::Result<()> {
self.restart_sender
.send(())
.map_err(eyre::Report::from)
.wrap_err("通知ID对应的下载任务重来失败")?;
tracing::debug!("已通知ID对应的下载任务重来");
Ok(())
}
#[instrument(level = "error", skip_all, fields(task_id = self.task_id))]
pub fn restart_with_params(&self, params: &RestartDownloadTaskParams) -> eyre::Result<()> {
{
let mut progress = self.progress.write();
progress.video_task.selected = params.video_task_selected;
progress.audio_task.selected = params.audio_task_selected;
progress.video_process_task.merge_selected = params.merge_selected;
progress.video_process_task.embed_chapter_selected = params.embed_chapter_selected;
progress.video_process_task.embed_skip_selected = params.embed_skip_selected;
progress.subtitle_task.selected = params.subtitle_task_selected;
progress.danmaku_task.xml_selected = params.xml_danmaku_selected;
progress.danmaku_task.ass_selected = params.ass_danmaku_selected;
progress.danmaku_task.json_selected = params.json_danmaku_selected;
progress.cover_task.selected = params.cover_task_selected;
progress.nfo_task.selected = params.nfo_task_selected;
progress.json_task.selected = params.json_task_selected;
progress.video_task.video_quality = params.video_quality;
progress.video_task.codec_type = params.codec_type;
progress.audio_task.audio_quality = params.audio_quality;
}
self.restart()
}
#[instrument( #[instrument(
level = "error", level = "error",
skip_all, skip_all,
@@ -211,6 +279,7 @@ impl DownloadTask {
ep_id = self.trace_fields.ep_id, ep_id = self.trace_fields.ep_id,
collection_title = self.trace_fields.collection_title, collection_title = self.trace_fields.collection_title,
episode_title = self.trace_fields.episode_title, episode_title = self.trace_fields.episode_title,
episode_title_in_collection = self.trace_fields.episode_title_in_collection,
episode_order = self.trace_fields.episode_order, episode_order = self.trace_fields.episode_order,
part_title = self.trace_fields.part_title, part_title = self.trace_fields.part_title,
part_order = self.trace_fields.part_order, part_order = self.trace_fields.part_order,
@@ -417,6 +486,7 @@ impl DownloadTask {
ep_id = self.trace_fields.ep_id, ep_id = self.trace_fields.ep_id,
collection_title = self.trace_fields.collection_title, collection_title = self.trace_fields.collection_title,
episode_title = self.trace_fields.episode_title, episode_title = self.trace_fields.episode_title,
episode_title_in_collection = self.trace_fields.episode_title_in_collection,
episode_order = self.trace_fields.episode_order, episode_order = self.trace_fields.episode_order,
part_title = self.trace_fields.part_title, part_title = self.trace_fields.part_title,
part_order = self.trace_fields.part_order, part_order = self.trace_fields.part_order,
@@ -424,7 +494,7 @@ impl DownloadTask {
up_uid = self.trace_fields.up_uid, up_uid = self.trace_fields.up_uid,
) )
)] )]
pub fn set_state(&self, state: DownloadTaskState) { fn set_state(&self, state: DownloadTaskState) {
if let Err(err) = self.state_sender.send(state).map_err(eyre::Report::from) { if let Err(err) = self.state_sender.send(state).map_err(eyre::Report::from) {
let err_title = format!("发送状态`{state:?}`失败"); let err_title = format!("发送状态`{state:?}`失败");
let message = err.to_message(); let message = err.to_message();
@@ -456,6 +526,18 @@ impl DownloadTask {
tracing::error!(err_title, message); tracing::error!(err_title, message);
} }
} }
#[instrument(level = "error", skip_all, fields(task_id = self.task_id))]
fn delete_progress_file(&self) -> eyre::Result<()> {
let app_data_dir = self.app.path().app_data_dir()?;
let task_file = app_data_dir
.join(".下载任务")
.join(format!("{}.json", self.task_id));
if task_file.exists() {
std::fs::remove_file(task_file)?;
}
Ok(())
}
} }
pub struct DownloadTaskTraceFields { pub struct DownloadTaskTraceFields {
@@ -467,6 +549,7 @@ pub struct DownloadTaskTraceFields {
pub ep_id: Option<i64>, pub ep_id: Option<i64>,
pub collection_title: String, pub collection_title: String,
pub episode_title: String, pub episode_title: String,
pub episode_title_in_collection: Option<String>,
pub episode_order: i64, pub episode_order: i64,
pub part_title: Option<String>, pub part_title: Option<String>,
pub part_order: Option<i64>, pub part_order: Option<i64>,
@@ -485,6 +568,7 @@ impl From<&DownloadProgress> for DownloadTaskTraceFields {
ep_id: progress.ep_id, ep_id: progress.ep_id,
collection_title: progress.collection_title.clone(), collection_title: progress.collection_title.clone(),
episode_title: progress.episode_title.clone(), episode_title: progress.episode_title.clone(),
episode_title_in_collection: progress.episode_title_in_collection.clone(),
episode_order: progress.episode_order, episode_order: progress.episode_order,
part_title: progress.part_title.clone(), part_title: progress.part_title.clone(),
part_order: progress.part_order, part_order: progress.part_order,
+1
View File
@@ -25,6 +25,7 @@ pub struct FmtParams {
pub pub_ts: i64, pub pub_ts: i64,
pub collection_title: String, pub collection_title: String,
pub episode_title: String, pub episode_title: String,
pub episode_title_in_collection: Option<String>,
pub episode_order: i64, pub episode_order: i64,
pub part_title: Option<String>, pub part_title: Option<String>,
pub part_order: Option<i64>, pub part_order: Option<i64>,
@@ -1,4 +1,4 @@
use std::{path::PathBuf, sync::Arc}; use std::{io, path::PathBuf, sync::Arc};
use eyre::{WrapErr, eyre}; use eyre::{WrapErr, eyre};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -98,54 +98,29 @@ impl VideoProcessTask {
let output_path = episode_dir.join(format!("{filename}-merged.mp4")); let output_path = episode_dir.join(format!("{filename}-merged.mp4"));
let (tx, rx) = tokio::sync::oneshot::channel(); let mut command = std::process::Command::new(ffmpeg_program);
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();
let current_span = tracing::Span::current(); command.arg("-i").arg(&video_path);
tauri::async_runtime::spawn_blocking(move || { command.arg("-i").arg(&audio_path);
let _enter = current_span.enter(); if let Some(metadata_path) = &metadata_path {
command.arg("-i").arg(metadata_path);
let mut command = std::process::Command::new(ffmpeg_program); command.arg("-map_metadata").arg("2");
command.arg("-i").arg(video_path_clone);
command.arg("-i").arg(audio_path_clone);
if let Some(metadata_path) = metadata_path_clone {
command.arg("-i").arg(metadata_path);
command.arg("-map_metadata").arg("2");
}
command.arg("-c").arg("copy");
command.arg("-map").arg("0:v:0");
command.arg("-map").arg("1:a:0");
command.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 = eyre!(format!("STDOUT: {stdout}"))
.wrap_err(format!("STDERR: {stderr}"))
.wrap_err("原因可能是视频或音频文件损坏,建议[重来]试试");
return Err(err);
} }
command.arg("-c").arg("copy");
command.arg("-map").arg("0:v:0");
command.arg("-map").arg("1:a:0");
command.arg(&output_path).arg("-y");
#[cfg(target_os = "windows")]
{
// 隐藏窗口
use std::os::windows::process::CommandExt;
command.creation_flags(0x0800_0000);
}
execute_ffmpeg(command).await.wrap_err("执行FFmpeg失败")?;
std::fs::remove_file(&video_path) std::fs::remove_file(&video_path)
.wrap_err(format!("删除视频文件`{}`失败", video_path.display()))?; .wrap_err(format!("删除视频文件`{}`失败", video_path.display()))?;
std::fs::remove_file(&audio_path) std::fs::remove_file(&audio_path)
@@ -192,49 +167,24 @@ impl VideoProcessTask {
let ffmpeg_program = utils::get_ffmpeg_program().wrap_err("获取FFmpeg程序路径失败")?; let ffmpeg_program = utils::get_ffmpeg_program().wrap_err("获取FFmpeg程序路径失败")?;
let (tx, rx) = tokio::sync::oneshot::channel(); let mut command = std::process::Command::new(ffmpeg_program);
let video_path_clone = video_path.clone();
let audio_path_clone = audio_path.clone();
let output_path_clone = output_path.clone();
let current_span = tracing::Span::current(); command.arg("-i").arg(&video_path);
tauri::async_runtime::spawn_blocking(move || { command.arg("-i").arg(&audio_path);
let _enter = current_span.enter(); command.arg("-c").arg("copy");
command.arg("-map").arg("0:v:0");
command.arg("-map").arg("1:a:0");
command.arg(&output_path).arg("-y");
let mut command = std::process::Command::new(ffmpeg_program); #[cfg(target_os = "windows")]
{
command.arg("-i").arg(video_path_clone); // 隐藏窗口
command.arg("-i").arg(audio_path_clone); use std::os::windows::process::CommandExt;
command.creation_flags(0x0800_0000);
command.arg("-c").arg("copy");
command.arg("-map").arg("0:v:0");
command.arg("-map").arg("1:a:0");
command.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 = eyre!(format!("STDOUT: {stdout}"))
.wrap_err(format!("STDERR: {stderr}"))
.wrap_err("原因可能是视频或音频文件损坏,建议[重来]试试");
return Err(err);
} }
execute_ffmpeg(command).await.wrap_err("执行FFmpeg失败")?;
std::fs::remove_file(&video_path) std::fs::remove_file(&video_path)
.wrap_err(format!("删除视频文件`{}`失败", video_path.display()))?; .wrap_err(format!("删除视频文件`{}`失败", video_path.display()))?;
std::fs::remove_file(&audio_path) std::fs::remove_file(&audio_path)
@@ -279,48 +229,23 @@ impl VideoProcessTask {
return Ok(()); return Ok(());
}; };
let (tx, rx) = tokio::sync::oneshot::channel(); let mut command = std::process::Command::new(ffmpeg_program);
let video_path_clone = video_path.clone();
let metadata_path_clone = metadata_path.clone();
let output_path_clone = output_path.clone();
let current_span = tracing::Span::current(); command.arg("-i").arg(&video_path);
tauri::async_runtime::spawn_blocking(move || { command.arg("-i").arg(&metadata_path);
let _enter = current_span.enter(); command.arg("-map_metadata").arg("1");
command.arg("-c").arg("copy");
command.arg(&output_path).arg("-y");
let mut command = std::process::Command::new(ffmpeg_program); #[cfg(target_os = "windows")]
{
command.arg("-i").arg(video_path_clone); // 隐藏窗口
command.arg("-i").arg(metadata_path_clone); use std::os::windows::process::CommandExt;
command.creation_flags(0x0800_0000);
command.arg("-map_metadata").arg("1");
command.arg("-c").arg("copy");
command.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 = eyre!(format!("STDOUT: {stdout}"))
.wrap_err(format!("STDERR: {stderr}"))
.wrap_err("原因可能是视频或音频文件损坏,建议[重来]试试");
return Err(err);
} }
execute_ffmpeg(command).await.wrap_err("执行FFmpeg失败")?;
std::fs::remove_file(&video_path) std::fs::remove_file(&video_path)
.wrap_err(format!("删除视频文件`{}`失败", video_path.display()))?; .wrap_err(format!("删除视频文件`{}`失败", video_path.display()))?;
std::fs::rename(&output_path, &video_path).wrap_err(format!( std::fs::rename(&output_path, &video_path).wrap_err(format!(
@@ -388,3 +313,32 @@ impl VideoProcessTask {
Ok(Some(metadata_path)) Ok(Some(metadata_path))
} }
} }
#[instrument(level = "error", skip_all)]
async fn execute_ffmpeg(mut command: std::process::Command) -> eyre::Result<()> {
let span = tracing::Span::current();
tauri::async_runtime::spawn_blocking(move || {
let _enter = span.enter();
let program = command.get_program().to_string_lossy().into_owned();
let output = command.output().map_err(|err| {
if err.kind() == io::ErrorKind::NotFound {
eyre!(err).wrap_err(format!("找不到FFmpeg `{program}`"))
} else {
eyre!(err)
}
})?;
if output.status.success() {
return Ok(());
}
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
Err(eyre!(format!("STDOUT: {stdout}")))
.wrap_err(format!("STDERR: {stderr}"))
.wrap_err("原因可能是视频或音频文件损坏,建议[重来]试试")
})
.await?
}
+1
View File
@@ -106,6 +106,7 @@ pub fn run() {
} }
tauri::Builder::default() tauri::Builder::default()
.plugin(tauri_plugin_window_state::Builder::default().build())
.plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_opener::init())
+11
View File
@@ -1,3 +1,5 @@
use serde::{Deserialize, Deserializer};
pub mod audio_quality; pub mod audio_quality;
pub mod available_media_formats; pub mod available_media_formats;
pub mod bangumi_follow_info; pub mod bangumi_follow_info;
@@ -36,3 +38,12 @@ pub mod user_info;
pub mod user_video_info; pub mod user_video_info;
pub mod video_quality; pub mod video_quality;
pub mod watch_later_info; pub mod watch_later_info;
fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
T: Default + Deserialize<'de>,
D: Deserializer<'de>,
{
let opt = Option::deserialize(deserializer)?;
Ok(opt.unwrap_or_default())
}
+3
View File
@@ -1,6 +1,8 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use specta::Type; use specta::Type;
use super::deserialize_null_default;
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)] #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
#[serde(default)] #[serde(default)]
#[allow(clippy::struct_excessive_bools)] #[allow(clippy::struct_excessive_bools)]
@@ -30,6 +32,7 @@ pub struct PlayerInfo {
pub online_count: i64, pub online_count: i64,
pub need_login_subtitle: bool, pub need_login_subtitle: bool,
pub subtitle: SubtitleInPlayerInfo, pub subtitle: SubtitleInPlayerInfo,
#[serde(default, deserialize_with = "deserialize_null_default")]
pub view_points: Vec<ViewPoint>, pub view_points: Vec<ViewPoint>,
pub preview_toast: String, pub preview_toast: String,
pub options: Options, pub options: Options,
+1
View File
@@ -8,6 +8,7 @@
"height": 600, "height": 600,
"minWidth": 800, "minWidth": 800,
"minHeight": 600, "minHeight": 600,
"visible": false,
"decorations": false, "decorations": false,
"theme": "Light" "theme": "Light"
} }
+1
View File
@@ -8,6 +8,7 @@
"height": 600, "height": 600,
"minWidth": 800, "minWidth": 800,
"minHeight": 600, "minHeight": 600,
"visible": false,
"hiddenTitle": true, "hiddenTitle": true,
"titleBarStyle": "Overlay", "titleBarStyle": "Overlay",
"theme": "Light" "theme": "Light"
+1
View File
@@ -8,6 +8,7 @@
"height": 600, "height": 600,
"minWidth": 800, "minWidth": 800,
"minHeight": 600, "minHeight": 600,
"visible": false,
"decorations": false, "decorations": false,
"shadow": true, "shadow": true,
"theme": "Light" "theme": "Light"
+1 -1
View File
@@ -329,7 +329,7 @@ export type Dimension = { width: number; height: number; rotate: number }
export type DimensionInBangumi = { height: number; rotate: number; width: number } export type DimensionInBangumi = { height: number; rotate: number; width: number }
export type DimensionInWatchLater = { width: number; height: number; rotate: 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 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; video_process_task: VideoProcessTask; subtitle_task: SubtitleTask; danmaku_task: DanmakuTask; cover_task: CoverTask; nfo_task: NfoTask; json_task: JsonTask; create_ts: number; completed_ts: number | null; is_drm: boolean; is_preview: boolean } 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_title_in_collection: string | null; 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; video_process_task: VideoProcessTask; subtitle_task: SubtitleTask; danmaku_task: DanmakuTask; cover_task: CoverTask; nfo_task: NfoTask; json_task: JsonTask; create_ts: number; completed_ts: number | null; is_drm: boolean; is_preview: boolean }
export type DownloadTaskState = "Pending" | "Downloading" | "Paused" | "Completed" | "Failed" export type DownloadTaskState = "Pending" | "Downloading" | "Paused" | "Completed" | "Failed"
export type Ed = { end: number; start: number } export type Ed = { end: number; start: number }
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 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 }
@@ -56,13 +56,17 @@ function AvailableFmtFields() {
<div> <div>
<span class="rounded bg-gray-500 px-1 select-all">collection_title</span> <span class="rounded bg-gray-500 px-1 select-all">collection_title</span>
<span class="ml-2"> <span class="ml-2">
合集名称 合集标题
<span class="text-blue">对于单独(没有合集)的视频合集名称就是视频名称</span> <span class="text-blue">对于单独(没有合集)的视频合集标题就是视频标题</span>
</span> </span>
</div> </div>
<div> <div>
<span class="rounded bg-gray-500 px-1 select-all">episode_title</span> <span class="rounded bg-gray-500 px-1 select-all">episode_title</span>
<span class="ml-2">视频名称</span> <span class="ml-2">视频标题</span>
</div>
<div>
<span class="rounded bg-gray-500 px-1 select-all">episode_title_in_collection</span>
<span class="ml-2">视频在合集内的标题</span>
</div> </div>
<div> <div>
<span class="rounded bg-gray-500 px-1 select-all">episode_order</span> <span class="rounded bg-gray-500 px-1 select-all">episode_order</span>
@@ -83,7 +87,7 @@ function AvailableFmtFields() {
</div> </div>
<div> <div>
<span class="rounded bg-gray-500 px-1 select-all">part_title</span> <span class="rounded bg-gray-500 px-1 select-all">part_title</span>
<span class="ml-2">分P名称</span> <span class="ml-2">分P标题</span>
</div> </div>
<div> <div>
<span class="rounded bg-gray-500 px-1 select-all">part_order</span> <span class="rounded bg-gray-500 px-1 select-all">part_order</span>
+11 -19
View File
@@ -27,36 +27,28 @@ export const useStore = defineStore('store', () => {
}) })
function useProgresses() { function useProgresses() {
// 内部的高频更新状态
const _progresses = new Map<string, ProgressData>()
// 对外暴露的响应式状态 // 对外暴露的响应式状态
const progresses = ref<Map<string, ProgressData>>(new Map()) const progresses = ref<Map<string, ProgressData>>(new Map())
// 等待在同一渲染帧内执行的更新函数
const pendingUpdateFns: Array<(progresses: Map<string, ProgressData>) => void> = []
// 用于确保在同一渲染帧内只安排一次UI更新 // 用于确保在同一渲染帧内只安排一次UI更新
let isUpdateScheduled = false let isUpdateScheduled = false
// 将 `_progresses` 的内容更新到 `progresses` 中,并触发重新渲染 // 在同一渲染帧内集中执行等待中的更新函数
const updateProgressesOnFrame = () => { const updateProgressesOnFrame = () => {
const newProgressesMap = new Map<string, ProgressData>()
for (const [key, value] of _progresses.entries()) {
const progressData = progresses.value.get(key)
if (progressData !== undefined) {
Object.assign(progressData, value)
newProgressesMap.set(key, progressData)
} else {
newProgressesMap.set(key, { ...value })
}
}
progresses.value = newProgressesMap
isUpdateScheduled = false isUpdateScheduled = false
const updateFns = pendingUpdateFns.splice(0)
for (const updateFn of updateFns) {
updateFn(progresses.value)
}
} }
const updateProgresses = (updateFn: (progresses: Map<string, ProgressData>) => void) => { const updateProgresses = (updateFn: (progresses: Map<string, ProgressData>) => void) => {
// 使用传入的更新函数来修改 `_progresses` // 传入的更新函数添加到等待队列
updateFn(_progresses) pendingUpdateFns.push(updateFn)
if (!isUpdateScheduled) { if (!isUpdateScheduled) {
// 如果没有安排过UI更新,则安排一次 // 如果没有安排过UI更新,则安排一次