Aria2 Rpc

Aria2 Rpc

This article will demonstrate how to develop a download module using aria2 rpc, note that code provided in this article is not a complete application, just to give you some inspiration.

techversion
electron30.0.6
webpack5.91.0
nodejsv20.14.0
aria21.37.0
React18.2.0
react-use-websocket4.8.1
@mui/x-charts/SparkLineChart7.3.2

aria2 docs react-use-websocket docs

Demonstration of the finished project

Package and start aria2

How to integrate aria2 into your project

You can either ask your users to install aria2c.exe by themselves, or package aria2c.exe directly into your project.

If you choose the latter one, here are some examples.

Suppose your project directory is:

1src
2build
3  |-- aria2c.exe
4package.json

Packaging

The following is an example of using Electron Builder, which copies build/aria2c.exe into the root directory of the installation.

 1"scripts": {
 2    "dev": "xxxxxxx"
 3},
 4"build": {
 5    "extraFiles": [
 6        {
 7            "from": "build/aria2c.exe",
 8            "to": ""
 9        },
10}

Put this code snippet in package.json.

Calling

We want this aria2c.exe to be ready for both development and production.

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}

How to start it

Just give it some necessary args, and start it.

 1function buildAargs(pid: number) {
 2  const mustOptions = [`--enable-rpc`, `--stop-with-process=${pid}`];
 3  // ... you can write some code to enable configurations from the program startup parameters
 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 listens to ws://127.0.0.1:6800/jsonrpc by default, which actually doesn’t conflict with the http port, so you don’t have to do anything about port conflicts for now.

Use react-use-websocket

The following is a simple example, you should use this small example first to make sure you can read the message from the 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}

Here is another module that uses this 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// Show current active download tasks
11const onButtonClick = (e) => {
12  sendJsonMessage([{
13    jsonrpc: "2.0",
14    method: "aria2.tellActive",
15    id: "rpc_timer_tell_active"
16  }]);
17};
18
19// Download two files at once that require cookie authentication
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);

The format of json rpc is very odd, you have to get used to it. In general, here is what it looks like:

1interface JsonRpcMessage {
2  id: string,
3  method: string,
4  jsonrpc: string,
5  params: [string[], { [key: string]: any }],
6}

The first element of params, which is params[0], is a list of download addresses, and params[1] is an object with a lot of configuration items in it.

For example, you can configure which folder to download to, how many downloads to split, and headers and cookies.

You must assemble a json string like the one above in code and send it to aria2c.

aria2 rpc react-use-websocket 文档

Status Monitoring

A lot of people might have given up by this point.

If you want to show real-time download progress, you can initiate a rpc call to aria and receive its results, again, this code won’t run directly, just to give you some ideas.

 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]);

And aria supports some rpc event notifications:

1type ariaEvent =
2  "aria2.onDownloadStart"
3  | "aria2.onDownloadPause"
4  | "aria2.onDownloadStop"
5  | "aria2.onDownloadComplete"
6  | "aria2.onDownloadError"
7  | "aria2.onBtDownloadComplete";

These events won’t tell you the progress of the downloads,

but they will notify you when the download reaches an important status.

You can use them for scenarios like failed downloads, re-downloads, etc.

Magnet, direct links and torrent downloads are handled differently.

Magnet and torrent tasks, after the downloads are complete, will do the seeding.

The result of your query, although the download progress has reached 100%, the status is still not complete.

So you should judge whether a task is completed based on the size of the download, whether it is active, etc.

 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};

Of course, you can also rely on onDownloadComplete events, but they are not stable because they send notifications only once.

File validation after download

optiondescription
checksumenable file validation, like checksum=sha-1=123123…
–check-integritywhether re-download if failed to checksum, won’t work if checksum not specified

When both parameters are turned on, files are verified when the download completes and the task will have the verifyIntegrityPending=true attribute in the results of the query status.

This can be used to determine the progress of the download task.

Done

By this point you will have sent json rpc messages via electron, interacted with aria2c, created new downloads, and queried the status of downloads.

I’m sorry I can’t post the full code, but as I’ve emphasized again and again, everyone’s project is different and there are many ways to achieve these goals. This article is meant to inspire you.

You can leave your comments if you would like help.