mirror of
https://github.com/lanyeeee/bilibili-video-downloader.git
synced 2026-09-09 09:36:46 +08:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6331032757 |
Generated
-63
@@ -301,7 +301,6 @@ dependencies = [
|
|||||||
"parking_lot 0.12.4",
|
"parking_lot 0.12.4",
|
||||||
"prost",
|
"prost",
|
||||||
"rand 0.9.1",
|
"rand 0.9.1",
|
||||||
"rayon",
|
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"reqwest-middleware",
|
"reqwest-middleware",
|
||||||
"reqwest-retry",
|
"reqwest-retry",
|
||||||
@@ -316,14 +315,12 @@ 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",
|
||||||
"tracing-appender",
|
"tracing-appender",
|
||||||
"tracing-error",
|
"tracing-error",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"urlencoding",
|
|
||||||
"uuid",
|
"uuid",
|
||||||
"yaserde",
|
"yaserde",
|
||||||
]
|
]
|
||||||
@@ -680,25 +677,6 @@ dependencies = [
|
|||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "crossbeam-deque"
|
|
||||||
version = "0.8.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb"
|
|
||||||
dependencies = [
|
|
||||||
"crossbeam-epoch",
|
|
||||||
"crossbeam-utils",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "crossbeam-epoch"
|
|
||||||
version = "0.9.20"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
|
|
||||||
dependencies = [
|
|
||||||
"crossbeam-utils",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-utils"
|
name = "crossbeam-utils"
|
||||||
version = "0.8.21"
|
version = "0.8.21"
|
||||||
@@ -3381,26 +3359,6 @@ version = "0.6.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rayon"
|
|
||||||
version = "1.12.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
|
|
||||||
dependencies = [
|
|
||||||
"either",
|
|
||||||
"rayon-core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rayon-core"
|
|
||||||
version = "1.13.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
|
|
||||||
dependencies = [
|
|
||||||
"crossbeam-deque",
|
|
||||||
"crossbeam-utils",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.2.16"
|
version = "0.2.16"
|
||||||
@@ -4505,21 +4463,6 @@ 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"
|
||||||
@@ -5134,12 +5077,6 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "urlencoding"
|
|
||||||
version = "2.1.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "urlpattern"
|
name = "urlpattern"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ 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" }
|
||||||
|
|
||||||
@@ -60,8 +59,6 @@ memchr = { version = "2.7.5" }
|
|||||||
md-5 = { version = "0.10.6" }
|
md-5 = { version = "0.10.6" }
|
||||||
rand = { version = "0.9.1" }
|
rand = { version = "0.9.1" }
|
||||||
base64 = { version = "0.22.1" }
|
base64 = { version = "0.22.1" }
|
||||||
rayon = { version = "1.12.0" }
|
|
||||||
urlencoding = { version = "2.1.3" }
|
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
strip = true
|
strip = true
|
||||||
|
|||||||
@@ -351,8 +351,7 @@ 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(¶ms)
|
.query(¶ms)
|
||||||
// 不带DedeUserID会随机触发412错误
|
.header("cookie", self.get_cookie());
|
||||||
.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();
|
||||||
@@ -1098,12 +1097,7 @@ impl BiliClient {
|
|||||||
|
|
||||||
pub fn get_cookie(&self) -> String {
|
pub fn get_cookie(&self) -> String {
|
||||||
let sessdata = self.app.get_config().read().sessdata.clone();
|
let sessdata = self.app.get_config().read().sessdata.clone();
|
||||||
|
format!("SESSDATA={}", sessdata.trim_end_matches(';'))
|
||||||
let trimmed = sessdata.trim_end_matches(';');
|
|
||||||
let decoded = urlencoding::decode(trimmed).unwrap_or_else(|_| trimmed.into());
|
|
||||||
let encoded = urlencoding::encode(&decoded);
|
|
||||||
|
|
||||||
format!("SESSDATA={encoded}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ use tracing::instrument;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::Config,
|
config::Config,
|
||||||
downloader::download_task::RestoredDownloadTask,
|
|
||||||
errors::{CommandError, CommandResult},
|
errors::{CommandError, CommandResult},
|
||||||
extensions::AppHandleExt,
|
extensions::AppHandleExt,
|
||||||
logger,
|
logger,
|
||||||
@@ -311,14 +310,13 @@ pub fn restart_download_task(app: AppHandle, params: RestartDownloadTaskParams)
|
|||||||
#[tauri::command(async)]
|
#[tauri::command(async)]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
#[instrument(level = "error", skip_all)]
|
#[instrument(level = "error", skip_all)]
|
||||||
pub async fn restore_download_tasks(app: AppHandle) -> CommandResult<Vec<RestoredDownloadTask>> {
|
pub fn restore_download_tasks(app: AppHandle) -> CommandResult<()> {
|
||||||
let download_manager = app.get_download_manager();
|
let download_manager = app.get_download_manager();
|
||||||
let restored_tasks = download_manager
|
download_manager
|
||||||
.restore_download_tasks()
|
.restore_download_tasks()
|
||||||
.await
|
|
||||||
.map_err(|err| CommandError::from("恢复下载任务失败", err))?;
|
.map_err(|err| CommandError::from("恢复下载任务失败", err))?;
|
||||||
tracing::debug!("恢复下载任务成功");
|
tracing::debug!("恢复下载任务成功");
|
||||||
Ok(restored_tasks)
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command(async)]
|
#[tauri::command(async)]
|
||||||
|
|||||||
@@ -10,17 +10,12 @@ use std::{
|
|||||||
|
|
||||||
use eyre::{WrapErr, eyre};
|
use eyre::{WrapErr, eyre};
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use rayon::prelude::*;
|
|
||||||
use tauri::{AppHandle, Manager};
|
use tauri::{AppHandle, Manager};
|
||||||
use tauri_specta::Event;
|
use tauri_specta::Event;
|
||||||
use tokio::{
|
use tokio::sync::Semaphore;
|
||||||
sync::{Semaphore, oneshot},
|
use tracing::instrument;
|
||||||
task::JoinSet,
|
|
||||||
};
|
|
||||||
use tracing::{Instrument, instrument};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
downloader::download_progress::DownloadProgress,
|
|
||||||
events::DownloadEvent,
|
events::DownloadEvent,
|
||||||
extensions::{AppHandleExt, EyreReportToMessage},
|
extensions::{AppHandleExt, EyreReportToMessage},
|
||||||
types::{
|
types::{
|
||||||
@@ -29,7 +24,7 @@ use crate::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::download_task::{DownloadTask, RestoredDownloadTask};
|
use super::{download_progress::DownloadProgress, download_task::DownloadTask};
|
||||||
|
|
||||||
pub struct DownloadManager {
|
pub struct DownloadManager {
|
||||||
pub app: AppHandle,
|
pub app: AppHandle,
|
||||||
@@ -63,92 +58,41 @@ impl DownloadManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(level = "error", skip_all)]
|
#[instrument(level = "error", skip_all)]
|
||||||
pub async fn restore_download_tasks(&self) -> eyre::Result<Vec<RestoredDownloadTask>> {
|
pub fn restore_download_tasks(&self) -> eyre::Result<()> {
|
||||||
struct TaskFile {
|
|
||||||
path: PathBuf,
|
|
||||||
content: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
let task_dir = self.get_task_dir()?;
|
let task_dir = self.get_task_dir()?;
|
||||||
std::fs::create_dir_all(&task_dir)
|
std::fs::create_dir_all(&task_dir)
|
||||||
.wrap_err(format!("创建下载任务目录`{}`失败", task_dir.display()))?;
|
.wrap_err(format!("创建下载任务目录`{}`失败", task_dir.display()))?;
|
||||||
|
|
||||||
let mut join_set = JoinSet::new();
|
let mut tasks = self.download_tasks.write();
|
||||||
for entry in std::fs::read_dir(&task_dir)?.filter_map(Result::ok) {
|
for entry in std::fs::read_dir(&task_dir)?.filter_map(Result::ok) {
|
||||||
let read_task_file = async move {
|
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
|
|
||||||
let extension = path.extension().and_then(|s| s.to_str());
|
let extension = path.extension().and_then(|s| s.to_str());
|
||||||
if extension != Some("json") {
|
if extension != Some("json") {
|
||||||
// 如果这个文件不是json则删除
|
// 如果这个文件不是json则删除
|
||||||
let _ = tokio::fs::remove_file(path).await;
|
let _ = std::fs::remove_file(&path);
|
||||||
return None;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let content = match tokio::fs::read_to_string(&path)
|
let progress_json = std::fs::read_to_string(&path)?;
|
||||||
.await
|
|
||||||
.map_err(eyre::Report::from)
|
|
||||||
{
|
|
||||||
Ok(content) => content,
|
|
||||||
Err(err) => {
|
|
||||||
let err_title = format!("读取下载任务文件`{}`失败", path.display());
|
|
||||||
let message = err.to_message();
|
|
||||||
tracing::error!(err_title, message);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(TaskFile { path, content })
|
let progress: DownloadProgress =
|
||||||
};
|
if let Ok(progress) = serde_json::from_str(&progress_json) {
|
||||||
|
progress
|
||||||
join_set.spawn(read_task_file.in_current_span());
|
} else {
|
||||||
}
|
// 如果这个json解析失败则删除
|
||||||
|
let _ = std::fs::remove_file(&path);
|
||||||
let mut task_files = Vec::new();
|
|
||||||
while let Some(join_result) = join_set.join_next().await {
|
|
||||||
let Ok(Some(task_file)) = join_result else {
|
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
task_files.push(task_file);
|
let new_task = DownloadTask::from_progress(self.app.clone(), progress);
|
||||||
}
|
|
||||||
|
|
||||||
let (progresses_sender, progresses_receiver) = oneshot::channel();
|
|
||||||
rayon::spawn(move || {
|
|
||||||
let progresses = task_files
|
|
||||||
.into_par_iter()
|
|
||||||
.filter_map(|task_file| {
|
|
||||||
if let Ok(progress) = serde_json::from_str(&task_file.content) {
|
|
||||||
Some(progress)
|
|
||||||
} else {
|
|
||||||
// 如果这个json解析失败则删除
|
|
||||||
let _ = std::fs::remove_file(&task_file.path);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let _ = progresses_sender.send(progresses);
|
|
||||||
});
|
|
||||||
let progresses: Vec<DownloadProgress> = progresses_receiver.await?;
|
|
||||||
|
|
||||||
let mut tasks = self.download_tasks.write();
|
|
||||||
let mut restored_tasks = Vec::new();
|
|
||||||
|
|
||||||
for progress in progresses {
|
|
||||||
let new_task = DownloadTask::from_progress(self.app.clone(), progress.clone());
|
|
||||||
let state = *new_task.state_sender.borrow();
|
|
||||||
|
|
||||||
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的下载任务已经存在,则取消旧的任务
|
||||||
old_task.cancel();
|
old_task.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
restored_tasks.push(RestoredDownloadTask { state, progress });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(restored_tasks)
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_download_tasks(&self, params: &CreateDownloadTaskParams) {
|
pub fn create_download_tasks(&self, params: &CreateDownloadTaskParams) {
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
use std::{sync::Arc, time::Duration};
|
use std::{sync::Arc, time::Duration};
|
||||||
|
|
||||||
use eyre::WrapErr;
|
use eyre::WrapErr;
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use serde::Serialize;
|
|
||||||
use specta::Type;
|
|
||||||
use tauri::{AppHandle, Manager};
|
use tauri::{AppHandle, Manager};
|
||||||
use tauri_specta::Event;
|
use tauri_specta::Event;
|
||||||
use tokio::{
|
use tokio::{
|
||||||
@@ -35,12 +33,6 @@ pub struct DownloadTask {
|
|||||||
pub progress: RwLock<DownloadProgress>,
|
pub progress: RwLock<DownloadProgress>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Type)]
|
|
||||||
pub struct RestoredDownloadTask {
|
|
||||||
pub state: DownloadTaskState,
|
|
||||||
pub progress: DownloadProgress,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DownloadTask {
|
impl DownloadTask {
|
||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
#[instrument(level = "error", skip_all)]
|
#[instrument(level = "error", skip_all)]
|
||||||
@@ -176,7 +168,7 @@ impl DownloadTask {
|
|||||||
progress: RwLock::new(progress),
|
progress: RwLock::new(progress),
|
||||||
});
|
});
|
||||||
|
|
||||||
tauri::async_runtime::spawn(task.clone().process(true));
|
tauri::async_runtime::spawn(task.clone().process());
|
||||||
|
|
||||||
tasks.push(task);
|
tasks.push(task);
|
||||||
}
|
}
|
||||||
@@ -206,7 +198,7 @@ impl DownloadTask {
|
|||||||
progress: RwLock::new(progress),
|
progress: RwLock::new(progress),
|
||||||
});
|
});
|
||||||
|
|
||||||
tauri::async_runtime::spawn(task.clone().process(false));
|
tauri::async_runtime::spawn(task.clone().process());
|
||||||
|
|
||||||
task
|
task
|
||||||
}
|
}
|
||||||
@@ -295,14 +287,14 @@ impl DownloadTask {
|
|||||||
up_uid = self.trace_fields.up_uid,
|
up_uid = self.trace_fields.up_uid,
|
||||||
)
|
)
|
||||||
)]
|
)]
|
||||||
async fn process(self: Arc<Self>, emit_create_events: bool) {
|
async fn process(self: Arc<Self>) {
|
||||||
if emit_create_events {
|
|
||||||
let state = *self.state_sender.borrow();
|
let state = *self.state_sender.borrow();
|
||||||
let progress = self.progress.read().clone();
|
let progress = self.progress.read().clone();
|
||||||
let _ = DownloadEvent::TaskCreate { state, progress }.emit(&self.app);
|
let _ = DownloadEvent::TaskCreate { state, progress }.emit(&self.app);
|
||||||
}
|
|
||||||
|
|
||||||
let mut state_receiver = self.state_sender.subscribe();
|
let mut state_receiver = self.state_sender.subscribe();
|
||||||
|
state_receiver.mark_changed();
|
||||||
|
|
||||||
let mut restart_receiver = self.restart_sender.subscribe();
|
let mut restart_receiver = self.restart_sender.subscribe();
|
||||||
let mut cancel_receiver = self.cancel_sender.subscribe();
|
let mut cancel_receiver = self.cancel_sender.subscribe();
|
||||||
let mut delete_receiver = self.delete_sender.subscribe();
|
let mut delete_receiver = self.delete_sender.subscribe();
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ impl DanmakuTask {
|
|||||||
let replies = bili_client
|
let replies = bili_client
|
||||||
.get_danmaku(progress.aid, progress.cid, progress.duration)
|
.get_danmaku(progress.aid, progress.cid, progress.duration)
|
||||||
.await
|
.await
|
||||||
.wrap_err("获取弹幕失败,请[继续]以重试。如果重试多次依旧失败,请在设置中取消勾选[下载弹幕]相关的所有选项")?;
|
.wrap_err("获取弹幕失败")?;
|
||||||
|
|
||||||
let xml = replies
|
let xml = replies
|
||||||
.to_xml(progress.cid)
|
.to_xml(progress.cid)
|
||||||
|
|||||||
@@ -47,9 +47,10 @@ impl SubtitleTask {
|
|||||||
|
|
||||||
for subtitle_detail in &player_info.subtitle.subtitles {
|
for subtitle_detail in &player_info.subtitle.subtitles {
|
||||||
let url = format!("http:{}", subtitle_detail.subtitle_url);
|
let url = format!("http:{}", subtitle_detail.subtitle_url);
|
||||||
let subtitle = bili_client.get_subtitle(&url).await.wrap_err(
|
let subtitle = bili_client
|
||||||
"获取字幕失败,请[继续]以重试。如果重试多次依旧失败,请在设置中取消勾选[下载字幕]",
|
.get_subtitle(&url)
|
||||||
)?;
|
.await
|
||||||
|
.wrap_err("获取字幕失败")?;
|
||||||
|
|
||||||
let mut srt_content = String::new();
|
let mut srt_content = String::new();
|
||||||
for (i, b) in subtitle.body.iter().enumerate() {
|
for (i, b) in subtitle.body.iter().enumerate() {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::{io, path::PathBuf, sync::Arc};
|
use std::{path::PathBuf, sync::Arc};
|
||||||
|
|
||||||
use eyre::{WrapErr, eyre};
|
use eyre::{WrapErr, eyre};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -98,11 +98,21 @@ 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 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();
|
||||||
|
tauri::async_runtime::spawn_blocking(move || {
|
||||||
|
let _enter = current_span.enter();
|
||||||
|
|
||||||
let mut command = std::process::Command::new(ffmpeg_program);
|
let mut command = std::process::Command::new(ffmpeg_program);
|
||||||
|
|
||||||
command.arg("-i").arg(&video_path);
|
command.arg("-i").arg(video_path_clone);
|
||||||
command.arg("-i").arg(&audio_path);
|
command.arg("-i").arg(audio_path_clone);
|
||||||
if let Some(metadata_path) = &metadata_path {
|
if let Some(metadata_path) = metadata_path_clone {
|
||||||
command.arg("-i").arg(metadata_path);
|
command.arg("-i").arg(metadata_path);
|
||||||
command.arg("-map_metadata").arg("2");
|
command.arg("-map_metadata").arg("2");
|
||||||
}
|
}
|
||||||
@@ -110,7 +120,8 @@ impl VideoProcessTask {
|
|||||||
command.arg("-c").arg("copy");
|
command.arg("-c").arg("copy");
|
||||||
command.arg("-map").arg("0:v:0");
|
command.arg("-map").arg("0:v:0");
|
||||||
command.arg("-map").arg("1:a:0");
|
command.arg("-map").arg("1:a:0");
|
||||||
command.arg(&output_path).arg("-y");
|
|
||||||
|
command.arg(output_path_clone).arg("-y");
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
@@ -119,7 +130,21 @@ impl VideoProcessTask {
|
|||||||
command.creation_flags(0x0800_0000);
|
command.creation_flags(0x0800_0000);
|
||||||
}
|
}
|
||||||
|
|
||||||
execute_ffmpeg(command).await.wrap_err("执行FFmpeg失败")?;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
std::fs::remove_file(&video_path)
|
std::fs::remove_file(&video_path)
|
||||||
.wrap_err(format!("删除视频文件`{}`失败", video_path.display()))?;
|
.wrap_err(format!("删除视频文件`{}`失败", video_path.display()))?;
|
||||||
@@ -167,14 +192,25 @@ 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 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();
|
||||||
|
tauri::async_runtime::spawn_blocking(move || {
|
||||||
|
let _enter = current_span.enter();
|
||||||
|
|
||||||
let mut command = std::process::Command::new(ffmpeg_program);
|
let mut command = std::process::Command::new(ffmpeg_program);
|
||||||
|
|
||||||
command.arg("-i").arg(&video_path);
|
command.arg("-i").arg(video_path_clone);
|
||||||
command.arg("-i").arg(&audio_path);
|
command.arg("-i").arg(audio_path_clone);
|
||||||
|
|
||||||
command.arg("-c").arg("copy");
|
command.arg("-c").arg("copy");
|
||||||
command.arg("-map").arg("0:v:0");
|
command.arg("-map").arg("0:v:0");
|
||||||
command.arg("-map").arg("1:a:0");
|
command.arg("-map").arg("1:a:0");
|
||||||
command.arg(&output_path).arg("-y");
|
|
||||||
|
command.arg(output_path_clone).arg("-y");
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
@@ -183,7 +219,21 @@ impl VideoProcessTask {
|
|||||||
command.creation_flags(0x0800_0000);
|
command.creation_flags(0x0800_0000);
|
||||||
}
|
}
|
||||||
|
|
||||||
execute_ffmpeg(command).await.wrap_err("执行FFmpeg失败")?;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
std::fs::remove_file(&video_path)
|
std::fs::remove_file(&video_path)
|
||||||
.wrap_err(format!("删除视频文件`{}`失败", video_path.display()))?;
|
.wrap_err(format!("删除视频文件`{}`失败", video_path.display()))?;
|
||||||
@@ -229,13 +279,24 @@ impl VideoProcessTask {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let (tx, rx) = tokio::sync::oneshot::channel();
|
||||||
|
let video_path_clone = video_path.clone();
|
||||||
|
let metadata_path_clone = metadata_path.clone();
|
||||||
|
let output_path_clone = output_path.clone();
|
||||||
|
|
||||||
|
let current_span = tracing::Span::current();
|
||||||
|
tauri::async_runtime::spawn_blocking(move || {
|
||||||
|
let _enter = current_span.enter();
|
||||||
|
|
||||||
let mut command = std::process::Command::new(ffmpeg_program);
|
let mut command = std::process::Command::new(ffmpeg_program);
|
||||||
|
|
||||||
command.arg("-i").arg(&video_path);
|
command.arg("-i").arg(video_path_clone);
|
||||||
command.arg("-i").arg(&metadata_path);
|
command.arg("-i").arg(metadata_path_clone);
|
||||||
|
|
||||||
command.arg("-map_metadata").arg("1");
|
command.arg("-map_metadata").arg("1");
|
||||||
command.arg("-c").arg("copy");
|
command.arg("-c").arg("copy");
|
||||||
command.arg(&output_path).arg("-y");
|
|
||||||
|
command.arg(output_path_clone).arg("-y");
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
@@ -244,7 +305,21 @@ impl VideoProcessTask {
|
|||||||
command.creation_flags(0x0800_0000);
|
command.creation_flags(0x0800_0000);
|
||||||
}
|
}
|
||||||
|
|
||||||
execute_ffmpeg(command).await.wrap_err("执行FFmpeg失败")?;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
std::fs::remove_file(&video_path)
|
std::fs::remove_file(&video_path)
|
||||||
.wrap_err(format!("删除视频文件`{}`失败", video_path.display()))?;
|
.wrap_err(format!("删除视频文件`{}`失败", video_path.display()))?;
|
||||||
@@ -292,10 +367,7 @@ impl VideoProcessTask {
|
|||||||
let bili_client = app.get_bili_client();
|
let bili_client = app.get_bili_client();
|
||||||
let cid = Some(progress.cid);
|
let cid = Some(progress.cid);
|
||||||
|
|
||||||
let skip_segments = bili_client
|
let skip_segments = bili_client.get_skip_segments(bvid, cid).await?;
|
||||||
.get_skip_segments(bvid, cid)
|
|
||||||
.await
|
|
||||||
.wrap_err("获取广告片段失败,请[继续]以重试。如果重试多次依旧失败,请在设置中取消勾选[标记广告]")?;
|
|
||||||
for segment in skip_segments.0 {
|
for segment in skip_segments.0 {
|
||||||
if let Some(chapter_segment) = segment.into_chapter_segment() {
|
if let Some(chapter_segment) = segment.into_chapter_segment() {
|
||||||
chapter_segments.insert(chapter_segment);
|
chapter_segments.insert(chapter_segment);
|
||||||
@@ -316,32 +388,3 @@ 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?
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -106,7 +106,6 @@ 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())
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
"height": 600,
|
"height": 600,
|
||||||
"minWidth": 800,
|
"minWidth": 800,
|
||||||
"minHeight": 600,
|
"minHeight": 600,
|
||||||
"visible": false,
|
|
||||||
"decorations": false,
|
"decorations": false,
|
||||||
"theme": "Light"
|
"theme": "Light"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
"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"
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
"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
-2
@@ -125,7 +125,7 @@ async restartDownloadTasks(taskIds: string[]) : Promise<void> {
|
|||||||
async restartDownloadTask(params: RestartDownloadTaskParams) : Promise<void> {
|
async restartDownloadTask(params: RestartDownloadTaskParams) : Promise<void> {
|
||||||
await TAURI_INVOKE("restart_download_task", { params });
|
await TAURI_INVOKE("restart_download_task", { params });
|
||||||
},
|
},
|
||||||
async restoreDownloadTasks() : Promise<Result<RestoredDownloadTask[], CommandError>> {
|
async restoreDownloadTasks() : Promise<Result<null, CommandError>> {
|
||||||
try {
|
try {
|
||||||
return { status: "ok", data: await TAURI_INVOKE("restore_download_tasks") };
|
return { status: "ok", data: await TAURI_INVOKE("restore_download_tasks") };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -429,7 +429,6 @@ export type RatingInBangumi = { count: number; score: number }
|
|||||||
export type RatingInBangumiFollow = { score: number; count: number }
|
export type RatingInBangumiFollow = { score: number; count: number }
|
||||||
export type RecommendSeason = { cover: string; ep_count: string; id: number; season_url: string; subtitle: string; title: string; view: number }
|
export type RecommendSeason = { cover: string; ep_count: string; id: number; season_url: string; subtitle: string; title: string; view: number }
|
||||||
export type RestartDownloadTaskParams = { task_id: string; video_task_selected: boolean; audio_task_selected: boolean; merge_selected: boolean; embed_chapter_selected: boolean; embed_skip_selected: boolean; subtitle_task_selected: boolean; xml_danmaku_selected: boolean; ass_danmaku_selected: boolean; json_danmaku_selected: boolean; cover_task_selected: boolean; nfo_task_selected: boolean; json_task_selected: boolean; video_quality: VideoQuality; codec_type: CodecType; audio_quality: AudioQuality }
|
export type RestartDownloadTaskParams = { task_id: string; video_task_selected: boolean; audio_task_selected: boolean; merge_selected: boolean; embed_chapter_selected: boolean; embed_skip_selected: boolean; subtitle_task_selected: boolean; xml_danmaku_selected: boolean; ass_danmaku_selected: boolean; json_danmaku_selected: boolean; cover_task_selected: boolean; nfo_task_selected: boolean; json_task_selected: boolean; video_quality: VideoQuality; codec_type: CodecType; audio_quality: AudioQuality }
|
||||||
export type RestoredDownloadTask = { state: DownloadTaskState; progress: DownloadProgress }
|
|
||||||
export type Rights = { bp: number; elec: number; download: number; movie: number; pay: number; hd5: number; no_reprint: number; autoplay: number; ugc_pay: number; is_cooperation: number; ugc_pay_preview: number; no_background: number; clean_mode: number; is_stein_gate: number; is_360: number; no_share: number; arc_pay: number; free_watch: number }
|
export type Rights = { bp: number; elec: number; download: number; movie: number; pay: number; hd5: number; no_reprint: number; autoplay: number; ugc_pay: number; is_cooperation: number; ugc_pay_preview: number; no_background: number; clean_mode: number; is_stein_gate: number; is_360: number; no_share: number; arc_pay: number; free_watch: number }
|
||||||
export type RightsInBangumi = { allow_bp: number; allow_bp_rank: number; allow_download: number; allow_review: number; area_limit: number; ban_area_show: number; can_watch: number; copyright: string; forbid_pre: number; freya_white: number; is_cover_show: number; is_preview: number; only_vip_download: number; resource: string; watch_platform: number }
|
export type RightsInBangumi = { allow_bp: number; allow_bp_rank: number; allow_download: number; allow_review: number; area_limit: number; ban_area_show: number; can_watch: number; copyright: string; forbid_pre: number; freya_white: number; is_cover_show: number; is_preview: number; only_vip_download: number; resource: string; watch_platform: number }
|
||||||
export type RightsInBangumiEp = { allow_dm: number; allow_download: number; area_limit: number }
|
export type RightsInBangumiEp = { allow_dm: number; allow_download: number; area_limit: number }
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ onMounted(async () => {
|
|||||||
...progress,
|
...progress,
|
||||||
state,
|
state,
|
||||||
percentage: 0,
|
percentage: 0,
|
||||||
stateIndicator: getStateIndicator(state),
|
stateIndicator: '',
|
||||||
taskIndicator: '',
|
taskIndicator: '',
|
||||||
}
|
}
|
||||||
store.updateProgresses((progresses) => {
|
store.updateProgresses((progresses) => {
|
||||||
@@ -48,8 +48,21 @@ onMounted(async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let stateIndicator = ''
|
||||||
|
if (state === 'Pending') {
|
||||||
|
stateIndicator = '排队中'
|
||||||
|
} else if (state === 'Downloading') {
|
||||||
|
stateIndicator = '下载中'
|
||||||
|
} else if (state === 'Paused') {
|
||||||
|
stateIndicator = '已暂停'
|
||||||
|
} else if (state === 'Completed') {
|
||||||
|
stateIndicator = '下载完成'
|
||||||
|
} else if (state === 'Failed') {
|
||||||
|
stateIndicator = '下载失败'
|
||||||
|
}
|
||||||
|
|
||||||
progressData.state = state
|
progressData.state = state
|
||||||
progressData.stateIndicator = getStateIndicator(state)
|
progressData.stateIndicator = stateIndicator
|
||||||
})
|
})
|
||||||
} else if (event === 'TaskSleeping') {
|
} else if (event === 'TaskSleeping') {
|
||||||
const { task_id, remaining_sec } = data
|
const { task_id, remaining_sec } = data
|
||||||
@@ -138,41 +151,8 @@ onMounted(async () => {
|
|||||||
const result = await commands.restoreDownloadTasks()
|
const result = await commands.restoreDownloadTasks()
|
||||||
if (result.status === 'error') {
|
if (result.status === 'error') {
|
||||||
console.error(result.error)
|
console.error(result.error)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
store.updateProgresses((progresses) => {
|
|
||||||
for (const { state, progress } of result.data) {
|
|
||||||
const progressData: ProgressData = {
|
|
||||||
...progress,
|
|
||||||
state,
|
|
||||||
percentage: 0,
|
|
||||||
stateIndicator: getStateIndicator(state),
|
|
||||||
taskIndicator: '',
|
|
||||||
}
|
|
||||||
|
|
||||||
progresses.set(progress.task_id, progressData)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function getStateIndicator(state: DownloadTaskState) {
|
|
||||||
let stateIndicator = ''
|
|
||||||
|
|
||||||
if (state === 'Pending') {
|
|
||||||
stateIndicator = '排队中'
|
|
||||||
} else if (state === 'Downloading') {
|
|
||||||
stateIndicator = '下载中'
|
|
||||||
} else if (state === 'Paused') {
|
|
||||||
stateIndicator = '已暂停'
|
|
||||||
} else if (state === 'Completed') {
|
|
||||||
stateIndicator = '下载完成'
|
|
||||||
} else if (state === 'Failed') {
|
|
||||||
stateIndicator = '下载失败'
|
|
||||||
}
|
|
||||||
|
|
||||||
return stateIndicator
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
Reference in New Issue
Block a user