Skip to content

Commit 2a6d186

Browse files
Merge pull request #823 from imsyy/dev-font
refactor(download): 重构下载进度跟踪和多线程下载逻辑 - 提取 ProgressTracker 结构体统一管理进度更新,避免重复代码 - 简化多线程下载实现,使用 futures_util 替代手动任务管理 - 改进文件大小探测逻辑,增强 Range 请求处理 - 优化元数据写入函数,提取标签获取逻辑
2 parents 05a6c31 + 3bf0bcb commit 2a6d186

30 files changed

Lines changed: 856 additions & 1104 deletions

File tree

Cargo.lock

Lines changed: 241 additions & 143 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

electron/main/ipc/ipc-file.ts

Lines changed: 65 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,16 @@ import { loadNativeModule } from "../utils/native-loader";
1616
type toolModule = typeof import("@native/tools");
1717
const tools: toolModule = loadNativeModule("tools.node", "tools");
1818

19-
interface DownloadProgress {
20-
percent: number;
21-
transferredBytes: number;
22-
totalBytes: number;
23-
}
24-
2519
/**
2620
* 文件相关 IPC
2721
*/
2822
const initFileIpc = (): void => {
2923
/** 本地音乐服务 */
3024
const localMusicService = new LocalMusicService();
3125

26+
// Store active download tasks: ID -> DownloadTask instance
27+
const activeDownloads = new Map<number, any>();
28+
3229
/**
3330
* 获取全局搜索配置
3431
* @param cwd 当前工作目录
@@ -510,6 +507,7 @@ const initFileIpc = (): void => {
510507
skipIfExist?: boolean;
511508
threadCount?: number;
512509
referer?: string;
510+
enableDownloadHttp2?: boolean;
513511
} = {
514512
fileName: "未知文件名",
515513
fileType: "mp3",
@@ -533,6 +531,7 @@ const initFileIpc = (): void => {
533531
songData,
534532
skipIfExist,
535533
referer,
534+
enableDownloadHttp2,
536535
} = options;
537536
// 规范化路径
538537
const downloadPath = resolve(path);
@@ -593,33 +592,46 @@ const initFileIpc = (): void => {
593592
}
594593

595594
const onProgress = (...args: any[]) => {
596-
// console.log("Received progress args:", args);
597-
let progressJson: string | undefined;
595+
let progressData: any;
598596

599597
// Handle (err, value) or (value) signature
600-
if (args.length > 1 && args[0] === null && typeof args[1] === "string") {
601-
progressJson = args[1];
602-
} else if (args.length > 0 && typeof args[0] === "string") {
603-
progressJson = args[0];
598+
if (args.length > 1 && args[0] === null) {
599+
progressData = args[1];
600+
} else if (args.length > 0) {
601+
progressData = args[0];
604602
}
605603

606604
try {
607-
if (!progressJson) return;
608-
const progress = JSON.parse(progressJson) as DownloadProgress;
609-
if (!progress) return;
605+
if (!progressData) return;
606+
607+
// Handle both object (new) and JSON string (legacy/fallback)
608+
if (typeof progressData === "string") {
609+
try {
610+
progressData = JSON.parse(progressData);
611+
} catch (e) {
612+
console.error("Failed to parse progress json", e);
613+
return;
614+
}
615+
}
616+
617+
if (!progressData || typeof progressData !== 'object') return;
618+
619+
// Map snake_case (Rust) to camelCase (JS)
620+
// Rust struct: { percent, transferred_bytes, total_bytes }
621+
const percent = progressData.percent;
622+
const transferredBytes = progressData.transferredBytes ?? progressData.transferred_bytes ?? 0;
623+
const totalBytes = progressData.totalBytes ?? progressData.total_bytes ?? 0;
610624

611625
win.webContents.send("download-progress", {
612626
id: songData?.id,
613-
percent: progress.percent,
614-
transferredBytes: progress.transferredBytes,
615-
totalBytes: progress.totalBytes,
627+
percent: percent,
628+
transferredBytes: transferredBytes,
629+
totalBytes: totalBytes,
616630
});
617631
} catch (e) {
618632
console.error(
619-
"Failed to parse progress json",
633+
"Error processing progress callback",
620634
e,
621-
"Input:",
622-
progressJson,
623635
"Args:",
624636
args,
625637
);
@@ -635,15 +647,35 @@ const initFileIpc = (): void => {
635647
const threadCount =
636648
(options.threadCount as number) || (store.get("downloadThreadCount") as number) || 8;
637649

638-
await tools.downloadFile(
639-
songData?.id || 0,
640-
url,
641-
finalFilePath,
642-
metadata,
643-
threadCount,
644-
referer,
645-
onProgress,
646-
);
650+
const enableHttp2 =
651+
enableDownloadHttp2 !== undefined
652+
? enableDownloadHttp2
653+
: (store.get("enableDownloadHttp2", true) as boolean);
654+
655+
// Upgrade HTTP to HTTPS if HTTP2 is enabled (HTTP2 usually requires HTTPS)
656+
let finalUrl = url;
657+
if (enableHttp2 && finalUrl.startsWith("http://")) {
658+
finalUrl = finalUrl.replace(/^http:\/\//, "https://");
659+
ipcLog.info(`🔒 Upgraded download URL to HTTPS for HTTP/2 support: ${finalUrl}`);
660+
}
661+
662+
const task = new tools.DownloadTask();
663+
const downloadId = songData?.id || 0;
664+
activeDownloads.set(downloadId, task);
665+
666+
try {
667+
await task.download(
668+
finalUrl,
669+
finalFilePath,
670+
metadata,
671+
threadCount,
672+
referer,
673+
onProgress,
674+
enableHttp2,
675+
);
676+
} finally {
677+
activeDownloads.delete(downloadId);
678+
}
647679

648680
// 创建同名歌词文件
649681
if (lyric && saveMetaFile && downloadLyric) {
@@ -667,8 +699,9 @@ const initFileIpc = (): void => {
667699

668700
// 取消下载
669701
ipcMain.handle("cancel-download", async (_, songId: number) => {
670-
if (tools) {
671-
tools.cancelDownload(songId);
702+
const task = activeDownloads.get(songId);
703+
if (task) {
704+
task.cancel();
672705
return true;
673706
}
674707
return false;

electron/main/services/MusicCacheService.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { existsSync } from "fs";
22
import { rename, stat, unlink } from "fs/promises";
33
import { cacheLog } from "../logger";
4+
import { useStore } from "../store";
45
import { loadNativeModule } from "../utils/native-loader";
56
import { CacheService } from "./CacheService";
67

@@ -90,17 +91,19 @@ export class MusicCacheService {
9091
}
9192

9293
// 使用 Rust 下载器
93-
// 这里的 id 仅用于进度或取消,缓存下载暂时传入 0 或尝试转换
94-
const numericId = typeof id === "number" ? id : 0;
9594

96-
await tools.downloadFile(
97-
numericId,
95+
const store = useStore();
96+
const enableHttp2 = store.get("enableDownloadHttp2", true) as boolean;
97+
98+
const task = new tools.DownloadTask();
99+
await task.download(
98100
url,
99101
tempPath,
100102
null, // No metadata for cache
101103
4, // Thread count
102104
null, // Referer
103105
() => {}, // No progress callback needed for cache currently
106+
enableHttp2,
104107
);
105108

106109
// 检查临时文件是否存在

electron/main/store/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ export interface StoreType {
6969
/** 端口 */
7070
port: number;
7171
};
72+
/** 下载线程数 */
73+
downloadThreadCount?: number;
74+
/** 启用HTTP2下载 */
75+
enableDownloadHttp2?: boolean;
7276
}
7377

7478
/**
@@ -109,6 +113,8 @@ export const useStore = () => {
109113
enabled: false,
110114
port: 25885,
111115
},
116+
downloadThreadCount: 8,
117+
enableDownloadHttp2: true,
112118
},
113119
});
114120
};

native/external-media-integration/src/discord.rs

Lines changed: 7 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,20 @@
11
use std::{
22
sync::{
3-
LazyLock,
4-
Mutex,
5-
mpsc::{
6-
self,
7-
Receiver,
8-
Sender,
9-
},
3+
LazyLock, Mutex,
4+
mpsc::{self, Receiver, Sender},
105
},
116
thread,
12-
time::{
13-
Duration,
14-
SystemTime,
15-
UNIX_EPOCH,
16-
},
7+
time::{Duration, SystemTime, UNIX_EPOCH},
178
};
189

1910
use discord_rich_presence::{
20-
DiscordIpc,
21-
DiscordIpcClient,
22-
activity::{
23-
Activity,
24-
ActivityType,
25-
Assets,
26-
Button,
27-
StatusDisplayType,
28-
Timestamps,
29-
},
30-
};
31-
use tracing::{
32-
debug,
33-
info,
34-
warn,
11+
DiscordIpc, DiscordIpcClient,
12+
activity::{Activity, ActivityType, Assets, Button, StatusDisplayType, Timestamps},
3513
};
14+
use tracing::{debug, info, warn};
3615

3716
use crate::model::{
38-
DiscordConfigPayload,
39-
DiscordDisplayMode,
40-
MetadataPayload,
41-
PlayStatePayload,
42-
PlaybackStatus,
17+
DiscordConfigPayload, DiscordDisplayMode, MetadataPayload, PlayStatePayload, PlaybackStatus,
4318
TimelinePayload,
4419
};
4520

native/external-media-integration/src/lib.rs

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,7 @@
66
77
use napi::{
88
Result,
9-
bindgen_prelude::{
10-
Function,
11-
Unknown,
12-
},
9+
bindgen_prelude::{Function, Unknown},
1310
threadsafe_function::UnknownReturnValue,
1411
};
1512
use napi_derive::napi;
@@ -20,13 +17,8 @@ mod model;
2017
mod sys_media;
2118

2219
use model::{
23-
DiscordConfigPayload,
24-
MetadataParam,
25-
MetadataPayload,
26-
PlayModePayload,
27-
PlayStatePayload,
28-
SystemMediaEvent,
29-
TimelinePayload,
20+
DiscordConfigPayload, MetadataParam, MetadataPayload, PlayModePayload, PlayStatePayload,
21+
SystemMediaEvent, TimelinePayload,
3022
};
3123

3224
/// 初始化插件

native/external-media-integration/src/logger.rs

Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,13 @@
1-
use std::{
2-
fs,
3-
path::PathBuf,
4-
sync::OnceLock,
5-
};
1+
use std::{fs, path::PathBuf, sync::OnceLock};
62

7-
use anyhow::{
8-
Context,
9-
Result,
10-
};
3+
use anyhow::{Context, Result};
114
use time::macros::format_description;
12-
use tracing::{
13-
error,
14-
trace,
15-
};
16-
use tracing_appender::{
17-
non_blocking::WorkerGuard,
18-
rolling::RollingFileAppender,
19-
};
5+
use tracing::{error, trace};
6+
use tracing_appender::{non_blocking::WorkerGuard, rolling::RollingFileAppender};
207
use tracing_subscriber::{
218
Layer,
22-
filter::{
23-
LevelFilter,
24-
Targets,
25-
},
26-
fmt::{
27-
self,
28-
time::LocalTime,
29-
},
9+
filter::{LevelFilter, Targets},
10+
fmt::{self, time::LocalTime},
3011
layer::SubscriberExt,
3112
util::SubscriberInitExt,
3213
};

native/external-media-integration/src/sys_media/linux.rs

Lines changed: 8 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,30 @@
11
use std::{
22
io::Write,
33
process,
4-
sync::{
5-
Arc,
6-
RwLock,
7-
},
4+
sync::{Arc, RwLock},
85
thread,
9-
time::{
10-
SystemTime,
11-
UNIX_EPOCH,
12-
},
6+
time::{SystemTime, UNIX_EPOCH},
137
};
148

159
use anyhow::Result;
1610
use mpris_server::{
17-
LoopStatus as MprisLoopStatus,
18-
Metadata,
19-
PlaybackStatus as MprisPlaybackStatus,
20-
Player,
21-
Time,
11+
LoopStatus as MprisLoopStatus, Metadata, PlaybackStatus as MprisPlaybackStatus, Player, Time,
2212
zbus::zvariant::ObjectPath,
2313
};
2414
use napi::threadsafe_function::ThreadsafeFunctionCallMode;
2515
use tempfile::NamedTempFile;
2616
use tokio::{
2717
runtime::Runtime,
28-
sync::mpsc::{
29-
UnboundedReceiver,
30-
UnboundedSender,
31-
unbounded_channel,
32-
},
33-
};
34-
use tracing::{
35-
debug,
36-
error,
18+
sync::mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel},
3719
};
20+
use tracing::{debug, error};
3821

3922
use crate::{
4023
model::{
41-
MetadataPayload,
42-
PlayModePayload,
43-
PlayStatePayload,
44-
PlaybackStatus,
45-
RepeatMode,
46-
SystemMediaEvent,
47-
SystemMediaEventType,
48-
TimelinePayload,
49-
},
50-
sys_media::{
51-
SystemMediaControls,
52-
SystemMediaThreadsafeFunction,
24+
MetadataPayload, PlayModePayload, PlayStatePayload, PlaybackStatus, RepeatMode,
25+
SystemMediaEvent, SystemMediaEventType, TimelinePayload,
5326
},
27+
sys_media::{SystemMediaControls, SystemMediaThreadsafeFunction},
5428
};
5529

5630
/// 主线程和后台 D-Bus 现成通信的指令

0 commit comments

Comments
 (0)