use serde::{Deserialize, Serialize}; use specta::Type; use std::collections::HashMap; /// 任务下载协议类型 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Type, Default)] #[serde(rename_all = "lowercase")] pub enum TaskProtocol { /// HTTP/HTTPS 直链 #[default] Http, /// BitTorrent(磁力链 / .torrent 文件) BitTorrent, } /// 任务状态 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Type)] #[serde(rename_all = "lowercase")] pub enum TaskStatus { /// 排队等待(并发数已满) Queued, /// 下载中 Active, /// 已暂停 Paused, /// 已完成 Complete, /// 错误 Error, /// 已取消(用户取消:进度与文件已清除,仅保留记录,只能再次下载) Cancelled, } /// 下载分段(多线程 Range 下载 / 断点续传用) #[derive(Debug, Clone, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub struct Segment { /// 分段索引 pub index: u32, /// 起始字节(含) pub start: u64, /// 结束字节(含) pub end: u64, /// 已下载字节 pub completed: u64, } impl Segment { /// 该分段总长度 pub fn len(&self) -> u64 { self.end.saturating_sub(self.start) + 1 } /// 是否为未知大小哨兵段(start=0, end=0,表示不支持 Range 或未探测到大小) pub fn is_unknown_size(&self) -> bool { self.start == 0 && self.end == 0 } /// 是否已完成 pub fn is_done(&self) -> bool { // 未知大小段无法用长度判断是否完成,由流结束(Ok(None))判定; // 若按 len()=1 判断,暂停/恢复后 completed>=1 会误判为已完成,导致文件被截断 if self.is_unknown_size() { return false; } self.completed >= self.len() } } /// BT 种子内文件条目(多文件任务用;阶段1下载全部文件,但保留列表供 UI 展示) #[derive(Debug, Clone, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub struct BtFileInfo { /// 文件在种子内的索引 pub index: u32, /// 相对种子根目录的路径(如 "sub/file.mkv") pub path: String, /// 文件大小(字节) pub size: u64, } /// 下载任务 #[derive(Debug, Clone, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub struct DownloadTask { /// 任务 ID(自增 hex 字符串) pub id: String, /// 下载地址(HTTP URL 或磁力链接) pub url: String, /// 文件名(HTTP:目标文件名;BT:种子名称) pub filename: String, /// 保存目录(绝对路径) pub dir: String, /// 协议类型 #[serde(default)] pub protocol: TaskProtocol, /// BT 种子 infohash(协议=BitTorrent 时存在) #[serde(default)] pub info_hash: Option, /// BT 种子内文件列表(协议=BitTorrent 时存在) #[serde(default)] pub bt_files: Vec, /// BT 元数据是否已解析就绪(异步添加时:后台解析完成前为 false,调度器跳过) #[serde(default)] pub bt_metadata_ready: bool, /// 状态 pub status: TaskStatus, /// 文件总大小(字节),0=未知 pub total_size: u64, /// 已下载字节 pub completed_size: u64, /// 当前下载速度 bytes/s pub speed: u64, /// 服务器是否支持断点续传 pub supports_resume: bool, /// 分段信息 #[serde(default)] pub segments: Vec, /// 错误信息 #[serde(default)] pub error: Option, /// 创建时间(Unix 时间戳,毫秒) pub created_at: i64, /// 自定义请求头(Cookie / Referer 等) #[serde(default)] pub headers: HashMap, } impl DownloadTask { /// 文件完整路径(最终文件名) pub fn file_path(&self) -> std::path::PathBuf { std::path::PathBuf::from(&self.dir).join(&self.filename) } /// 下载临时文件路径(下载未完成时使用,完成后重命名为 file_path) pub fn temp_file_path(&self) -> std::path::PathBuf { std::path::PathBuf::from(&self.dir).join(format!("{}.thingdl", self.filename)) } /// 更新已下载总量(聚合所有分段) pub fn recalc_completed(&mut self) { if self.segments.is_empty() { return; } self.completed_size = self.segments.iter().map(|s| s.completed).sum(); } } /// 下载设置 #[derive(Debug, Clone, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub struct DownloaderSettings { /// 下载目录 #[serde(default = "default_download_dir")] pub download_dir: String, /// 最大同时下载数 #[serde(default = "default_max_concurrent")] pub max_concurrent: u32, /// 单任务最大连接数(多线程分段数) #[serde(default = "default_max_connections")] pub max_connections: u32, /// 断点续传 #[serde(default = "default_true")] pub continue_download: bool, /// 全局速度限制 KB/s(0=不限) #[serde(default)] pub global_speed_limit: u64, /// 扩展 HTTP API 端口 #[serde(default = "default_extension_port")] pub extension_port: u16, /// 扩展认证密钥(空=不认证) #[serde(default)] pub extension_secret: String, /// 删除任务时是否同时删除已下载的文件 #[serde(default)] pub delete_files_on_remove: bool, /// 添加下载前检查重复(URL 或文件名重复时询问) #[serde(default = "default_true")] pub check_duplicate: bool, /// 下载是否使用代理:true=尊重系统代理(mihomo 开启系统代理时经其转发),false=强制直连 #[serde(default = "default_true")] pub use_proxy: bool, /// BitTorrent 上传限速 KB/s(0=不限) #[serde(default)] pub bt_upload_limit_kb: u64, /// BitTorrent 下载完成后是否继续做种上传(false=下载完即停止上传) #[serde(default)] pub bt_seed_after_download: bool, /// BitTorrent 监听端口(0=自动选择) #[serde(default)] pub bt_listen_port: u16, /// BitTorrent 使用代理下载:开启后自动使用代理模块(mihomo)的 SOCKS5 端口;代理不可用时降级直连 #[serde(default)] pub bt_use_proxy: bool, } fn default_max_concurrent() -> u32 { 5 } fn default_max_connections() -> u32 { 8 } fn default_true() -> bool { true } fn default_extension_port() -> u16 { 16800 } fn default_download_dir() -> String { dirs::download_dir() .or_else(|| { std::env::var("USERPROFILE") .ok() .map(|p| std::path::PathBuf::from(p).join("Downloads")) }) .map(|p| p.to_string_lossy().to_string()) .unwrap_or_else(|| "downloads".to_string()) } impl Default for DownloaderSettings { fn default() -> Self { Self { download_dir: default_download_dir(), max_concurrent: default_max_concurrent(), max_connections: default_max_connections(), continue_download: true, global_speed_limit: 0, extension_port: default_extension_port(), extension_secret: String::new(), delete_files_on_remove: false, check_duplicate: true, use_proxy: true, bt_upload_limit_kb: 0, bt_seed_after_download: false, bt_listen_port: 0, bt_use_proxy: false, } } } /// HEAD/Range 探测结果 #[derive(Debug, Clone)] pub struct ProbeResult { /// 文件大小(字节),None=未知 pub total_size: Option, /// 是否支持 Range 请求 pub supports_resume: bool, /// 从 Content-Disposition 或 URL 推断的文件名 pub filename: Option, }