feat: 后端支持音视频合并

This commit is contained in:
lanyeeee
2025-07-24 06:35:36 +08:00
parent 17c98a423e
commit 9c311ef3e1
11 changed files with 139 additions and 6 deletions
+2
View File
@@ -16,6 +16,7 @@ pub struct Config {
pub prefer_audio_quality: PreferAudioQuality,
pub download_video: bool,
pub download_audio: bool,
pub auto_merge: bool,
pub dir_fmt: String,
pub dir_fmt_for_part: String,
pub time_fmt: String,
@@ -88,6 +89,7 @@ impl Config {
prefer_audio_quality: PreferAudioQuality::Best,
download_video: true,
download_audio: true,
auto_merge: true,
dir_fmt: "{collection_title}/{episode_title}".to_string(),
dir_fmt_for_part: DEFAULT_FMT_FOR_PART.to_string(),
time_fmt: "%Y-%m-%d_%H-%M-%S".to_string(),
+25 -3
View File
@@ -11,7 +11,7 @@ use uuid::Uuid;
use crate::{
config::Config,
downloader::tasks::{audio_task::AudioTask, video_task::VideoTask},
downloader::tasks::{audio_task::AudioTask, merge_task::MergeTask, video_task::VideoTask},
extensions::AppHandleExt,
types::{
audio_quality::AudioQuality,
@@ -48,6 +48,7 @@ pub struct DownloadProgress {
pub filename: String,
pub video_task: VideoTask,
pub audio_task: AudioTask,
pub merge_task: MergeTask,
pub create_ts: u64,
pub completed_ts: Option<u64>,
}
@@ -114,6 +115,7 @@ impl DownloadProgress {
filename: String::new(),
video_task: tasks.video,
audio_task: tasks.audio,
merge_task: tasks.merge,
create_ts,
completed_ts: None,
};
@@ -159,6 +161,7 @@ impl DownloadProgress {
filename: String::new(),
video_task: tasks.video,
audio_task: tasks.audio,
merge_task: tasks.merge,
create_ts,
completed_ts: None,
};
@@ -291,12 +294,15 @@ impl DownloadProgress {
}
pub fn is_completed(&self) -> bool {
self.video_task.is_completed() && self.audio_task.is_completed()
self.video_task.is_completed()
&& self.audio_task.is_completed()
&& self.merge_task.is_completed()
}
pub fn mark_uncompleted(&mut self) {
self.video_task.mark_uncompleted();
self.audio_task.mark_uncompleted();
self.merge_task.completed = false;
}
pub fn get_ids_string(&self) -> String {
@@ -344,6 +350,7 @@ fn create_normal_progresses_for_single(
filename: String::new(),
video_task: tasks.video,
audio_task: tasks.audio,
merge_task: tasks.merge,
create_ts,
completed_ts: None,
};
@@ -378,6 +385,7 @@ fn create_normal_progresses_for_single(
filename: String::new(),
video_task: tasks.video,
audio_task: tasks.audio,
merge_task: tasks.merge,
create_ts,
completed_ts: None,
};
@@ -412,6 +420,7 @@ fn create_normal_progresses_for_single(
filename: String::new(),
video_task: tasks.video.clone(),
audio_task: tasks.audio.clone(),
merge_task: tasks.merge.clone(),
create_ts,
completed_ts: None,
};
@@ -478,6 +487,7 @@ fn create_normal_progresses_for_season(
filename: String::new(),
video_task: tasks.video,
audio_task: tasks.audio,
merge_task: tasks.merge,
create_ts,
completed_ts: None,
};
@@ -512,6 +522,7 @@ fn create_normal_progresses_for_season(
filename: String::new(),
video_task: tasks.video,
audio_task: tasks.audio,
merge_task: tasks.merge,
create_ts,
completed_ts: None,
};
@@ -547,6 +558,7 @@ fn create_normal_progresses_for_season(
filename: String::new(),
video_task: tasks.video.clone(),
audio_task: tasks.audio.clone(),
merge_task: tasks.merge.clone(),
create_ts,
completed_ts: None,
};
@@ -563,6 +575,7 @@ fn create_normal_progresses_for_season(
struct Tasks {
video: VideoTask,
audio: AudioTask,
merge: MergeTask,
}
impl Tasks {
@@ -586,6 +599,15 @@ impl Tasks {
completed: false,
};
Self { video, audio }
let merge = MergeTask {
selected: config.auto_merge,
completed: false,
};
Self {
video,
audio,
merge,
}
}
}
+90
View File
@@ -296,6 +296,13 @@ impl DownloadTask {
tracing::debug!("{ids_string} `{filename}`音频下载完成");
}
if !progress.merge_task.is_completed() {
self.merge_video_audio(&progress)
.await
.context(format!("{ids_string} `{filename}`合并视频和音频失败"))?;
tracing::debug!("{ids_string} `{filename}`视频和音频合并完成");
}
let completed_ts = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
@@ -530,6 +537,89 @@ impl DownloadTask {
Ok(())
}
async fn merge_video_audio(&self, progress: &DownloadProgress) -> 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);
return Ok(());
}
let audio_path = episode_dir.join(format!("{filename}.m4a"));
if !audio_path.exists() {
self.update_progress(|p| p.merge_task.completed = true);
return Ok(());
}
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 (tx, rx) = tokio::sync::oneshot::channel();
let video_path_clone = video_path.clone();
let audio_path_clone = audio_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(audio_path_clone)
.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()
))?;
self.update_progress(|p| p.merge_task.completed = true);
Ok(())
}
async fn sleep_between_task(&self) {
let task_id = &self.task_id;
let mut remaining_sec = self.app.get_config().read().task_download_interval_sec;
@@ -0,0 +1,14 @@
use serde::{Deserialize, Serialize};
use specta::Type;
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
pub struct MergeTask {
pub selected: bool,
pub completed: bool,
}
impl MergeTask {
pub fn is_completed(&self) -> bool {
!self.selected || self.completed
}
}
+1
View File
@@ -1,2 +1,3 @@
pub mod audio_task;
pub mod merge_task;
pub mod video_task;
+4 -1
View File
@@ -30,6 +30,9 @@
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"externalBin": [
"ffmpeg/com.lanyeeee.bilibili-video-downloader-ffmpeg"
]
}
}
}