本文将演示如何使用 aria2 rpc 开发一个下载模块,注意,这不是一个完整的应用,仅仅是为了给你一些启发。
tech | version |
---|---|
electron | 30.0.6 |
webpack | 5.91.0 |
nodejs | v20.14.0 |
aria2 | 1.37.0 |
React | 18.2.0 |
react-use-websocket | 4.8.1 |
@mui/x-charts/SparkLineChart | 7.3.2 |
成品演示
加载并启动 aria2
怎么将 aria2 集成到你的项目中
你可以要求你的用户自行安装 aria2c.exe,或者将 aria2c.exe 直接打包到你的项目中。
如果你采用后者,下面是一些示例。
假设你的工程目录是:
1src
2build
3 |-- aria2c.exe
4package.json
打包
下面是一个使用 Electron Builder 的示例,它将 build/aria2c.exe 拷贝到安装后的根目录。
1"scripts": {
2 "dev": "xxxxxxx"
3},
4"build": {
5 "extraFiles": [
6 {
7 "from": "build/aria2c.exe",
8 "to": ""
9 },
10}
将它放到 package.json 中。
调用
我们希望开发和生产的时候,都能调用到这个 aria2c.exe.
1// prod
2let downloadBin = path.join(path.dirname(process.execPath), 'aria2c.exe');
3if (dev) {
4 // dev
5 downloadBin = path.join(process.cwd(), 'build', 'aria2c.exe');
6}
如何启动
给它一些必要的参数,启动!
1function buildAargs(pid: number) {
2 const mustOptions = [`--enable-rpc`, `--stop-with-process=${pid}`];
3 // ... 比如支持从程序启动参数中自由添加配置,可以随意定制
4 return [...mustOptions];
5}
6
7const start = async () => new Promise < number | undefined > ((resolve, reject) => {
8 const mainPid = process.pid;
9 const args = buildAargs(mainPid);
10
11 if (!fs.existsSync(downloadBin)) {
12 logger.error(downloadBin + " not exists.");
13 reject(something);
14 return;
15 }
16
17 logger.log(`aria2c.exe started`);
18 const downloadClient = spawn(downloadBin, args);
19
20 downloadClient.stdout.on('data', (data) => {
21 const str = JSON.stringify(data);
22 if (str.indexOf("RPC: listening on TCP port") > 0) {
23 resolve(downloadClient.pid);
24 // ...
25 }
26 });
27
28 downloadClient.stderr.on('data', (data) => {
29 logger.error(`aria2c.exe error: ${data}`);
30 // ...
31 });
32
33 downloadClient.on('close', (code) => {
34 logger.log(`aria2c.exe: child process exited with code ${code || ""}`);
35 // ...
36 });
37});
aria2c默认会监听到 ws://127.0.0.1:6800/jsonrpc,实际上该端口不会和 http 端口冲突,所以你暂且可以不用做一些端口冲突的处理。
使用react-use-websocket连接
以下是一个简单的示例,最终使用的示例在下一小节,你应该先使用这个小示例,确保能够从 rpc server 中读取到消息。
1import useWebSocket from "react-use-websocket";
2import { Options } from "react-use-websocket/src/lib/types";
3
4export function useMyAria(options?: Options) {
5 const rpcServer = "ws://127.0.0.1:6800/jsonrpc";
6 // options was shared
7 return useWebSocket<Partial<AriaResponse>>(rpcServer, {
8 share: true,
9 shouldReconnect: () => true,
10 onOpen: () => {
11 },
12 onMessage: (e) => {
13 },
14 onError: (e) => {
15 nconsole.log("rpc listener: error occurred" + JSON.stringify(e));
16 },
17 onClose: () => {
18 },
19 ...options,
20 });
21}
假设你在另一个模块中,使用这个 hook:
1const { sendJsonMessage, readyState } = useMyAria({
2 onMessage(e: MessageEvent<any>) {
3 if (!e.data) {
4 return;
5 }
6 console.log(e.data);
7 }
8});
9
10// 显示当前的活跃下载数量
11const onButtonClick = (e) => {
12 sendJsonMessage([{
13 jsonrpc: "2.0",
14 method: "aria2.tellActive",
15 id: "rpc_timer_tell_active"
16 }]);
17};
18
19// 一次下载两个需要 cookie 认证的文件
20const onDownloadClick = (e) => {
21 const jsonRpcMsg = [
22 {
23 "id": "e46bc4e56b7a6e33",
24 "jsonrpc": "2.0",
25 "method": "aria2.addUri",
26 "params": [
27 ["https://zzzz.xxxx.com/tttt/myfile.7z.003?fname=myfile.7z.003"],
28 {
29 "dir": "D:\\games",
30 "gid": "e46bc4e56b7a6e33",
31 "header": [
32 "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
33 "Accept-Encoding: gzip, deflate, br, zstd",
34 "Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6,hu;q=0.5",
35 "Connection: keep-alive",
36 "Cookie: your cookie",
37 "Host: yyyy.xxxx.com",
38 "Referer: https://www.xxxx.com/",
39 "Sec-Ch-Ua: \"Chromium\";v=\"124\", \"Microsoft Edge\";v=\"124\", \"Not-A.Brand\";v=\"99\"",
40 "Sec-Ch-Ua-Mobile: ?0",
41 "Sec-Ch-Ua-Platform: \"Windows\"",
42 "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0"
43 ],
44 "max-connection-per-server": 16,
45 "min-split-size": "1M",
46 "split": 16
47 }
48 ]
49 },
50 {
51 "jsonrpc": "2.0",
52 "method": "aria2.addUri",
53 "params": [
54 [
55 "https://zzzz.xxxx.com/tttt/myfile.7z.001?fname=myfile.7z.001\u0026from=30111\u0026version=3.3.3.3"
56 ],
57 {
58 "dir": "D:\\games",
59 "gid": "fcfc5dd991923b96",
60 "header": [
61 "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
62 "Accept-Encoding: gzip, deflate, br, zstd",
63 "Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6,hu;q=0.5",
64 "Connection: keep-alive",
65 "Cookie: your cookie",
66 "Host: yyyy.xxxx.com",
67 "Referer: https://www.xxxx.com/",
68 "Sec-Ch-Ua: \"Chromium\";v=\"124\", \"Microsoft Edge\";v=\"124\", \"Not-A.Brand\";v=\"99\"",
69 "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0"
70 ],
71 "max-connection-per-server": 16,
72 "min-split-size": "1M",
73 "split": 16
74 }
75 ],
76 "id": "fcfc5dd991923b96"
77 }
78 ];
79 sendJsonMessage(jsonRpcMsg);
80};
81
82return (
83 <div>
84 <button onClick={onButtonClick}>显示当前活跃的下载信息</button>
85 <button onClick={onDownloadClick}>下载</button>
86 </div>
87);
jsonRpc 的格式非常奇怪,你必须适应。一般来说,它是这种格式:
1interface JsonRpcMessage {
2 id: string,
3 method: string,
4 jsonrpc: string,
5 params: [string[], { [key: string]: any }],
6}
它的 params 数组,params[0] 是一系列下载地址,params[1] 是一个 object,里面有很多的配置项。例如,你可以配置下载的文件夹,分割多少份下载,还有 cookie。
你 必须 在代码中组装上面这样的 json 字符串,然后发送给 aria2c。
定时轮询与监听
一般人到这已经放弃了。
如果你希望显示实时的下载进度,你可以主动向 aria 发起 rpc 调用,然后接收它的结果,再次声明,这个代码无法直接运行,只是为了给你一些启发。
1interface AriaResponse {
2 id: string;
3 error: {
4 code: number;
5 message: string;
6 };
7 jsonrpc: string;
8 method: string,
9 params: any[],
10 result: any,
11}
12
13interface AriaGidReport {
14 gid: string;
15 status: string;
16 downloadSpeed: string;
17 errorCode: string;
18 errorMessage: string;
19 completedLength: string;
20 followedBy: string[];
21 following: string;
22 totalLength: string;
23 verifyIntegrityPending: string;
24 files: { path: string }[];
25}
26
27const { sendJsonMessage, readyState } = useMyAria({
28 onMessage(e: MessageEvent<any>) {
29 if (!e.data) {
30 return;
31 }
32 const informs = JSON.parse(e.data) as AriaResponse[];
33 if (!informs || informs.length === 0) {
34 return;
35 }
36 let totalResults: AriaGidReport[] = [];
37 for (let i = 0; i < informs.length; i++) {
38 const msg = informs[i];
39 if (msg.id === "rpc_timer_tell_active" || msg.id === "rpc_timer_tell_stop" || msg.id === "rpc_timer_tell_wait") {
40 totalResults = totalResults.concat(msg.result as AriaGidReport[]);
41 }
42 }
43 if (totalResults.length === 0) {
44 return;
45 }
46 memStore.addAllAriaStats(totalResults);// 这里就是所有的结果,你可以在另外一个模块中消费它
47 },
48});
49
50useEffect(() => {
51 const queryStatus = () => {
52 // nconsole.log("rpc timer query status");
53 sendJsonMessage([{
54 jsonrpc: "2.0",
55 method: "aria2.tellActive",
56 id: 'rpc_timer_tell_active',
57 },
58 {
59 jsonrpc: "2.0",
60 method: "aria2.tellWaiting",
61 id: 'rpc_timer_tell_wait',
62 params: [0, 1000],
63 },
64 {
65 jsonrpc: "2.0",
66 method: "aria2.tellStopped",
67 id: 'rpc_timer_tell_stop',
68 params: [0, 1000],
69 },
70 ]);
71 };
72
73 const queryId = setInterval(queryStatus, queryAndHandleInterval);
74 return () => {
75 nconsole.debug("rpc timer query clearing interval id queryStatus");
76 clearInterval(queryId);
77 };
78}, [readyState]);
而 aria 支持一些 rpc 的通知:
1type ariaEvent =
2 "aria2.onDownloadStart"
3 | "aria2.onDownloadPause"
4 | "aria2.onDownloadStop"
5 | "aria2.onDownloadComplete"
6 | "aria2.onDownloadError"
7 | "aria2.onBtDownloadComplete";
这些通知不能告诉你下载的进度,但是当下载达到重要节点的时候,会通知你。你可以利用它们做一些事情,例如下载失败,重新下载等。
磁力链,直链与种子文件
磁力链,种子和直链下载的处理方式是不一样的。
磁力链和种子任务,下载完成后,会做种。你查询回的结果,虽然下载进度到了 100%,但是 status 仍然不是 complete。
所以你应该根据下载的大小,是否 active 等结合起来判断一个任务是否已经完成。
1const isTaskPending = (task: Task) => {
2 // const gids = ... || [];
3 if (gids.length === 1 && task.type === "magnet") return true;
4 if (gids.length === 0) return true;
5 const isPending = gids.some((x) => {
6 const stat = memStore.status.taskBlink[x];
7 if (stat) {
8 nconsole.log(stat.files[0].path, stat.status, stat.verifyIntegrityPending ? "verifying" : "", stat.completedLength, stat.totalLength);
9 }
10 return !stat
11 || (Number(stat.totalLength) === 0)
12 || (Number(stat.completedLength) < Number(stat.totalLength))
13 || stat.status === "waiting"
14 || stat.status === "active"
15 || stat.verifyIntegrityPending === "true"; // 如果开启了文件校验,应该等待文件校验完毕再判定下载成功
16 });
17
18 if (!isPending) {
19 // 为了 debug 用
20 nconsole.log("task finished");
21 }
22
23 return isPending;
24};
当然你也可以依赖 onDownloadComplete 这些事件,但是它们只发送一次,因此并不可靠。
下载完成校验文件
option | description |
---|---|
checksum | 开启文件校验,如 checksum=sha-1=123123… |
–check-integrity | 校验失败是否重新下载,checksum不开启没有作用 |
当开启这些参数,下载完成后,会进行文件校验,并且查询状态的结果中,该任务会有 verifyIntegrityPending=true 属性。
可以借此来判断下载任务的进度。
完成
到此,你已经会通过 electron 发送 json rpc 消息,和 aria2c 交互,创建新的下载,查询下载状态,并监控下载完成。
抱歉我不能将完整的代码发出来,但是如我再三强调,每个人的项目都不一样,而且实现这些目标的方法有很多种。本文旨在给你启发。
如果你希望获得帮助,可以留下你的评论。