mirror of
https://github.com/lanyeeee/bilibili-video-downloader.git
synced 2026-09-08 17:16:52 +08:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1254c6bf2a | ||
|
|
9fe5cf0a45 | ||
|
|
b4b35f6055 | ||
|
|
f0e79b584f |
@@ -57,7 +57,7 @@ https://github.com/user-attachments/assets/adf84b93-684f-43f3-9948-6ba527213812
|
||||
我能想到的解决办法只有:
|
||||
|
||||
1. 根据下面的**如何构建(build)**,自行编译
|
||||
2. 希望你相信我的承诺,我承诺你在[Release页面](https://github.com/lanyeeee/bilibili-video-downloader/releases)下载到的所有东西都是安全的
|
||||
2. 希望你相信我的承诺,我承诺你在[Release页面](https://github.com/lanyeeee/bilibili-video-downloader/releases)下载到的所有东西都是安全的。切勿轻信他人分享的文件,请**仅**在[Release页面](https://github.com/lanyeeee/bilibili-video-downloader/releases)下载。任何不是从该页面下载的版本均可能**已被篡改**并**真的包含病毒**(而非误报),包括但不限于`网盘`、`通过邮箱或社交软件分享`、`issue或discussion里的文件`、`其他fork(仓库)`、`其他网站`
|
||||
|
||||
## 🛠️如何构建(build)
|
||||
|
||||
|
||||
@@ -192,8 +192,6 @@ pub mod v1 {
|
||||
Video1080P60 = 116,
|
||||
#[serde(rename = "4K")]
|
||||
Video4K = 120,
|
||||
#[serde(rename = "SDR")]
|
||||
VideoSDR = 122,
|
||||
#[serde(rename = "HDR")]
|
||||
VideoHDR = 125,
|
||||
#[serde(rename = "Dolby")]
|
||||
@@ -329,7 +327,6 @@ pub mod v1 {
|
||||
pub part_title: Option<String>,
|
||||
pub part_order: Option<i64>,
|
||||
pub episode_title: String,
|
||||
pub episode_title_in_collection: Option<String>,
|
||||
pub episode_order: i64,
|
||||
pub up_name: Option<String>,
|
||||
pub up_uid: Option<i64>,
|
||||
|
||||
Generated
-63
@@ -301,7 +301,6 @@ dependencies = [
|
||||
"parking_lot 0.12.4",
|
||||
"prost",
|
||||
"rand 0.9.1",
|
||||
"rayon",
|
||||
"reqwest",
|
||||
"reqwest-middleware",
|
||||
"reqwest-retry",
|
||||
@@ -316,14 +315,12 @@ dependencies = [
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-opener",
|
||||
"tauri-plugin-os",
|
||||
"tauri-plugin-window-state",
|
||||
"tauri-specta",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-appender",
|
||||
"tracing-error",
|
||||
"tracing-subscriber",
|
||||
"urlencoding",
|
||||
"uuid",
|
||||
"yaserde",
|
||||
]
|
||||
@@ -680,25 +677,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.21"
|
||||
@@ -3381,26 +3359,6 @@ version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.16"
|
||||
@@ -4505,21 +4463,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "tauri-runtime"
|
||||
version = "2.7.0"
|
||||
@@ -5134,12 +5077,6 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urlencoding"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||
|
||||
[[package]]
|
||||
name = "urlpattern"
|
||||
version = "0.3.0"
|
||||
|
||||
@@ -22,7 +22,6 @@ tauri = { version = "2", features = [] }
|
||||
tauri-plugin-opener = "2"
|
||||
tauri-plugin-os = "2"
|
||||
tauri-plugin-dialog = "2"
|
||||
tauri-plugin-window-state = "2"
|
||||
dlopen2 = { version = "0.8.2" }
|
||||
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" }
|
||||
rand = { version = "0.9.1" }
|
||||
base64 = { version = "0.22.1" }
|
||||
rayon = { version = "1.12.0" }
|
||||
urlencoding = { version = "2.1.3" }
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
|
||||
@@ -351,8 +351,7 @@ impl BiliClient {
|
||||
.read()
|
||||
.get("https://api.bilibili.com/x/space/wbi/arc/search")
|
||||
.query(¶ms)
|
||||
// 不带DedeUserID会随机触发412错误
|
||||
.header("cookie", format!("DedeUserID=1; {}", self.get_cookie()));
|
||||
.header("cookie", self.get_cookie());
|
||||
let http_resp = request.send().await?;
|
||||
// 检查http响应状态码
|
||||
let status = http_resp.status();
|
||||
@@ -435,7 +434,7 @@ impl BiliClient {
|
||||
let params = json!({
|
||||
"cid": cid,
|
||||
"qn": 127,
|
||||
"fnval": 139216,
|
||||
"fnval": 4048,
|
||||
"drm_tech_type": 2,
|
||||
});
|
||||
// 发送获取番剧url的请求
|
||||
@@ -478,7 +477,7 @@ impl BiliClient {
|
||||
let params = json!({
|
||||
"cid": cid,
|
||||
"qn": 127,
|
||||
"fnval": 139216,
|
||||
"fnval": 4048,
|
||||
"drm_tech_type": 2,
|
||||
"from_client": "BROWSER",
|
||||
});
|
||||
@@ -522,7 +521,7 @@ impl BiliClient {
|
||||
let params = json!({
|
||||
"ep_id": ep_id,
|
||||
"qn": 127,
|
||||
"fnval": 139216,
|
||||
"fnval": 4048,
|
||||
"drm_tech_type": 2,
|
||||
});
|
||||
// 发送获取课程url的请求
|
||||
@@ -1098,12 +1097,7 @@ impl BiliClient {
|
||||
|
||||
pub fn get_cookie(&self) -> String {
|
||||
let sessdata = self.app.get_config().read().sessdata.clone();
|
||||
|
||||
let trimmed = sessdata.trim_end_matches(';');
|
||||
let decoded = urlencoding::decode(trimmed).unwrap_or_else(|_| trimmed.into());
|
||||
let encoded = urlencoding::encode(&decoded);
|
||||
|
||||
format!("SESSDATA={encoded}")
|
||||
format!("SESSDATA={}", sessdata.trim_end_matches(';'))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ use tracing::instrument;
|
||||
|
||||
use crate::{
|
||||
config::Config,
|
||||
downloader::download_task::RestoredDownloadTask,
|
||||
errors::{CommandError, CommandResult},
|
||||
extensions::AppHandleExt,
|
||||
logger,
|
||||
@@ -311,14 +310,13 @@ pub fn restart_download_task(app: AppHandle, params: RestartDownloadTaskParams)
|
||||
#[tauri::command(async)]
|
||||
#[specta::specta]
|
||||
#[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 restored_tasks = download_manager
|
||||
download_manager
|
||||
.restore_download_tasks()
|
||||
.await
|
||||
.map_err(|err| CommandError::from("恢复下载任务失败", err))?;
|
||||
tracing::debug!("恢复下载任务成功");
|
||||
Ok(restored_tasks)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
|
||||
+26
-54
@@ -53,7 +53,7 @@ impl Config {
|
||||
let app_data_dir = app.path().app_data_dir()?;
|
||||
let config_path = app_data_dir.join("config.json");
|
||||
|
||||
let mut config = if config_path.exists() {
|
||||
let config = if config_path.exists() {
|
||||
let config_string = std::fs::read_to_string(config_path)?;
|
||||
match serde_json::from_str(&config_string) {
|
||||
// 如果能够直接解析为Config,则直接返回
|
||||
@@ -65,8 +65,6 @@ impl Config {
|
||||
} else {
|
||||
Config::default(&app_data_dir)
|
||||
};
|
||||
|
||||
config.reconcile_priority_lists();
|
||||
config.save(app)?;
|
||||
Ok(config)
|
||||
}
|
||||
@@ -80,18 +78,6 @@ impl Config {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reconcile_priority_lists(&mut self) {
|
||||
reconcile(
|
||||
&mut self.video_quality_priority,
|
||||
DEFAULT_VIDEO_QUALITY_PRIORITY,
|
||||
);
|
||||
reconcile(
|
||||
&mut self.audio_quality_priority,
|
||||
DEFAULT_AUDIO_QUALITY_PRIORITY,
|
||||
);
|
||||
reconcile(&mut self.codec_type_priority, DEFAULT_CODEC_TYPE_PRIORITY);
|
||||
}
|
||||
|
||||
fn merge_config(config_string: &str, app_data_dir: &Path) -> Config {
|
||||
let Ok(mut json_value) = serde_json::from_str::<serde_json::Value>(config_string) else {
|
||||
return Config::default(app_data_dir);
|
||||
@@ -117,14 +103,36 @@ impl Config {
|
||||
fn default(app_data_dir: &Path) -> Config {
|
||||
const DEFAULT_FMT_FOR_PART: &str =
|
||||
"{collection_title}/{episode_title}/{episode_title}-P{part_order} {part_title}";
|
||||
let default_video_quality_priority = vec![
|
||||
VideoQuality::Video8K,
|
||||
VideoQuality::VideoDolby,
|
||||
VideoQuality::VideoHDR,
|
||||
VideoQuality::Video4K,
|
||||
VideoQuality::Video1080P60,
|
||||
VideoQuality::Video1080PPlus,
|
||||
VideoQuality::Video1080P,
|
||||
VideoQuality::VideoAiRepair,
|
||||
VideoQuality::Video720P60,
|
||||
VideoQuality::Video720P,
|
||||
VideoQuality::Video480P,
|
||||
VideoQuality::Video360P,
|
||||
VideoQuality::Video240P,
|
||||
];
|
||||
let default_audio_quality_priority = vec![
|
||||
AudioQuality::AudioHiRes,
|
||||
AudioQuality::AudioDolby,
|
||||
AudioQuality::Audio192K,
|
||||
AudioQuality::Audio132K,
|
||||
AudioQuality::Audio64K,
|
||||
];
|
||||
|
||||
Config {
|
||||
download_dir: app_data_dir.join("视频下载"),
|
||||
enable_file_logger: true,
|
||||
sessdata: String::new(),
|
||||
video_quality_priority: DEFAULT_VIDEO_QUALITY_PRIORITY.to_vec(),
|
||||
codec_type_priority: DEFAULT_CODEC_TYPE_PRIORITY.to_vec(),
|
||||
audio_quality_priority: DEFAULT_AUDIO_QUALITY_PRIORITY.to_vec(),
|
||||
video_quality_priority: default_video_quality_priority,
|
||||
codec_type_priority: vec![CodecType::AVC, CodecType::HEVC, CodecType::AV1],
|
||||
audio_quality_priority: default_audio_quality_priority,
|
||||
download_video: true,
|
||||
download_audio: true,
|
||||
auto_merge: true,
|
||||
@@ -154,14 +162,6 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
fn reconcile<T: Copy + Eq>(priority: &mut Vec<T>, all: &[T]) {
|
||||
for &item in all {
|
||||
if !priority.contains(&item) {
|
||||
priority.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Type)]
|
||||
pub enum ProxyMode {
|
||||
#[default]
|
||||
@@ -176,31 +176,3 @@ pub enum FileExistAction {
|
||||
Overwrite,
|
||||
Skip,
|
||||
}
|
||||
|
||||
const DEFAULT_VIDEO_QUALITY_PRIORITY: &[VideoQuality] = &[
|
||||
VideoQuality::Video8K,
|
||||
VideoQuality::VideoDolby,
|
||||
VideoQuality::VideoHDR,
|
||||
VideoQuality::VideoSDR,
|
||||
VideoQuality::Video4K,
|
||||
VideoQuality::Video1080P60,
|
||||
VideoQuality::Video1080PPlus,
|
||||
VideoQuality::Video1080P,
|
||||
VideoQuality::VideoAiRepair,
|
||||
VideoQuality::Video720P60,
|
||||
VideoQuality::Video720P,
|
||||
VideoQuality::Video480P,
|
||||
VideoQuality::Video360P,
|
||||
VideoQuality::Video240P,
|
||||
];
|
||||
|
||||
const DEFAULT_AUDIO_QUALITY_PRIORITY: &[AudioQuality] = &[
|
||||
AudioQuality::AudioHiRes,
|
||||
AudioQuality::AudioDolby,
|
||||
AudioQuality::Audio192K,
|
||||
AudioQuality::Audio132K,
|
||||
AudioQuality::Audio64K,
|
||||
];
|
||||
|
||||
const DEFAULT_CODEC_TYPE_PRIORITY: &[CodecType] =
|
||||
&[CodecType::AVC, CodecType::HEVC, CodecType::AV1];
|
||||
|
||||
@@ -10,17 +10,12 @@ use std::{
|
||||
|
||||
use eyre::{WrapErr, eyre};
|
||||
use parking_lot::RwLock;
|
||||
use rayon::prelude::*;
|
||||
use tauri::{AppHandle, Manager};
|
||||
use tauri_specta::Event;
|
||||
use tokio::{
|
||||
sync::{Semaphore, oneshot},
|
||||
task::JoinSet,
|
||||
};
|
||||
use tracing::{Instrument, instrument};
|
||||
use tokio::sync::Semaphore;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::{
|
||||
downloader::download_progress::DownloadProgress,
|
||||
events::DownloadEvent,
|
||||
extensions::{AppHandleExt, EyreReportToMessage},
|
||||
types::{
|
||||
@@ -29,7 +24,10 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
use super::download_task::{DownloadTask, RestoredDownloadTask};
|
||||
use super::{
|
||||
download_progress::DownloadProgress, download_task::DownloadTask,
|
||||
download_task_state::DownloadTaskState,
|
||||
};
|
||||
|
||||
pub struct DownloadManager {
|
||||
pub app: AppHandle,
|
||||
@@ -63,92 +61,41 @@ impl DownloadManager {
|
||||
}
|
||||
|
||||
#[instrument(level = "error", skip_all)]
|
||||
pub async fn restore_download_tasks(&self) -> eyre::Result<Vec<RestoredDownloadTask>> {
|
||||
struct TaskFile {
|
||||
path: PathBuf,
|
||||
content: String,
|
||||
}
|
||||
|
||||
pub fn restore_download_tasks(&self) -> eyre::Result<()> {
|
||||
let task_dir = self.get_task_dir()?;
|
||||
std::fs::create_dir_all(&task_dir)
|
||||
.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) {
|
||||
let read_task_file = async move {
|
||||
let path = entry.path();
|
||||
let path = entry.path();
|
||||
let extension = path.extension().and_then(|s| s.to_str());
|
||||
if extension != Some("json") {
|
||||
// 如果这个文件不是json则删除
|
||||
let _ = std::fs::remove_file(&path);
|
||||
continue;
|
||||
}
|
||||
|
||||
let extension = path.extension().and_then(|s| s.to_str());
|
||||
if extension != Some("json") {
|
||||
// 如果这个文件不是json则删除
|
||||
let _ = tokio::fs::remove_file(path).await;
|
||||
return None;
|
||||
}
|
||||
let progress_json = std::fs::read_to_string(&path)?;
|
||||
|
||||
let content = match tokio::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;
|
||||
}
|
||||
let progress: DownloadProgress =
|
||||
if let Ok(progress) = serde_json::from_str(&progress_json) {
|
||||
progress
|
||||
} else {
|
||||
// 如果这个json解析失败则删除
|
||||
let _ = std::fs::remove_file(&path);
|
||||
continue;
|
||||
};
|
||||
|
||||
Some(TaskFile { path, content })
|
||||
};
|
||||
|
||||
join_set.spawn(read_task_file.in_current_span());
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
task_files.push(task_file);
|
||||
}
|
||||
|
||||
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 new_task = DownloadTask::from_progress(self.app.clone(), progress);
|
||||
let old_task = tasks.insert(new_task.task_id.clone(), new_task);
|
||||
if let Some(old_task) = old_task {
|
||||
// 如果同一个ID的下载任务已经存在,则取消旧的任务
|
||||
old_task.cancel();
|
||||
let _ = old_task.cancel_sender.send(());
|
||||
}
|
||||
|
||||
restored_tasks.push(RestoredDownloadTask { state, progress });
|
||||
}
|
||||
|
||||
Ok(restored_tasks)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn create_download_tasks(&self, params: &CreateDownloadTaskParams) {
|
||||
@@ -173,7 +120,8 @@ impl DownloadManager {
|
||||
tracing::error!(err_title, message);
|
||||
continue;
|
||||
};
|
||||
task.pause();
|
||||
task.set_state(DownloadTaskState::Paused);
|
||||
tracing::debug!("已将ID对应的下载任务状态设置为`Paused`");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,7 +139,8 @@ impl DownloadManager {
|
||||
tracing::error!(err_title, message);
|
||||
continue;
|
||||
};
|
||||
task.resume();
|
||||
task.set_state(DownloadTaskState::Pending);
|
||||
tracing::debug!("已将ID对应的下载任务状态设置为`Pending`");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,13 +158,27 @@ impl DownloadManager {
|
||||
tracing::error!(err_title, message);
|
||||
continue;
|
||||
};
|
||||
if let Err(err) = task.delete() {
|
||||
// TODO: 应该先发删除新号再删文件
|
||||
// 因为发信号失败会把任务重新塞回去
|
||||
// 目前先删文件会导致发信号失败时出现 任务还在但文件没了的情况
|
||||
if let Err(err) = self.delete_progress_file(task_id) {
|
||||
let err_title = "删除下载任务失败";
|
||||
let message = err.to_message();
|
||||
tracing::error!(err_title, message);
|
||||
tasks.insert(task_id.clone(), task);
|
||||
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对应的下载任务删除");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,12 +197,15 @@ impl DownloadManager {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Err(err) = task.restart() {
|
||||
if let Err(err) = task.restart_sender.send(()).map_err(eyre::Report::from) {
|
||||
let err_title = "重来下载任务失败";
|
||||
let err = err.wrap_err("通知ID对应的下载任务重来失败");
|
||||
let message = err.to_message();
|
||||
tracing::error!(err_title, message);
|
||||
continue;
|
||||
}
|
||||
|
||||
tracing::debug!("已通知ID对应的下载任务重来");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,11 +222,37 @@ impl DownloadManager {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Err(err) = task.restart_with_params(params) {
|
||||
// TODO: 把这块代码变成DownloadProgress的mark_restart函数
|
||||
{
|
||||
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 = err.wrap_err("通知ID对应的下载任务重来失败");
|
||||
let message = err.to_message();
|
||||
tracing::error!(err_title, message);
|
||||
return;
|
||||
}
|
||||
|
||||
tracing::debug!("已通知ID对应的下载任务重来");
|
||||
}
|
||||
|
||||
async fn emit_download_speed_loop(app: AppHandle, byte_per_sec: Arc<AtomicU64>) {
|
||||
@@ -282,4 +274,14 @@ impl DownloadManager {
|
||||
let task_dir = app_data_dir.join(".下载任务");
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ use crate::{
|
||||
bangumi_info::BangumiInfo,
|
||||
cheese_info::CheeseInfo,
|
||||
codec_type::CodecType,
|
||||
get_normal_info_params::GetNormalInfoParams,
|
||||
normal_info::{NormalInfo, UgcSeason},
|
||||
video_quality::VideoQuality,
|
||||
},
|
||||
@@ -55,7 +54,6 @@ pub struct DownloadProgress {
|
||||
pub part_title: Option<String>,
|
||||
pub part_order: Option<i64>,
|
||||
pub episode_title: String,
|
||||
pub episode_title_in_collection: Option<String>,
|
||||
pub episode_order: i64,
|
||||
pub up_name: Option<String>,
|
||||
pub up_uid: Option<i64>,
|
||||
@@ -131,7 +129,6 @@ impl DownloadProgress {
|
||||
part_title: None,
|
||||
part_order: None,
|
||||
episode_title: episode.show_title.clone().unwrap_or(episode.title.clone()),
|
||||
episode_title_in_collection: None,
|
||||
episode_order,
|
||||
up_name,
|
||||
up_uid,
|
||||
@@ -182,7 +179,6 @@ impl DownloadProgress {
|
||||
part_title: None,
|
||||
part_order: None,
|
||||
episode_title: episode.title.clone(),
|
||||
episode_title_in_collection: None,
|
||||
episode_order: episode.index,
|
||||
up_name: Some(info.up_info.uname.clone()),
|
||||
up_uid: Some(info.up_info.mid),
|
||||
@@ -217,9 +213,6 @@ impl DownloadProgress {
|
||||
|
||||
self.prepare(app).await.wrap_err("准备下载失败")?;
|
||||
|
||||
self.completed_ts = None; // 重置完成时间戳
|
||||
download_task.update_progress(|p| *p = self.clone());
|
||||
|
||||
let progress_before_hook = self.clone();
|
||||
app.get_plugin_manager()
|
||||
.run_hook(HookContext::AfterPrepare(AfterPrepareContext::new(self)))
|
||||
@@ -228,6 +221,9 @@ impl DownloadProgress {
|
||||
download_task.update_progress(|p| *p = self.clone());
|
||||
}
|
||||
|
||||
self.completed_ts = None; // 重置完成时间戳
|
||||
download_task.update_progress(|p| *p = self.clone());
|
||||
|
||||
std::fs::create_dir_all(&self.episode_dir)
|
||||
.wrap_err(format!("创建目录`{}`失败", self.episode_dir.display()))?;
|
||||
|
||||
@@ -250,7 +246,6 @@ impl DownloadProgress {
|
||||
tracing::debug!("音频下载任务完成");
|
||||
}
|
||||
|
||||
*self = download_task.progress.read().clone();
|
||||
let progress_before_hook = self.clone();
|
||||
app.get_plugin_manager()
|
||||
.run_hook(HookContext::BeforeVideoProcess(
|
||||
@@ -325,7 +320,6 @@ impl DownloadProgress {
|
||||
download_task.update_progress(|p| p.completed_ts = Some(completed_ts));
|
||||
}
|
||||
|
||||
*self = download_task.progress.read().clone();
|
||||
let progress_before_hook = self.clone();
|
||||
app.get_plugin_manager()
|
||||
.run_hook(HookContext::OnCompleted(OnCompletedContext::new(self)))
|
||||
@@ -344,18 +338,6 @@ impl DownloadProgress {
|
||||
let audio_selected = self.audio_task.selected;
|
||||
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) {
|
||||
// 如果视频和音频都没有选中,或者都已经完成,则更新需要格式化的字段就返回
|
||||
self.update_fmt_fields(app)
|
||||
@@ -370,7 +352,6 @@ impl DownloadProgress {
|
||||
let Some(bvid) = &self.bvid else {
|
||||
return Err(eyre!("progress中的bvid为None,无法获取视频链接"));
|
||||
};
|
||||
|
||||
let media_url = bili_client
|
||||
.get_normal_url(bvid, self.cid)
|
||||
.await
|
||||
@@ -461,7 +442,6 @@ impl DownloadProgress {
|
||||
pub_ts: self.pub_ts,
|
||||
collection_title: self.collection_title.clone(),
|
||||
episode_title: self.episode_title.clone(),
|
||||
episode_title_in_collection: self.episode_title_in_collection.clone(),
|
||||
episode_order: self.episode_order,
|
||||
part_title: self.part_title.clone(),
|
||||
part_order: self.part_order,
|
||||
@@ -546,7 +526,6 @@ fn create_normal_progresses_for_single(
|
||||
part_title: Some(page.part.clone()),
|
||||
part_order: Some(page.page),
|
||||
episode_title: info.title.clone(),
|
||||
episode_title_in_collection: None,
|
||||
episode_order: 1,
|
||||
up_name: Some(info.owner.name.clone()),
|
||||
up_uid: Some(info.owner.mid),
|
||||
@@ -585,7 +564,6 @@ fn create_normal_progresses_for_single(
|
||||
part_title: None,
|
||||
part_order: None,
|
||||
episode_title: info.title.clone(),
|
||||
episode_title_in_collection: None,
|
||||
episode_order: 1,
|
||||
up_name: Some(info.owner.name.clone()),
|
||||
up_uid: Some(info.owner.mid),
|
||||
@@ -624,7 +602,6 @@ fn create_normal_progresses_for_single(
|
||||
part_title: Some(page.part.clone()),
|
||||
part_order: Some(page.page),
|
||||
episode_title: info.title.clone(),
|
||||
episode_title_in_collection: None,
|
||||
episode_order: 1,
|
||||
up_name: Some(info.owner.name.clone()),
|
||||
up_uid: Some(info.owner.mid),
|
||||
@@ -676,10 +653,6 @@ fn create_normal_progresses_for_season(
|
||||
|
||||
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();
|
||||
|
||||
if let Some(cid) = cid {
|
||||
@@ -699,8 +672,7 @@ fn create_normal_progresses_for_season(
|
||||
collection_title: ugc_season.title.clone(),
|
||||
part_title: Some(page.part.clone()),
|
||||
part_order: Some(page.page),
|
||||
episode_title: episode_title.clone(),
|
||||
episode_title_in_collection: episode_title_in_collection.clone(),
|
||||
episode_title: ep.title.clone(),
|
||||
episode_order,
|
||||
up_name: Some(info.owner.name.clone()),
|
||||
up_uid: Some(info.owner.mid),
|
||||
@@ -738,8 +710,7 @@ fn create_normal_progresses_for_season(
|
||||
collection_title: ugc_season.title.clone(),
|
||||
part_title: None,
|
||||
part_order: None,
|
||||
episode_title: episode_title.clone(),
|
||||
episode_title_in_collection: episode_title_in_collection.clone(),
|
||||
episode_title: ep.title.clone(),
|
||||
episode_order,
|
||||
up_name: Some(info.owner.name.clone()),
|
||||
up_uid: Some(info.owner.mid),
|
||||
@@ -778,8 +749,7 @@ fn create_normal_progresses_for_season(
|
||||
collection_title: ugc_season.title.clone(),
|
||||
part_title: Some(page.part.clone()),
|
||||
part_order: Some(page.page),
|
||||
episode_title: episode_title.clone(),
|
||||
episode_title_in_collection: episode_title_in_collection.clone(),
|
||||
episode_title: ep.title.clone(),
|
||||
episode_order,
|
||||
up_name: Some(info.owner.name.clone()),
|
||||
up_uid: Some(info.owner.mid),
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use eyre::WrapErr;
|
||||
use parking_lot::RwLock;
|
||||
use serde::Serialize;
|
||||
use specta::Type;
|
||||
use tauri::{AppHandle, Manager};
|
||||
use tauri::AppHandle;
|
||||
use tauri_specta::Event;
|
||||
use tokio::{
|
||||
sync::{SemaphorePermit, watch},
|
||||
@@ -16,10 +14,7 @@ use crate::{
|
||||
downloader::episode_type::EpisodeType,
|
||||
events::DownloadEvent,
|
||||
extensions::{AppHandleExt, EyreReportToMessage},
|
||||
types::{
|
||||
create_download_task_params::CreateDownloadTaskParams,
|
||||
restart_download_task_params::RestartDownloadTaskParams,
|
||||
},
|
||||
types::create_download_task_params::CreateDownloadTaskParams,
|
||||
};
|
||||
|
||||
use super::{download_progress::DownloadProgress, download_task_state::DownloadTaskState};
|
||||
@@ -35,12 +30,6 @@ pub struct DownloadTask {
|
||||
pub progress: RwLock<DownloadProgress>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Type)]
|
||||
pub struct RestoredDownloadTask {
|
||||
pub state: DownloadTaskState,
|
||||
pub progress: DownloadProgress,
|
||||
}
|
||||
|
||||
impl DownloadTask {
|
||||
#[allow(clippy::too_many_lines)]
|
||||
#[instrument(level = "error", skip_all)]
|
||||
@@ -138,7 +127,6 @@ impl DownloadTask {
|
||||
ep_id = progress.ep_id,
|
||||
collection_title = progress.collection_title,
|
||||
episode_title = progress.episode_title,
|
||||
episode_title_in_collection = progress.episode_title_in_collection,
|
||||
episode_order = progress.episode_order,
|
||||
part_title = progress.part_title,
|
||||
part_order = progress.part_order,
|
||||
@@ -176,7 +164,7 @@ impl DownloadTask {
|
||||
progress: RwLock::new(progress),
|
||||
});
|
||||
|
||||
tauri::async_runtime::spawn(task.clone().process(true));
|
||||
tauri::async_runtime::spawn(task.clone().process());
|
||||
|
||||
tasks.push(task);
|
||||
}
|
||||
@@ -206,75 +194,11 @@ impl DownloadTask {
|
||||
progress: RwLock::new(progress),
|
||||
});
|
||||
|
||||
tauri::async_runtime::spawn(task.clone().process(false));
|
||||
tauri::async_runtime::spawn(task.clone().process());
|
||||
|
||||
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(
|
||||
level = "error",
|
||||
skip_all,
|
||||
@@ -287,7 +211,6 @@ impl DownloadTask {
|
||||
ep_id = self.trace_fields.ep_id,
|
||||
collection_title = self.trace_fields.collection_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,
|
||||
part_title = self.trace_fields.part_title,
|
||||
part_order = self.trace_fields.part_order,
|
||||
@@ -295,14 +218,14 @@ impl DownloadTask {
|
||||
up_uid = self.trace_fields.up_uid,
|
||||
)
|
||||
)]
|
||||
async fn process(self: Arc<Self>, emit_create_events: bool) {
|
||||
if emit_create_events {
|
||||
let state = *self.state_sender.borrow();
|
||||
let progress = self.progress.read().clone();
|
||||
let _ = DownloadEvent::TaskCreate { state, progress }.emit(&self.app);
|
||||
}
|
||||
async fn process(self: Arc<Self>) {
|
||||
let state = *self.state_sender.borrow();
|
||||
let progress = self.progress.read().clone();
|
||||
let _ = DownloadEvent::TaskCreate { state, progress }.emit(&self.app);
|
||||
|
||||
let mut state_receiver = self.state_sender.subscribe();
|
||||
state_receiver.mark_changed();
|
||||
|
||||
let mut restart_receiver = self.restart_sender.subscribe();
|
||||
let mut cancel_receiver = self.cancel_sender.subscribe();
|
||||
let mut delete_receiver = self.delete_sender.subscribe();
|
||||
@@ -494,7 +417,6 @@ impl DownloadTask {
|
||||
ep_id = self.trace_fields.ep_id,
|
||||
collection_title = self.trace_fields.collection_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,
|
||||
part_title = self.trace_fields.part_title,
|
||||
part_order = self.trace_fields.part_order,
|
||||
@@ -502,7 +424,7 @@ impl DownloadTask {
|
||||
up_uid = self.trace_fields.up_uid,
|
||||
)
|
||||
)]
|
||||
fn set_state(&self, state: DownloadTaskState) {
|
||||
pub fn set_state(&self, state: DownloadTaskState) {
|
||||
if let Err(err) = self.state_sender.send(state).map_err(eyre::Report::from) {
|
||||
let err_title = format!("发送状态`{state:?}`失败");
|
||||
let message = err.to_message();
|
||||
@@ -534,18 +456,6 @@ impl DownloadTask {
|
||||
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 {
|
||||
@@ -557,7 +467,6 @@ pub struct DownloadTaskTraceFields {
|
||||
pub ep_id: Option<i64>,
|
||||
pub collection_title: String,
|
||||
pub episode_title: String,
|
||||
pub episode_title_in_collection: Option<String>,
|
||||
pub episode_order: i64,
|
||||
pub part_title: Option<String>,
|
||||
pub part_order: Option<i64>,
|
||||
@@ -576,7 +485,6 @@ impl From<&DownloadProgress> for DownloadTaskTraceFields {
|
||||
ep_id: progress.ep_id,
|
||||
collection_title: progress.collection_title.clone(),
|
||||
episode_title: progress.episode_title.clone(),
|
||||
episode_title_in_collection: progress.episode_title_in_collection.clone(),
|
||||
episode_order: progress.episode_order,
|
||||
part_title: progress.part_title.clone(),
|
||||
part_order: progress.part_order,
|
||||
|
||||
@@ -25,7 +25,6 @@ pub struct FmtParams {
|
||||
pub pub_ts: i64,
|
||||
pub collection_title: String,
|
||||
pub episode_title: String,
|
||||
pub episode_title_in_collection: Option<String>,
|
||||
pub episode_order: i64,
|
||||
pub part_title: Option<String>,
|
||||
pub part_order: Option<i64>,
|
||||
|
||||
@@ -67,7 +67,7 @@ impl DanmakuTask {
|
||||
let replies = bili_client
|
||||
.get_danmaku(progress.aid, progress.cid, progress.duration)
|
||||
.await
|
||||
.wrap_err("获取弹幕失败,请[继续]以重试。如果重试多次依旧失败,请在设置中取消勾选[下载弹幕]相关的所有选项")?;
|
||||
.wrap_err("获取弹幕失败")?;
|
||||
|
||||
let xml = replies
|
||||
.to_xml(progress.cid)
|
||||
|
||||
@@ -47,9 +47,10 @@ impl SubtitleTask {
|
||||
|
||||
for subtitle_detail in &player_info.subtitle.subtitles {
|
||||
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();
|
||||
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 serde::{Deserialize, Serialize};
|
||||
@@ -98,29 +98,54 @@ impl VideoProcessTask {
|
||||
|
||||
let output_path = episode_dir.join(format!("{filename}-merged.mp4"));
|
||||
|
||||
let mut command = std::process::Command::new(ffmpeg_program);
|
||||
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();
|
||||
|
||||
command.arg("-i").arg(&video_path);
|
||||
command.arg("-i").arg(&audio_path);
|
||||
if let Some(metadata_path) = &metadata_path {
|
||||
command.arg("-i").arg(metadata_path);
|
||||
command.arg("-map_metadata").arg("2");
|
||||
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);
|
||||
|
||||
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)
|
||||
.wrap_err(format!("删除视频文件`{}`失败", video_path.display()))?;
|
||||
std::fs::remove_file(&audio_path)
|
||||
@@ -167,24 +192,49 @@ impl VideoProcessTask {
|
||||
|
||||
let ffmpeg_program = utils::get_ffmpeg_program().wrap_err("获取FFmpeg程序路径失败")?;
|
||||
|
||||
let mut command = std::process::Command::new(ffmpeg_program);
|
||||
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();
|
||||
|
||||
command.arg("-i").arg(&video_path);
|
||||
command.arg("-i").arg(&audio_path);
|
||||
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 current_span = tracing::Span::current();
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
let _enter = current_span.enter();
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// 隐藏窗口
|
||||
use std::os::windows::process::CommandExt;
|
||||
command.creation_flags(0x0800_0000);
|
||||
let mut command = std::process::Command::new(ffmpeg_program);
|
||||
|
||||
command.arg("-i").arg(video_path_clone);
|
||||
command.arg("-i").arg(audio_path_clone);
|
||||
|
||||
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)
|
||||
.wrap_err(format!("删除视频文件`{}`失败", video_path.display()))?;
|
||||
std::fs::remove_file(&audio_path)
|
||||
@@ -229,23 +279,48 @@ impl VideoProcessTask {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let mut command = std::process::Command::new(ffmpeg_program);
|
||||
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();
|
||||
|
||||
command.arg("-i").arg(&video_path);
|
||||
command.arg("-i").arg(&metadata_path);
|
||||
command.arg("-map_metadata").arg("1");
|
||||
command.arg("-c").arg("copy");
|
||||
command.arg(&output_path).arg("-y");
|
||||
let current_span = tracing::Span::current();
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
let _enter = current_span.enter();
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// 隐藏窗口
|
||||
use std::os::windows::process::CommandExt;
|
||||
command.creation_flags(0x0800_0000);
|
||||
let mut command = std::process::Command::new(ffmpeg_program);
|
||||
|
||||
command.arg("-i").arg(video_path_clone);
|
||||
command.arg("-i").arg(metadata_path_clone);
|
||||
|
||||
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)
|
||||
.wrap_err(format!("删除视频文件`{}`失败", video_path.display()))?;
|
||||
std::fs::rename(&output_path, &video_path).wrap_err(format!(
|
||||
@@ -292,10 +367,7 @@ impl VideoProcessTask {
|
||||
let bili_client = app.get_bili_client();
|
||||
let cid = Some(progress.cid);
|
||||
|
||||
let skip_segments = bili_client
|
||||
.get_skip_segments(bvid, cid)
|
||||
.await
|
||||
.wrap_err("获取广告片段失败,请[继续]以重试。如果重试多次依旧失败,请在设置中取消勾选[标记广告]")?;
|
||||
let skip_segments = bili_client.get_skip_segments(bvid, cid).await?;
|
||||
for segment in skip_segments.0 {
|
||||
if let Some(chapter_segment) = segment.into_chapter_segment() {
|
||||
chapter_segments.insert(chapter_segment);
|
||||
@@ -316,32 +388,3 @@ impl VideoProcessTask {
|
||||
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()
|
||||
.plugin(tauri_plugin_window_state::Builder::default().build())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_os::init())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use serde::{Deserialize, Deserializer};
|
||||
|
||||
pub mod audio_quality;
|
||||
pub mod available_media_formats;
|
||||
pub mod bangumi_follow_info;
|
||||
@@ -38,12 +36,3 @@ pub mod user_info;
|
||||
pub mod user_video_info;
|
||||
pub mod video_quality;
|
||||
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())
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ pub struct PaidJump {
|
||||
pub struct Payment {
|
||||
pub bp_enough: i64,
|
||||
pub desc: String,
|
||||
pub my_bp: f64,
|
||||
pub my_bp: i64,
|
||||
pub pay_shade: String,
|
||||
pub price: f64,
|
||||
pub price_format: String,
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
|
||||
use super::deserialize_null_default;
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
@@ -32,7 +30,6 @@ pub struct PlayerInfo {
|
||||
pub online_count: i64,
|
||||
pub need_login_subtitle: bool,
|
||||
pub subtitle: SubtitleInPlayerInfo,
|
||||
#[serde(default, deserialize_with = "deserialize_null_default")]
|
||||
pub view_points: Vec<ViewPoint>,
|
||||
pub preview_toast: String,
|
||||
pub options: Options,
|
||||
|
||||
@@ -41,8 +41,6 @@ pub enum VideoQuality {
|
||||
Video1080P60 = 116,
|
||||
#[serde(rename = "4K")]
|
||||
Video4K = 120,
|
||||
#[serde(rename = "SDR")]
|
||||
VideoSDR = 122,
|
||||
#[serde(rename = "HDR")]
|
||||
VideoHDR = 125,
|
||||
#[serde(rename = "Dolby")]
|
||||
|
||||
@@ -8,10 +8,9 @@
|
||||
"height": 600,
|
||||
"minWidth": 800,
|
||||
"minHeight": 600,
|
||||
"visible": false,
|
||||
"decorations": false,
|
||||
"theme": "Light"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,10 @@
|
||||
"height": 600,
|
||||
"minWidth": 800,
|
||||
"minHeight": 600,
|
||||
"visible": false,
|
||||
"hiddenTitle": true,
|
||||
"titleBarStyle": "Overlay",
|
||||
"theme": "Light"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,10 @@
|
||||
"height": 600,
|
||||
"minWidth": 800,
|
||||
"minHeight": 600,
|
||||
"visible": false,
|
||||
"decorations": false,
|
||||
"shadow": true,
|
||||
"theme": "Light"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-4
@@ -125,7 +125,7 @@ async restartDownloadTasks(taskIds: string[]) : Promise<void> {
|
||||
async restartDownloadTask(params: RestartDownloadTaskParams) : Promise<void> {
|
||||
await TAURI_INVOKE("restart_download_task", { params });
|
||||
},
|
||||
async restoreDownloadTasks() : Promise<Result<RestoredDownloadTask[], CommandError>> {
|
||||
async restoreDownloadTasks() : Promise<Result<null, CommandError>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("restore_download_tasks") };
|
||||
} catch (e) {
|
||||
@@ -329,7 +329,7 @@ export type Dimension = { width: number; height: number; rotate: number }
|
||||
export type DimensionInBangumi = { height: number; rotate: number; width: number }
|
||||
export type DimensionInWatchLater = { width: number; height: number; rotate: number }
|
||||
export type DownloadEvent = { event: "Speed"; data: { speed: string } } | { event: "TaskCreate"; data: { state: DownloadTaskState; progress: DownloadProgress } } | { event: "TaskStateUpdate"; data: { task_id: string; state: DownloadTaskState } } | { event: "TaskSleeping"; data: { task_id: string; remaining_sec: number } } | { event: "TaskDelete"; data: { task_id: string } } | { event: "ProgressPreparing"; data: { task_id: string } } | { event: "ProgressUpdate"; data: { progress: DownloadProgress } }
|
||||
export type DownloadProgress = { task_id: string; episode_type: EpisodeType; aid: number; bvid: string | null; cid: number; ep_id: number | null; duration: number; pub_ts: number; collection_title: string; part_title: string | null; part_order: number | null; episode_title: string; episode_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 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 DownloadTaskState = "Pending" | "Downloading" | "Paused" | "Completed" | "Failed"
|
||||
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 }
|
||||
@@ -429,7 +429,6 @@ export type RatingInBangumi = { count: number; score: 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 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 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 }
|
||||
@@ -475,7 +474,7 @@ export type UserVideoInfo = { list: UserVideoList; page: PageInUserVideo }
|
||||
export type UserVideoList = { vlist: EpInUserVideo[] }
|
||||
export type UserVideoSearchResult = UserVideoInfo
|
||||
export type VideoProcessTask = { merge_selected: boolean; embed_chapter_selected: boolean; embed_skip_selected: boolean; completed: boolean; skipped: boolean }
|
||||
export type VideoQuality = "Unknown" | "240P" | "360P" | "480P" | "720P" | "720P60" | "1080P" | "AiRepair" | "1080P+" | "1080P60" | "4K" | "SDR" | "HDR" | "Dolby" | "8K"
|
||||
export type VideoQuality = "Unknown" | "240P" | "360P" | "480P" | "720P" | "720P60" | "1080P" | "AiRepair" | "1080P+" | "1080P60" | "4K" | "HDR" | "Dolby" | "8K"
|
||||
export type VideoQualityAndCodecType = { video_quality: VideoQuality; codec_type: CodecType }
|
||||
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 }
|
||||
|
||||
@@ -56,17 +56,13 @@ function AvailableFmtFields() {
|
||||
<div>
|
||||
<span class="rounded bg-gray-500 px-1 select-all">collection_title</span>
|
||||
<span class="ml-2">
|
||||
合集标题,
|
||||
<span class="text-blue">对于单独(没有合集)的视频,合集标题就是视频标题</span>
|
||||
合集名称,
|
||||
<span class="text-blue">对于单独(没有合集)的视频,合集名称就是视频名称</span>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="rounded bg-gray-500 px-1 select-all">episode_title</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>
|
||||
<span class="ml-2">视频名称</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="rounded bg-gray-500 px-1 select-all">episode_order</span>
|
||||
@@ -87,7 +83,7 @@ function AvailableFmtFields() {
|
||||
</div>
|
||||
<div>
|
||||
<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>
|
||||
<span class="rounded bg-gray-500 px-1 select-all">part_order</span>
|
||||
|
||||
@@ -33,7 +33,7 @@ onMounted(async () => {
|
||||
...progress,
|
||||
state,
|
||||
percentage: 0,
|
||||
stateIndicator: getStateIndicator(state),
|
||||
stateIndicator: '',
|
||||
taskIndicator: '',
|
||||
}
|
||||
store.updateProgresses((progresses) => {
|
||||
@@ -48,8 +48,21 @@ onMounted(async () => {
|
||||
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.stateIndicator = getStateIndicator(state)
|
||||
progressData.stateIndicator = stateIndicator
|
||||
})
|
||||
} else if (event === 'TaskSleeping') {
|
||||
const { task_id, remaining_sec } = data
|
||||
@@ -138,41 +151,8 @@ onMounted(async () => {
|
||||
const result = await commands.restoreDownloadTasks()
|
||||
if (result.status === '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>
|
||||
|
||||
<template>
|
||||
|
||||
+18
-10
@@ -27,28 +27,36 @@ export const useStore = defineStore('store', () => {
|
||||
})
|
||||
|
||||
function useProgresses() {
|
||||
// 内部的高频更新状态
|
||||
const _progresses = new Map<string, ProgressData>()
|
||||
// 对外暴露的响应式状态
|
||||
const progresses = ref<Map<string, ProgressData>>(new Map())
|
||||
|
||||
// 等待在同一渲染帧内执行的更新函数
|
||||
const pendingUpdateFns: Array<(progresses: Map<string, ProgressData>) => void> = []
|
||||
|
||||
// 用于确保在同一渲染帧内只安排一次UI更新
|
||||
let isUpdateScheduled = false
|
||||
|
||||
// 在同一渲染帧内集中执行等待中的更新函数
|
||||
// 将 `_progresses` 的内容更新到 `progresses` 中,并触发重新渲染
|
||||
const updateProgressesOnFrame = () => {
|
||||
isUpdateScheduled = false
|
||||
const newProgressesMap = new Map<string, ProgressData>()
|
||||
|
||||
const updateFns = pendingUpdateFns.splice(0)
|
||||
for (const updateFn of updateFns) {
|
||||
updateFn(progresses.value)
|
||||
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
|
||||
}
|
||||
|
||||
const updateProgresses = (updateFn: (progresses: Map<string, ProgressData>) => void) => {
|
||||
// 将传入的更新函数添加到等待队列
|
||||
pendingUpdateFns.push(updateFn)
|
||||
// 使用传入的更新函数来修改 `_progresses`
|
||||
updateFn(_progresses)
|
||||
|
||||
if (!isUpdateScheduled) {
|
||||
// 如果没有安排过UI更新,则安排一次
|
||||
|
||||
@@ -249,7 +249,6 @@ const videoQualityNameMap: Record<VideoQuality, string> = {
|
||||
'1080P+': '1080P 高码率',
|
||||
'1080P60': '1080P 60帧',
|
||||
'4K': '4K 超高清',
|
||||
SDR: '4K SDR增强',
|
||||
HDR: 'HDR 真彩色',
|
||||
Dolby: '杜比视界',
|
||||
'8K': '8K 超高清',
|
||||
|
||||
Reference in New Issue
Block a user