滚动截图,细节调整
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
@@ -115,8 +115,8 @@ struct EngineInner {
|
||||
http: HttpDownloader,
|
||||
/// BitTorrent 下载器(封装 librqbit 全局会话)
|
||||
torrent: TorrentDownloader,
|
||||
/// 下载是否使用代理(true=尊重系统代理,false=强制直连)
|
||||
/// 独立于 settings 存储,避免下载过程中反复锁 settings;save_settings 时同步更新
|
||||
/// 下载是否使用代理(true=尝试走本地 mihomo 代理(mihomo 运行时才走),false=强制直连)。
|
||||
/// 不再依赖系统代理。独立于 settings 存储,避免下载过程中反复锁 settings;save_settings 时同步更新
|
||||
use_proxy: AtomicBool,
|
||||
/// Tauri 应用句柄(用于发事件)
|
||||
app_handle: AppHandle,
|
||||
@@ -126,6 +126,11 @@ struct EngineInner {
|
||||
next_gen: AtomicU64,
|
||||
/// 持久化节流:上次保存时间
|
||||
last_save: Mutex<Instant>,
|
||||
/// mihomo 代理地址解析缓存(含解析时刻),避免每个任务反复探测控制端
|
||||
mihomo_proxy_cache: Mutex<Option<(Instant, Option<String>)>>,
|
||||
/// HTTP 任务元数据后台探测中(占位任务已建但尚未完成探测/分段):
|
||||
/// 期间 schedule 跳过此类任务,防止用"未知大小/单段"提前启动;探测完成后移除并重新调度
|
||||
http_pending: Mutex<HashSet<String>>,
|
||||
}
|
||||
|
||||
impl DownloadEngine {
|
||||
@@ -161,7 +166,7 @@ impl DownloadEngine {
|
||||
let use_proxy = settings.use_proxy;
|
||||
// BT 会话默认目录(下载目录,每个任务用 output_folder 覆盖)
|
||||
// 解析 BT 代理地址(若开启且代理模块可用)
|
||||
let bt_socks = bt_proxy_addr(&app_handle, settings.bt_use_proxy);
|
||||
let bt_socks = bt_proxy_addr(&app_handle, settings.use_proxy);
|
||||
let torrent = TorrentDownloader::new(PathBuf::from(&settings.download_dir));
|
||||
// 同步 BT 专属设置(上传限速 / 监听端口 / 代理)
|
||||
torrent.set_settings(settings.bt_upload_limit_kb, settings.bt_listen_port, bt_socks);
|
||||
@@ -180,6 +185,8 @@ impl DownloadEngine {
|
||||
started: AtomicBool::new(false),
|
||||
next_gen: AtomicU64::new(0),
|
||||
last_save: Mutex::new(Instant::now()),
|
||||
mihomo_proxy_cache: Mutex::new(None),
|
||||
http_pending: Mutex::new(HashSet::new()),
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -227,6 +234,7 @@ impl DownloadEngine {
|
||||
}
|
||||
|
||||
let use_proxy = self.inner.use_proxy.load(Ordering::SeqCst);
|
||||
self.ensure_mihomo_proxy().await;
|
||||
let probe = self.inner.http.probe(url, headers, use_proxy).await;
|
||||
let settings = self.inner.settings.lock().unwrap_or_else(|e| e.into_inner()).clone();
|
||||
let task_dir = dir.map(|d| d.to_string()).unwrap_or_else(|| settings.download_dir.clone());
|
||||
@@ -439,50 +447,50 @@ impl DownloadEngine {
|
||||
return self.add_bt_task(url, dir, only_files).await;
|
||||
}
|
||||
|
||||
// 探测资源信息
|
||||
let use_proxy = self.inner.use_proxy.load(Ordering::SeqCst);
|
||||
let probe = self.inner.http.probe(&url, &headers, use_proxy).await;
|
||||
|
||||
// HTTP 任务采用"立即占位 + 后台探测":
|
||||
// 点击添加后立即返回 id 并让任务出现在列表,探测/分段/调度全部放后台,
|
||||
// 避免添加操作阻塞在慢速探测(如 mihomo 未就绪/网络不通)上,也防止用户因"没反应"多次点击。
|
||||
let settings = self.inner.settings.lock().unwrap_or_else(|e| e.into_inner()).clone();
|
||||
let task_dir = dir.unwrap_or_else(|| settings.download_dir.clone());
|
||||
let task_dir = dir.clone().unwrap_or_else(|| settings.download_dir.clone());
|
||||
|
||||
// 确定文件名
|
||||
let mut task_filename = filename
|
||||
.or_else(|| probe.as_ref().ok().and_then(|p| p.filename.clone()))
|
||||
.unwrap_or_else(|| {
|
||||
url.split('?')
|
||||
.next()
|
||||
.and_then(|u| u.rsplit('/').next())
|
||||
.filter(|n| !n.is_empty())
|
||||
.map(|n| n.to_string())
|
||||
.unwrap_or_else(|| format!("download_{}", chrono::Utc::now().timestamp()))
|
||||
});
|
||||
|
||||
// 自动重命名:若磁盘已存在同名文件,追加 (1)、(2)...
|
||||
if auto_rename {
|
||||
task_filename = self.generate_unique_filename(&task_dir, &task_filename);
|
||||
// 1) 后端 URL 去重:同 URL 且非终态任务已存在 → 直接复用既有 id,不重复创建。
|
||||
// 接住多次点击/重复转发造成的重复添加。
|
||||
if settings.check_duplicate {
|
||||
let dup_id = {
|
||||
let tasks = self.inner.tasks.lock().unwrap_or_else(|e| e.into_inner());
|
||||
tasks
|
||||
.values()
|
||||
.find(|t| {
|
||||
t.protocol == TaskProtocol::Http
|
||||
&& t.url == url
|
||||
&& matches!(
|
||||
t.status,
|
||||
TaskStatus::Queued | TaskStatus::Active | TaskStatus::Paused
|
||||
)
|
||||
})
|
||||
.map(|t| t.id.clone())
|
||||
};
|
||||
if let Some(id) = dup_id {
|
||||
return Ok(id);
|
||||
}
|
||||
}
|
||||
|
||||
let id = self.inner.storage.next_task_id();
|
||||
|
||||
// 创建分段(受 continue_download 设置控制:关闭时强制单线程、不支持续传)
|
||||
let resume_enabled = settings.continue_download;
|
||||
let segments = match &probe {
|
||||
Ok(p) if resume_enabled && p.supports_resume && p.total_size.map(|s| s > 0).unwrap_or(false) => {
|
||||
split_segments(p.total_size.unwrap(), settings.max_connections)
|
||||
}
|
||||
_ => vec![Segment {
|
||||
index: 0,
|
||||
start: 0,
|
||||
end: 0,
|
||||
completed: 0,
|
||||
}],
|
||||
};
|
||||
|
||||
let total_size = probe.as_ref().ok().and_then(|p| p.total_size).unwrap_or(0);
|
||||
let supports_resume = resume_enabled
|
||||
&& probe.as_ref().ok().map(|p| p.supports_resume).unwrap_or(false);
|
||||
// 2) 占位文件名:优先用调用方指定,否则先用 URL 推导,探测完成后用 Content-Disposition 修正
|
||||
let mut task_filename = filename.clone().unwrap_or_else(|| {
|
||||
url.split('?')
|
||||
.next()
|
||||
.and_then(|u| u.rsplit('/').next())
|
||||
.filter(|n| !n.is_empty())
|
||||
.map(|n| n.to_string())
|
||||
.unwrap_or_else(|| format!("download_{}", chrono::Utc::now().timestamp()))
|
||||
});
|
||||
if auto_rename {
|
||||
task_filename = self.generate_unique_filename(&task_dir, &task_filename);
|
||||
}
|
||||
|
||||
// 3) 立即插入占位任务:Queued(未探测完成前 schedule 会跳过),未知大小单段
|
||||
let task = DownloadTask {
|
||||
id: id.clone(),
|
||||
url: url.clone(),
|
||||
@@ -493,47 +501,128 @@ impl DownloadEngine {
|
||||
bt_files: Vec::new(),
|
||||
bt_metadata_ready: false,
|
||||
status: TaskStatus::Queued,
|
||||
total_size,
|
||||
total_size: 0,
|
||||
completed_size: 0,
|
||||
speed: 0,
|
||||
supports_resume,
|
||||
segments: segments.clone(),
|
||||
supports_resume: false,
|
||||
segments: vec![Segment {
|
||||
index: 0,
|
||||
start: 0,
|
||||
end: 0,
|
||||
completed: 0,
|
||||
}],
|
||||
error: None,
|
||||
created_at: chrono::Utc::now().timestamp_millis(),
|
||||
headers,
|
||||
headers: headers.clone(),
|
||||
};
|
||||
|
||||
// 如果探测失败,标记为 Error
|
||||
let task = match probe {
|
||||
Ok(_) => task,
|
||||
Err(e) => DownloadTask {
|
||||
status: TaskStatus::Error,
|
||||
error: Some(e),
|
||||
..task
|
||||
},
|
||||
};
|
||||
|
||||
{
|
||||
let mut tasks = self.inner.tasks.lock().unwrap_or_else(|e| e.into_inner());
|
||||
tasks.insert(id.clone(), task);
|
||||
}
|
||||
// 标记该任务元数据探测中,避免被并发调度提前启动
|
||||
self.inner
|
||||
.http_pending
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.insert(id.clone());
|
||||
self.persist_now();
|
||||
self.emit_task_added(&id);
|
||||
|
||||
// 如果探测成功,尝试调度
|
||||
let should_schedule = {
|
||||
let tasks = self.inner.tasks.lock().unwrap_or_else(|e| e.into_inner());
|
||||
tasks.get(&id).map(|t| t.status == TaskStatus::Queued).unwrap_or(false)
|
||||
};
|
||||
if should_schedule {
|
||||
self.schedule();
|
||||
}
|
||||
// 4) 后台探测/分段/调度:不阻塞本函数,点击添加后立即返回
|
||||
let engine = self.clone();
|
||||
let rid = id.clone();
|
||||
let raw_url = url.clone();
|
||||
let raw_headers = headers.clone();
|
||||
let use_proxy = self.inner.use_proxy.load(Ordering::SeqCst);
|
||||
tauri::async_runtime::spawn(async move {
|
||||
engine.ensure_mihomo_proxy().await;
|
||||
let probe = engine.inner.http.probe(&raw_url, &raw_headers, use_proxy).await;
|
||||
|
||||
// 通知前端有新任务加入(扩展通过 HTTP API 添加时,前端需要刷新)
|
||||
let _ = self.inner.app_handle.emit(crate::constants::events::DOWNLOAD_ADDED, serde_json::json!({ "id": id }));
|
||||
// 探测结束,解除 pending 门控
|
||||
engine
|
||||
.inner
|
||||
.http_pending
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.remove(&rid);
|
||||
|
||||
let mut should_schedule = false;
|
||||
// 先在锁内应用非文件名字段,并取出目录/探测文件名(释放锁后再做唯一化命名,
|
||||
// 因为 generate_unique_filename 内部会再锁 tasks,持锁调用会死锁)
|
||||
let rename_from: Option<(String, Option<String>)> = {
|
||||
let mut tasks = engine.inner.tasks.lock().unwrap_or_else(|e| e.into_inner());
|
||||
match tasks.get_mut(&rid) {
|
||||
Some(t) => match &probe {
|
||||
Ok(p) => {
|
||||
t.total_size = p.total_size.unwrap_or(0);
|
||||
let resume = settings.continue_download
|
||||
&& p.supports_resume
|
||||
&& p.total_size.map(|s| s > 0).unwrap_or(false);
|
||||
t.supports_resume = resume;
|
||||
t.segments = if resume {
|
||||
split_segments(p.total_size.unwrap(), settings.max_connections)
|
||||
} else {
|
||||
vec![Segment {
|
||||
index: 0,
|
||||
start: 0,
|
||||
end: p.total_size.map(|s| s.saturating_sub(1)).unwrap_or(0),
|
||||
completed: 0,
|
||||
}]
|
||||
};
|
||||
if t.status == TaskStatus::Queued {
|
||||
should_schedule = true;
|
||||
}
|
||||
// 调用方未指定文件名时,才后续用 Content-Disposition 修正
|
||||
if filename.is_none() {
|
||||
Some((t.dir.clone(), p.filename.clone()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
if t.status == TaskStatus::Queued {
|
||||
t.status = TaskStatus::Error;
|
||||
t.error = Some(e.clone());
|
||||
}
|
||||
None
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
}
|
||||
};
|
||||
|
||||
// 释放 tasks 锁后,若探测返回了真实文件名,重新做唯一化并写回
|
||||
if let Some((dir_owned, Some(real_name))) = rename_from {
|
||||
let final_name = if auto_rename {
|
||||
engine.generate_unique_filename(&dir_owned, &real_name)
|
||||
} else {
|
||||
real_name
|
||||
};
|
||||
let mut tasks = engine.inner.tasks.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if let Some(t) = tasks.get_mut(&rid) {
|
||||
t.filename = final_name;
|
||||
}
|
||||
}
|
||||
|
||||
engine.persist_now();
|
||||
// 探测完成:刷新大小/文件名/分段/错误态到前端
|
||||
engine.emit_task_added(&rid);
|
||||
if should_schedule {
|
||||
engine.schedule();
|
||||
}
|
||||
});
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// 通知前端任务已新增/元数据已更新(用于刷新任务列表)
|
||||
fn emit_task_added(&self, id: &str) {
|
||||
let _ = self.inner.app_handle.emit(
|
||||
crate::constants::events::DOWNLOAD_ADDED,
|
||||
serde_json::json!({ "id": id }),
|
||||
);
|
||||
}
|
||||
|
||||
/// 添加 BitTorrent 任务(磁力链 / 本地 .torrent)— 全异步添加。
|
||||
/// 立即创建占位任务并**立即返回** id(不阻塞);加入会话与元数据解析全在后台进行。
|
||||
/// 注意:librqbit 的 add_torrent 对磁力会在元数据就绪前阻塞返回,因此绝不能在此 await。
|
||||
@@ -1157,8 +1246,9 @@ impl DownloadEngine {
|
||||
.use_proxy
|
||||
.store(settings.use_proxy, Ordering::SeqCst);
|
||||
|
||||
// 同步 BT 专属设置(上传限速 / 监听端口 / 代理;监听端口、代理首次创建会话时生效)
|
||||
let bt_socks = bt_proxy_addr(&self.inner.app_handle, settings.bt_use_proxy);
|
||||
// 同步 BT 专属设置(上传限速 / 监听端口 / 代理;监听端口、代理首次创建会话时生效)。
|
||||
// BT 是否走代理跟随下载设置 use_proxy(bt_use_proxy 不再独立控制)
|
||||
let bt_socks = bt_proxy_addr(&self.inner.app_handle, settings.use_proxy);
|
||||
self.inner
|
||||
.torrent
|
||||
.set_settings(settings.bt_upload_limit_kb, settings.bt_listen_port, bt_socks);
|
||||
@@ -1175,6 +1265,68 @@ impl DownloadEngine {
|
||||
self.inner.started.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
/// 读取代理模块(mihomo)配置:mixed_port 与 external_controller(settings.json)。
|
||||
/// 无配置/端口非法返回 None。
|
||||
fn mihomo_proxy_config(&self) -> Option<(u16, String)> {
|
||||
let dir = self.inner.app_handle.path().app_data_dir().ok()?;
|
||||
let p = dir.join("proxy").join("settings.json");
|
||||
let s = std::fs::read_to_string(p).ok()?;
|
||||
let v: serde_json::Value = serde_json::from_str(&s).ok()?;
|
||||
let port = v.get("mixedPort").and_then(|x| x.as_u64())?;
|
||||
if port == 0 || port > 65535 {
|
||||
return None;
|
||||
}
|
||||
let controller = v
|
||||
.get("externalController")
|
||||
.and_then(|x| x.as_str())
|
||||
.unwrap_or("127.0.0.1:9090")
|
||||
.to_string();
|
||||
Some((port as u16, controller))
|
||||
}
|
||||
|
||||
/// 解析当前 mihomo 显式代理地址(带 TTL 缓存):
|
||||
/// 代理开关开启 & mihomo 控制端在线 → `http://127.0.0.1:{mixed_port}`;
|
||||
/// 否则 → None(直连)。不再依赖系统代理。
|
||||
async fn mihomo_proxy(&self) -> Option<String> {
|
||||
// 先查缓存(短锁,不跨 await 持有)
|
||||
{
|
||||
let cache = self
|
||||
.inner
|
||||
.mihomo_proxy_cache
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
if let Some((at, addr)) = cache.as_ref() {
|
||||
if at.elapsed() < Duration::from_secs(5) {
|
||||
return addr.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 重新解析:读配置端口 + 探测控制端确认 mihomo 运行
|
||||
let addr = match self.mihomo_proxy_config() {
|
||||
Some((port, controller)) => {
|
||||
if self.inner.http.probe_controller(&controller).await {
|
||||
Some(format!("http://127.0.0.1:{}", port))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
if let Ok(mut cache) = self.inner.mihomo_proxy_cache.lock() {
|
||||
*cache = Some((Instant::now(), addr.clone()));
|
||||
}
|
||||
addr
|
||||
}
|
||||
|
||||
/// 下载/探测前,把解析出的 mihomo 代理地址应用到 HTTP 下载器。
|
||||
/// 代理开关关闭时强制直连。
|
||||
async fn ensure_mihomo_proxy(&self) {
|
||||
let use_proxy = self.inner.use_proxy.load(Ordering::SeqCst);
|
||||
let proxy = if use_proxy { self.mihomo_proxy().await } else { None };
|
||||
self.inner.http.configure_mihomo_proxy(proxy);
|
||||
}
|
||||
|
||||
/// 退出时清理:停止所有下载、保存状态
|
||||
pub fn cleanup_on_exit(&self) {
|
||||
// 取消所有活跃下载(HTTP 设 cancel 标志;BT 中止等待 future)
|
||||
@@ -1219,13 +1371,20 @@ impl DownloadEngine {
|
||||
|
||||
let (active_count, queued_ids) = {
|
||||
let tasks = self.inner.tasks.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let pending = self.inner.http_pending.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let active = tasks.values().filter(|t| t.status == TaskStatus::Active).count();
|
||||
let mut queued: Vec<_> = tasks
|
||||
.values()
|
||||
// 跳过元数据未就绪的 BT 任务(异步添加后台解析中,等待用户勾选文件)
|
||||
// 及 HTTP 占位任务(后台探测未完成,未得到大小/分段前不得提前启动)
|
||||
.filter(|t| {
|
||||
t.status == TaskStatus::Queued
|
||||
&& (t.protocol != TaskProtocol::BitTorrent || t.bt_metadata_ready)
|
||||
if t.status != TaskStatus::Queued {
|
||||
return false;
|
||||
}
|
||||
if t.protocol == TaskProtocol::BitTorrent {
|
||||
return t.bt_metadata_ready;
|
||||
}
|
||||
!pending.contains(&t.id)
|
||||
})
|
||||
.collect();
|
||||
queued.sort_by_key(|t| t.created_at);
|
||||
@@ -1473,6 +1632,8 @@ impl DownloadEngine {
|
||||
|
||||
let join = tauri::async_runtime::spawn(async move {
|
||||
let my_gen = gen;
|
||||
// 下载前应用当前 mihomo 代理(engine 与 http 共享同一 mihomo 客户端状态)
|
||||
engine.ensure_mihomo_proxy().await;
|
||||
let result = http
|
||||
.download(&url, &headers, &segments, &temp_file_path, cancel_clone, &progress_clone, limiter, use_proxy)
|
||||
.await;
|
||||
|
||||
Reference in New Issue
Block a user