这里仅简单说一下 aria2 是什么:一款下载工具,一款很NB的下载工具,它可以作为命令式使用,也能开启rpc保持后台启动并通过http或websocket调用。

1. 启动服务

  • 1.1 命令式启动

    aria2c --enable-rpc --rpc-listen-all=true --rpc-allow-origin-all -c -D
    
  • 1.2 配置文件启动

    使用配置文件,比如${HOME}/.aria2.conf,不过这不在本篇的主题内,可参考附录3自行了解。

2. 简单封装模块

前言: 只用到部分功能,且不想安装太多依赖,所以仅封装了addUri和部分相关联的接口,用于node.js环境,要求aria2开启rpc通信。

'use strict'

const { EventEmitter } = require('events')
const WebSocket = require('ws')

const CFG = {
    aria2: {
        url: 'ws://127.0.0.1:6800/jsonrpc',
        secret: ''
    }
}

let isURL = /(http|ftp|https):\/\/[\w-]+(\.[\w-]+)+([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])?/

function isObject(o) {
    return o !== null && typeof o === 'object' && Array.isArray(o) === false
}

class Aria2 {
    constructor() {
        if (!CFG.aria2.url) throw Error('invalid config')
        this.url = CFG.aria2.url
        this.secret = CFG.aria2.secret ? `token:${CFG.aria2.secret}` : ''
        // _gd(global data) format(object): key=gid, value=gid data
        //   - gid data(object with gid):
        //     - status(string): download status
        //     - stream(object): with tellStatus method
        //     - info(object): with getFiles method
        this._gd = {}
        this._emt = new EventEmitter()
        this._ws = new WebSocket(this.url)
        this._ws.on('message', res => {
            this.parseResponse(res)
        })
        this._ws.on('error', err => {
            throw Error(err)
        })
        this._lastsid = null
        this.on('downInit', gid => {
            this._gd[gid] = { status: 'init', gid }
            this.getFiles(gid)
            this.tellStatus(gid)
            this._lastsid = setInterval(() => {
                this.tellStatus(gid)
            }, 1000)
        })
        this.on('downStatus', res => {
            this._gd[res.gid].stream = res
            this._gd[res.gid].status = res.status
        })
        this.on('downInfo', res => {
            this._gd[res.gid].info = res
        })
        this.on('downComplete', gid => {
            if (this._lastsid) clearInterval(this._lastsid)
            this.getFiles(gid)
            this.tellStatus(gid)
            this._gd[gid].status = 'complete'
        })
        this.on('downError', gid => {
            this._gd[gid].status = 'error'
        })
        this.on('response', res => {
            //console.log(res)
        })
    }

    send(body) {
        if (this._ws.readyState === 1) {
            this._ws.send(body)
        } else {
            setTimeout(() => {
                this._ws.send(body)
            }, 1000)
        }
    }

    on(event, cb) {
        if (event && typeof cb === 'function') {
            /**
             * @event response: param is jsonrpc message
             * @event error: param is jsonrpc message
             * @event success: param is jsonrpc message(object)
             * @event downInit: param is gid
             * @event downError: param is gid
             * @event downStatus: param is result(object with gid)
             * @event downInfo: param is result(object with gid)
             * @event downComplete: param is gid
             */
            this._emt.on(event, cb)
        }
    }

    addUri(url) {
        if (isURL.test(url)) {
            let req = {
                params: [this.secret, [url]],
                jsonrpc: '2.0',
                id: 'Init',
                method: 'aria2.addUri'
            }
            this.send(JSON.stringify(req))
        } else {
            throw Error('invalid url')
        }
    }

    getFiles(gid) {
        if (gid) {
            let req = {
                params: [this.secret, gid],
                jsonrpc: '2.0',
                id: `Info.${gid}`,
                method: 'aria2.getFiles'
            }
            this.send(JSON.stringify(req))
        }
    }

    tellStatus(gid) {
        if (gid) {
            let req = {
                params: [
                    this.secret,
                    gid,
                    [
                        'gid',
                        'status',
                        'totalLength',
                        'completedLength',
                        'errorCode',
                        'errorMessage',
                        'dir'
                    ]
                ],
                jsonrpc: '2.0',
                id: 'Status',
                method: 'aria2.tellStatus'
            }
            this.send(JSON.stringify(req))
        }
    }

    parseResponse(res) {
        try {
            res = JSON.parse(res)
        } catch (error) {
            throw Error('invalid response')
        }
        this._emt.emit('response', res)
        if (!isObject(res) || res.hasOwnProperty('error')) {
            //error response
            this._emt.emit('error', res)
        } else {
            //success response
            this._emt.emit('success', res)
            if (res.hasOwnProperty('id')) {
                //a request-response mapping pair
                switch (res.id.split('.')[0]) {
                    case 'Init':
                        this._emt.emit('downInit', res.result)
                        break
                    case 'Status':
                        this._emt.emit('downStatus', res.result)
                        break
                    case 'Info':
                        let { completedLength, length, path } = res.result[0]
                        this._emt.emit('downInfo', {
                            gid: res.id.split('.')[1],
                            path,
                            completedLength,
                            totalLength: length
                        })
                        break
                }
            } else {
                //a notification from server, has method, param as data
                switch (res.method) {
                    case 'aria2.onDownloadError':
                        this._emt.emit('downError', res.params[0].gid)
                        break
                    case 'aria2.onDownloadComplete':
                        this._emt.emit('downComplete', res.params[0].gid)
                        break
                }
            }
        }
    }

    getData(gid) {
        return this._gd[gid] || {}
    }
}

module.exports = Aria2

3. 附录

  • 3.1 使用nginx反向代理aria2 jsonrpc

    location ^~ /jsonrpc {
        # websocket
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_http_version 1.1;
        # aria2 jsonrpc address
        proxy_pass http://127.0.0.1:6800/jsonrpc;
        add_header Front-End-Https on;
        proxy_set_header Host $http_host;
        proxy_set_header X-NginX-Proxy true;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_pass_header X-Transmission-Session-Id;
    }
    
  • 3.2 以上脚本内容托管于github/staugur/script/~/aria2.js

  • 3.3 关于aria2配置可以参考这篇博客


·End·