Skill: killgfat/aria2-rpc (version 0.1.2)

--- name: aria2-rpc description: Remote control for aria2 download service via JSON-RPC 2.0. Supports adding downloads (HTTP/FTP/Torrent/Magnet), querying task status, pausing/resuming, and removing tasks. Works with local or remote aria2 instances. allowed-tools: Bash(aria2:*) homepage: https://aria2.github.io metadata: openclaw: requires: bins: [curl, python3] pip: [requests] --- # aria2-rpc Skill 通过 JSON-RPC 2.0 协议远程控制 aria2 下载服务。 ## 功能 - 添加 HTTP/FTP/Magnet/Torrent 下载 - 查询任务状态、暂停/继续/删除任务 - 获取全局统计和配置 - 支持远程 aria2 实例和 RPC 认证 ## 安装 ```bash # 安装依赖 pip3 install requests # 配置 aria2（启用 RPC） aria2c --enable-rpc --rpc-listen-all=true --rpc-[REDACTED_SECRET] -D ``` ## 快速开始 ```bash # 添加下载 python3 /root/.openclaw/workspace/skills/aria2-rpc/scripts/aria2_rpc.py add-uri "http://example.com/file.zip" --rpc-secret mytoken # 查看进度 python3 scripts/aria2_rpc.py tell-active --rpc-secret mytoken ``` ## 环境变量 ```bash export ARIA2_RPC_URL="http://localhost:6800/jsonrpc" export ARIA2_RPC_SECRET="your-secret-token" ``` ## 脚本路径 - **主脚本**: `/root/.openclaw/workspace/skills/aria2-rpc/scripts/aria2_rpc.py` ## 详细文档 - [快速开始](references/QUICKSTART.md) - [配置指南](references/CONFIG_GUIDE.md) - [使用示例](references/USAGE.md) - [API 参考](references/API_REFERENCE.md) - [配置示例](references/aria2.conf.example)

path: clawhub.json
content: {
  "slug": "aria2-rpc",
  "name": "Aria2 RPC",
  "version": "0.1.2",
  "description": "Remote control for aria2 download service via JSON-RPC 2.0 - add downloads, query status, pause/resume, remove tasks, and manage configurations for local or remote aria2 instances",
  "author": "Cortana",
  "changelog": "v0.1.2: Cleaned up test files and README.md for cleaner distribution",
  "tags": ["aria2", "download", "rpc", "remote-control", "bittorrent", "http", "ftp", "magnet"],
  "emoji": "⬇️",
  "category": "download",
  "homepage": "https://aria2.github.io"
}


path: references/API_REFERENCE.md
content: # Aria2 RPC API 参考

## RPC 方法列表

| 方法 | 说明 |
|------|------|
| `aria2.addUri` | 添加 HTTP/FTP/Magnet 下载 |
| `aria2.addTorrent` | 添加 Torrent 下载 |
| `aria2.addMetalink` | 添加 Metalink 下载 |
| `aria2.tellActive` | 获取活动下载列表 |
| `aria2.tellWaiting` | 获取等待下载列表 |
| `aria2.tellStopped` | 获取已停止下载列表 |
| `aria2.tellStatus` | 获取任务详细信息 |
| `aria2.remove` | 删除任务 |
| `aria2.forceRemove` | 强制删除任务 |
| `aria2.pause` | 暂停任务 |
| `aria2.forcePause` | 强制暂停任务 |
| `aria2.unpause` | 继续任务 |
| `aria2.getGlobalStat` | 获取全局统计 |
| `aria2.getOption` | 获取任务级配置 |
| `aria2.getGlobalOption` | 获取全局配置 |
| `aria2.changeOption` | 修改任务级配置 |
| `aria2.changeGlobalOption` | 修改全局配置 |

## 官方文档

详见 [aria2 RPC 官方文档](https://aria2.github.io/manual/en/html/aria2c.html#rpc-interface)

## GID 说明

每个下载任务有唯一的 GID（全局 ID），用于后续操作。GID 是 16 字符的十六进制字符串。

## 故障排除

### 连接被拒绝

```bash
# 检查 aria2 是否运行
ps aux | grep aria2

# 检查端口是否监听
netstat -tlnp | grep 6800

# 检查防火墙
sudo ufw allow 6800/tcp
```

### 认证失败

确保 `rpc-secret` 配置正确，调用时传入正确的 token：
```bash
python3 scripts/aria2_rpc.py add-uri "http://example.com/file.zip" --rpc-secret correct-token
```

### 下载失败

查看 aria2 日志：
```bash
tail -f ~/.aria2/aria2.log
```

path: references/CONFIG_GUIDE.md
content: # aria2 配置管理指南

本文档介绍如何使用 aria2-rpc 获取和修改 aria2 的配置选项。

## 命令概览

| 命令 | 说明 | 作用范围 |
|------|------|---------|
| `get-global-option` | 获取全局配置 | 所有下载任务 |
| `set-global-option` | 修改全局配置 | 所有下载任务 |
| `get-option` | 获取任务配置 | 单个下载任务 |
| `set-option` | 修改任务配置 | 单个下载任务 |

---

## 全局配置管理

### 获取全局配置

```bash
# 查看所有常用配置
python3 scripts/aria2_rpc.py get-global-option

# 查看特定配置项
python3 scripts/aria2_rpc.py get-global-option --key max-overall-download-limit

# 查看所有配置（包括不常用的）
python3 scripts/aria2_rpc.py get-global-option --verbose
```

### 修改全局配置

```bash
# 修改单个配置
python3 scripts/aria2_rpc.py set-global-option max-overall-download-limit=10M

# 修改多个配置
python3 scripts/aria2_rpc.py set-global-option \
  max-overall-download-limit=50M \
  max-connection-per-server=16 \
  split=16

# 带认证的远程修改
python3 scripts/aria2_rpc.py set-global-option \
  max-overall-download-limit=100M \
  --rpc-url http://192.168.1.100:6800/jsonrpc \
  --rpc-secret mytoken
```

### 常用全局配置项

#### 速度限制
- `max-overall-download-limit` - 全局最大下载速度（如：10M, 100K）
- `max-overall-upload-limit` - 全局最大上传速度
- `max-download-limit` - 每个任务的最大下载速度

#### 并发控制
- `max-concurrent-downloads` - 最大同时下载数（默认：5）
- `max-connection-per-server` - 每服务器最大连接数（默认：1）
- `split` - 文件分块数（默认：5）

#### 连接设置
- `timeout` - 超时时间（秒）
- `connect-timeout` - 连接超时时间（秒）
- `max-tries` - 最大重试次数（0 为无限）
- `retry-wait` - 重试等待时间（秒）

#### 下载路径
- `dir` - 默认下载目录

#### 其他常用
- `continue` - 断点续传（true/false）
- `user-agent` - 用户代理字符串
- `all-proxy` - 代理服务器设置
- `save-session` - 会话保存路径

---

## 任务级配置管理

### 获取任务配置

```bash
# 查看任务的常用配置
python3 scripts/aria2_rpc.py get-option <GID>

# 查看任务的所有配置
python3 scripts/aria2_rpc.py get-option <GID> --verbose
```

### 修改任务配置

```bash
# 修改任务的下载速度限制
python3 scripts/aria2_rpc.py set-option <GID> max-download-limit=5M

# 修改任务的分块数
python3 scripts/aria2_rpc.py set-option <GID> split=8

# 修改任务的下载目录
python3 scripts/aria2_rpc.py set-option <GID> dir=/path/to/new/dir

# 同时修改多个配置
python3 scripts/aria2_rpc.py set-option <GID> \
  split=16 \
  max-connection-per-server=8 \
  max-download-limit=20M
```

### 任务级配置 vs 全局配置

- **任务级配置**只影响指定的下载任务
- **全局配置**影响所有新创建的下载任务
- 任务级配置优先级高于全局配置
- 修改全局配置不会影响已在运行的任务

---

## 实用场景

### 场景 1：临时限速

```bash
# 下载时临时降低速度，避免影响其他网络活动
python3 scripts/aria2_rpc.py set-global-option max-overall-download-limit=1M

# 恢复不限速
python3 scripts/aria2_rpc.py set-global-option max-overall-download-limit=0
```

### 场景 2：为重要任务分配更多资源

```bash
# 为特定任务增加连接数和分块数
python3 scripts/aria2_rpc.py set-option <GID> \
  max-connection-per-server=16 \
  split=32
```

### 场景 3：批量修改等待中任务的配置

```bash
# 获取所有等待中的任务
GIDS=$(python3 scripts/aria2_rpc.py tell-waiting | grep -oP '\[GID: \K[a-f0-9]+')

# 批量修改
for gid in $GIDS; do
  python3 scripts/aria2_rpc.py set-option $gid split=16
done
```

### 场景 4：查看当前配置状态

```bash
# 查看当前下载速度限制
python3 scripts/aria2_rpc.py get-global-option --key max-overall-download-limit

# 查看当前并发数设置
python3 scripts/aria2_rpc.py get-global-option --key max-concurrent-downloads
```

---

## 配置项参考

### 完整配置项列表

运行以下命令查看所有可用的配置项：

```bash
python3 scripts/aria2_rpc.py get-global-option --verbose
```

### 部分重要配置项说明

| 配置项 | 说明 | 示例值 |
|--------|------|--------|
| `dir` | 下载文件保存目录 | `/home/user/downloads` |
| `max-overall-download-limit` | 全局下载速度限制 | `10M`, `100K`, `0`(不限) |
| `max-concurrent-downloads` | 最大同时下载数 | `5` |
| `split` | 文件分块数 | `16` |
| `max-connection-per-server` | 每服务器连接数 | `16` |
| `min-split-size` | 最小分块大小 | `10M` |
| `user-agent` | 用户代理 | `Mozilla/5.0...` |
| `all-proxy` | 代理服务器 | `http://proxy:8080` |
| `timeout` | 超时时间（秒） | `60` |
| `continue` | 断点续传 | `true` |
| `enable-rpc` | 启用 RPC | `true` |
| `rpc-listen-port` | RPC 监听端口 | `6800` |
| `save-session` | 会话保存路径 | `/path/aria2.session` |

---

## 注意事项

1. **配置持久性**
   - 通过 RPC 修改的配置只在 aria2 运行期间有效
   - 重启 aria2 后会恢复配置文件中的设置
   - 如需永久修改，请编辑 `aria2.conf` 配置文件

2. **配置生效时间**
   - 全局配置修改后，对新创建的任务立即生效
   - 已在运行的任务不受全局配置修改影响
   - 任务级配置修改后立即对该任务生效

3. **配置值格式**
   - 速度限制可以使用 K（KB/s）或 M（MB/s）后缀
   - 布尔值使用 `true` 或 `false`
   - 数字直接使用阿拉伯数字

4. **错误处理**
   - 如果修改失败，检查配置项名称是否正确
   - 某些配置项在任务运行后无法修改
   - 使用 `get-global-option` 查看当前有效配置

---

## 示例工作流

```bash
# 1. 查看当前全局配置
python3 scripts/aria2_rpc.py get-global-option

# 2. 添加一个下载任务
GID=$(python3 scripts/aria2_rpc.py add-uri "http://example.com/largefile.zip" | grep GID | grep -oP '[a-f0-9]+')

# 3. 为该任务设置高速下载
python3 scripts/aria2_rpc.py set-option $GID split=32 max-connection-per-server=16

# 4. 监控下载进度
python3 scripts/aria2_rpc.py tell-status $GID

# 5. 下载完成后，恢复全局配置
python3 scripts/aria2_rpc.py set-global-option max-overall-download-limit=0
```

---

## 更多信息

- [aria2 官方文档 - RPC 方法](https://aria2.github.io/manual/en/html/aria2c.html#rpc-interface)
- [aria2 官方文档 - 配置选项](https://aria2.github.io/manual/en/html/aria2c.html#id3)


path: references/QUICKSTART.md
content: # aria2-rpc Quick Reference

## 快速命令参考

### 添加下载

```bash
# 添加单个 HTTP 链接
python3 scripts/aria2_rpc.py add-uri "http://example.com/file.zip"

# 添加多个镜像源
python3 scripts/aria2_rpc.py add-uri \
  "http://mirror1/file.zip" \
  "http://mirror2/file.zip"

# 添加 Magnet 链接
python3 scripts/aria2_rpc.py add-uri "magnet:?xt=urn:btih:HASH"

# 指定下载目录和文件名
python3 scripts/aria2_rpc.py add-uri "http://example.com/file.zip" \
  --dir /path/to/download \
  --out myfile.zip

# 使用自定义连接数
python3 scripts/aria2_rpc.py add-uri "http://example.com/file.zip" \
  --split 16 \
  --max-connections 8
```

### 查询状态

```bash
# 查看所有活动下载
python3 scripts/aria2_rpc.py tell-active

# 查看等待中的下载
python3 scripts/aria2_rpc.py tell-waiting

# 查看已停止/已完成的下载
python3 scripts/aria2_rpc.py tell-stopped

# 查看特定任务详情
python3 scripts/aria2_rpc.py tell-status <GID>

# 查看全局统计
python3 scripts/aria2_rpc.py global-stat
```

### 控制下载

```bash
# 暂停下载
python3 scripts/aria2_rpc.py pause <GID>

# 强制暂停下载
python3 scripts/aria2_rpc.py pause <GID> --force

# 继续下载
python3 scripts/aria2_rpc.py unpause <GID>

# 删除下载
python3 scripts/aria2_rpc.py remove <GID>

# 强制删除下载
python3 scripts/aria2_rpc.py remove <GID> --force
```

### 远程连接

```bash
# 使用远程 aria2 服务器
python3 scripts/aria2_rpc.py tell-active \
  --rpc-url http://192.168.1.100:6800/jsonrpc \
  --rpc-secret my-secret-token

# 或设置环境变量
export ARIA2_RPC_URL="http://192.168.1.100:6800/jsonrpc"
export ARIA2_RPC_SECRET="my-secret-token"
python3 scripts/aria2_rpc.py tell-active
```

## 常用场景

### 场景 1: 批量下载

```bash
# 从文件读取 URLs
cat urls.txt | while read url; do
  python3 scripts/aria2_rpc.py add-uri "$url"
done
```

### 场景 2: 监控下载进度

```bash
# 每秒刷新显示活动下载
watch -n 1 'python3 scripts/aria2_rpc.py tell-active 2>/dev/null | head -20'
```

### 场景 3: 下载完成后清理

```bash
# 删除所有已完成的下载
for gid in $(python3 scripts/aria2_rpc.py tell-stopped | grep -oP '\[GID: \K[a-f0-9]+'); do
  python3 scripts/aria2_rpc.py remove "$gid"
done
```

## GID 说明

- 每个下载任务有唯一的 GID（全局 ID）
- GID 是 16 字符的十六进制字符串
- 可以通过 `tell-active`、`tell-waiting`、`tell-stopped` 获取
- 后续操作（暂停、继续、删除）都需要 GID

## 输出说明

### tell-active / tell-waiting / tell-stopped

```
[GID: abc12345] filename.zip
  Status: active | Progress: 45.2%
  Size: 45.20 MB / 100.00 MB
  Speed: ↓ 2.50 MB/s | ↑ 0.00 B/s
  Connections/Peers: 8 | ETA: 22s
```

### global-stat

```
aria2 Global Statistics
============================================================
Active downloads:      3
Waiting downloads:     5
Stopped downloads:     12
Total speed:           ↓ 5.25 MB/s
                       ↑ 125.00 KB/s
Files downloaded:      156
Files removed:         42

aria2 Version:       1.36.0
Enabled Features:    Async DNS, BitTorrent, ...
```

## 故障排除

### 连接被拒绝
```bash
# 检查 aria2 是否运行
ps aux | grep aria2c

# 检查端口
netstat -tlnp | grep 6800

# 启动 aria2（带 RPC）
aria2c --enable-rpc --rpc-listen-all=true --rpc-[REDACTED_SECRET] -D
```

### 认证失败
```bash
# 确保使用正确的 secret
python3 scripts/aria2_rpc.py tell-active --rpc-secret correct-token
```

### 依赖缺失
```bash
# 安装 requests 库
pip3 install requests
```

### 配置管理

```bash
# 获取全局配置
python3 scripts/aria2_rpc.py get-global-option

# 获取特定配置项
python3 scripts/aria2_rpc.py get-global-option --key max-overall-download-limit

# 修改全局配置（单个）
python3 scripts/aria2_rpc.py set-global-option max-overall-download-limit=10M

# 修改全局配置（多个）
python3 scripts/aria2_rpc.py set-global-option \
  max-overall-download-limit=50M \
  split=16 \
  max-connection-per-server=8

# 获取任务配置
python3 scripts/aria2_rpc.py get-option <GID>

# 修改任务配置
python3 scripts/aria2_rpc.py set-option <GID> split=8 dir=/downloads
```

## 常用配置项

### 速度限制
- `max-overall-download-limit` - 全局最大下载速度（如：10M, 0 为不限）
- `max-overall-upload-limit` - 全局最大上传速度

### 并发控制
- `max-concurrent-downloads` - 最大同时下载数（默认：5）
- `split` - 文件分块数（默认：5）
- `max-connection-per-server` - 每服务器连接数（默认：1）

### 其他常用
- `dir` - 下载目录
- `user-agent` - 用户代理
- `timeout` - 超时时间（秒）
- `continue` - 断点续传（true/false）

详见：[references/CONFIG_GUIDE.md](references/CONFIG_GUIDE.md)


path: references/USAGE.md
content: # Aria2 RPC 使用示例

## 基本语法

```bash
python3 /root/.openclaw/workspace/skills/aria2-rpc/scripts/aria2_rpc.py <command> [options]
```

## 命令示例

### 添加下载任务

```bash
# 添加 HTTP/FTP 下载
python3 scripts/aria2_rpc.py add-uri "http://example.com/file.zip"

# 添加多个链接（同一文件的不同源）
python3 scripts/aria2_rpc.py add-uri "http://mirror1/file.zip" "http://mirror2/file.zip"

# 添加 Magnet 链接
python3 scripts/aria2_rpc.py add-uri "magnet:?xt=urn:btih:..."

# 指定下载目录
python3 scripts/aria2_rpc.py add-uri "http://example.com/file.zip" --dir /path/to/download

# 指定文件名
python3 scripts/aria2_rpc.py add-uri "http://example.com/file.zip" --out myfile.zip

# 使用认证
python3 scripts/aria2_rpc.py add-uri "http://example.com/file.zip" --rpc-secret your-token
```

### 查询下载任务

```bash
# 获取活动下载
python3 scripts/aria2_rpc.py tell-active

# 获取等待中的下载
python3 scripts/aria2_rpc.py tell-waiting

# 获取已停止的下载
python3 scripts/aria2_rpc.py tell-stopped

# 获取特定任务详情
python3 scripts/aria2_rpc.py tell-status <GID>
```

### 控制下载任务

```bash
# 暂停下载
python3 scripts/aria2_rpc.py pause <GID>

# 继续下载
python3 scripts/aria2_rpc.py unpause <GID>

# 删除下载
python3 scripts/aria2_rpc.py remove <GID>

# 强制删除（包括已完成的）
python3 scripts/aria2_rpc.py remove-force <GID>
```

### 全局统计

```bash
python3 scripts/aria2_rpc.py global-stat
```

## 远程实例配置

```bash
# 使用远程服务器
export ARIA2_RPC_URL="http://192.168.1.100:6800/jsonrpc"
export ARIA2_RPC_SECRET="my-secret"

# 添加下载
python3 scripts/aria2_rpc.py add-uri "http://example.com/file.zip"
```

## 配置文件

创建 `~/.aria2/rpc_config.json`：

```json
{
  "rpc_url": "http://localhost:6800/jsonrpc",
  "rpc_secret": "your-secret-token",
  "timeout": 30
}
```

脚本会自动读取此配置文件。

path: scripts/aria2_rpc.py
content: #!/usr/bin/env python3
"""
aria2-rpc: Remote control for aria2 download service via JSON-RPC 2.0

Usage:
    python3 aria2_rpc.py <command> [options] [arguments]

Commands:
    add-uri       Add HTTP/FTP/Magnet download
    tell-active   Get active downloads
    tell-waiting  Get waiting downloads
    tell-stopped  Get stopped downloads
    tell-status   Get task status by GID
    pause         Pause download
    unpause       Resume download
    remove        Remove download
    remove-force  Force remove download
    global-stat   Get global statistics

Environment Variables:
    ARIA2_RPC_URL     RPC endpoint URL (default: http://localhost:6800/jsonrpc)
    ARIA2_RPC_SECRET  RPC secret token (optional)
"""

import json
import sys
import os
import argparse
import base64
from pathlib import Path

try:
    import requests
except ImportError:
    print("Error: requests library not installed. Run: pip3 install requests", file=sys.stderr)
    sys.exit(1)


class Aria2RPC:
    """aria2 JSON-RPC 2.0 client"""
    
    def __init__(self, rpc_url=None, rpc_secret=None, timeout=30):
        self.rpc_url = rpc_url or os.environ.get(
            'ARIA2_RPC_URL', 
            'http://localhost:6800/jsonrpc'
        )
        self.rpc_secret = rpc_secret or os.environ.get('ARIA2_RPC_SECRET')
        self.timeout = timeout
        self.session = requests.Session()
    
    def _get_auth_token(self):
        """Get authentication token"""
        if self.rpc_secret:
            return f'[REDACTED_SECRET]'
        return None
    
    def _call(self, method, params=None):
        """Make JSON-RPC 2.0 call"""
        payload = {
            'jsonrpc': '2.0',
            'id': 'aria2-rpc',
            'method': f'aria2.{method}'
        }
        
        # Add auth token as first parameter if exists
        auth_token = self._get_auth_token()
        if params is None:
            params = []
        
        if auth_token:
            params = [auth_token] + list(params)
        
        payload['params'] = params
        
        try:
            response = self.session.post(
                self.rpc_url,
                json=payload,
                timeout=self.timeout,
                headers={'Content-Type': 'application/json'}
            )
            response.raise_for_status()
            result = response.json()
            
            if 'error' in result:
                error = result['error']
                print(f"Error: {error.get('message', 'Unknown error')}", file=sys.stderr)
                if 'data' in error:
                    print(f"Details: {error['data']}", file=sys.stderr)
                return None
            
            return result.get('result')
        
        except requests.exceptions.ConnectionError as e:
            print(f"Error: Cannot connect to aria2 RPC at {self.rpc_url}", file=sys.stderr)
            print(f"Details: {e}", file=sys.stderr)
            return None
        except requests.exceptions.Timeout:
            print(f"Error: Connection timeout ({self.timeout}s)", file=sys.stderr)
            return None
        except Exception as e:
            print(f"Error: {e}", file=sys.stderr)
            return None
    
    def add_uri(self, uris, options=None):
        """Add URI download(s)"""
        params = [uris]
        if options:
            params.append(options)
        return self._call('addUri', params)
    
    def add_torrent(self, torrent_path, uris=None, options=None):
        """Add torrent download"""
        try:
            with open(torrent_path, 'rb') as f:
                torrent_b64 = base64.b64encode(f.read()).decode('utf-8')
            
            params = [torrent_b64]
            if uris is None:
                uris = []
            params.append(uris)
            if options:
                params.append(options)
            else:
                params.append({})
            return self._call('addTorrent', params)
        except FileNotFoundError:
            print(f"Error: Torrent file not found: {torrent_path}", file=sys.stderr)
            return None
        except Exception as e:
            print(f"Error reading torrent file: {e}", file=sys.stderr)
            return None
    
    def add_metalink(self, metalink_path):
        """Add metalink download"""
        try:
            with open(metalink_path, 'rb') as f:
                metalink_b64 = base64.b64encode(f.read()).decode('utf-8')
            return self._call('addMetalink', [metalink_b64])
        except Exception as e:
            print(f"Error reading metalink file: {e}", file=sys.stderr)
            return None
    
    def tell_active(self):
        """Get active downloads"""
        return self._call('tellActive')
    
    def tell_waiting(self, offset=0, num=1000):
        """Get waiting downloads"""
        return self._call('tellWaiting', [offset, num])
    
    def tell_stopped(self, offset=0, num=1000):
        """Get stopped downloads"""
        return self._call('tellStopped', [offset, num])
    
    def tell_status(self, gid):
        """Get task status by GID"""
        return self._call('tellStatus', [gid])
    
    def pause(self, gid):
        """Pause download"""
        return self._call('pause', [gid])
    
    def force_pause(self, gid):
        """Force pause download"""
        return self._call('forcePause', [gid])
    
    def unpause(self, gid):
        """Resume download"""
        return self._call('unpause', [gid])
    
    def remove(self, gid):
        """Remove download"""
        return self._call('remove', [gid])
    
    def force_remove(self, gid):
        """Force remove download"""
        return self._call('forceRemove', [gid])
    
    def get_global_stat(self):
        """Get global statistics"""
        return self._call('getGlobalStat')
    
    def purge_download_result(self):
        """Purge completed/error/removed downloads"""
        return self._call('purgeDownloadResult')
    
    def get_version(self):
        """Get aria2 version"""
        return self._call('getVersion')
    
    def get_global_option(self):
        """Get global options"""
        return self._call('getGlobalOption')
    
    def change_global_option(self, options):
        """Change global options
        
        Args:
            options: dict of option name -> value
            
        Returns:
            'OK' on success
        """
        return self._call('changeGlobalOption', [options])
    
    def get_option(self, gid):
        """Get options for a specific download"""
        return self._call('getOption', [gid])
    
    def change_option(self, gid, options):
        """Change options for a specific download
        
        Args:
            gid: Task GID
            options: dict of option name -> value
            
        Returns:
            'OK' on success
        """
        return self._call('changeOption', [gid, options])


def format_size(size_bytes):
    """Format byte size to human readable"""
    if size_bytes is None:
        return "N/A"
    
    size_bytes = int(size_bytes)
    if size_bytes < 0:
        return "N/A"
    
    for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
        if size_bytes < 1024.0:
            return f"{size_bytes:.2f} {unit}"
        size_bytes /= 1024.0
    return f"{size_bytes:.2f} PB"


def format_speed(speed):
    """Format speed to human readable"""
    return format_size(speed) + "/s" if speed else "N/A"


def format_time(seconds):
    """Format seconds to human readable"""
    if seconds is None or seconds < 0:
        return "N/A"
    
    seconds = int(seconds)
    if seconds < 60:
        return f"{seconds}s"
    elif seconds < 3600:
        mins = seconds // 60
        secs = seconds % 60
        return f"{mins}m {secs}s"
    else:
        hours = seconds // 3600
        mins = (seconds % 3600) // 60
        return f"{hours}h {mins}m"


def print_task(task, verbose=False):
    """Print task information"""
    gid = task.get('gid', 'N/A')[:8]
    status = task.get('status', 'unknown')
    files = task.get('files', [])
    filename = files[0].get('path', '') if files else 'N/A'
    if not filename or filename == 'N/A':
        filename = files[0].get('uris', [{}])[0].get('uri', 'N/A') if files else 'N/A'
    
    # Just show filename without path
    if '/' in filename:
        filename = filename.split('/')[-1]
    
    total_length = int(task.get('totalLength', 0))
    completed_length = int(task.get('completedLength', 0))
    download_speed = int(task.get('downloadSpeed', 0))
    upload_speed = int(task.get('uploadSpeed', 0))
    num_peers = task.get('numSeeders', task.get('connections', 'N/A'))
    
    # Progress
    if total_length > 0:
        progress = (completed_length / total_length) * 100
        progress_str = f"{progress:.1f}%"
    else:
        progress_str = "N/A"
    
    # ETA
    if download_speed > 0 and total_length > completed_length:
        eta = (total_length - completed_length) / download_speed
        eta_str = format_time(eta)
    else:
        eta_str = "N/A"
    
    print(f"\n[GID: {gid}] {filename}")
    print(f"  Status: {status} | Progress: {progress_str}")
    print(f"  Size: {format_size(completed_length)} / {format_size(total_length)}")
    print(f"  Speed: ↓ {format_speed(download_speed)} | ↑ {format_speed(upload_speed)}")
    print(f"  Connections/Peers: {num_peers} | ETA: {eta_str}")
    
    if verbose:
        if 'bittorrent' in task:
            bt = task['bittorrent']
            print(f"  Torrent: {bt.get('name', 'N/A')}")
            print(f"  Mode: {task.get('seeder', 'N/A')}")


def cmd_add_uri(args):
    """Handle add-uri command"""
    rpc = Aria2RPC(rpc_url=args.rpc_url, rpc_secret=args.rpc_secret)
    
    uris = args.uris
    options = {}
    
    if args.dir:
        options['dir'] = args.dir
    if args.out:
        options['out'] = args.out
    if args.split:
        options['split'] = str(args.split)
    if args.max_connections:
        options['max-connection-per-server'] = str(args.max_connections)
    
    result = rpc.add_uri(uris, options if options else None)
    
    if result:
        print(f"✓ Download added successfully")
        print(f"  GID: {result}")
        if len(uris) == 1:
            print(f"  URI: {uris[0]}")
        else:
            print(f"  URIs: {len(uris)} sources")
    else:
        sys.exit(1)


def cmd_tell_active(args):
    """Handle tell-active command"""
    rpc = Aria2RPC(rpc_url=args.rpc_url, rpc_secret=args.rpc_secret)
    
    result = rpc.tell_active()
    
    if result is None:
        sys.exit(1)
    
    tasks = result if isinstance(result, list) else []
    
    if not tasks:
        print("No active downloads")
    else:
        print(f"Active downloads: {len(tasks)}")
        print("=" * 60)
        for task in tasks:
            print_task(task, verbose=args.verbose)


def cmd_tell_waiting(args):
    """Handle tell-waiting command"""
    rpc = Aria2RPC(rpc_url=args.rpc_url, rpc_secret=args.rpc_secret)
    
    result = rpc.tell_waiting(offset=args.offset, num=args.num)
    
    if result is None:
        sys.exit(1)
    
    tasks = result if isinstance(result, list) else []
    
    if not tasks:
        print("No waiting downloads")
    else:
        print(f"Waiting downloads: {len(tasks)}")
        print("=" * 60)
        for task in tasks:
            print_task(task, verbose=args.verbose)


def cmd_tell_stopped(args):
    """Handle tell-stopped command"""
    rpc = Aria2RPC(rpc_url=args.rpc_url, rpc_secret=args.rpc_secret)
    
    result = rpc.tell_stopped(offset=args.offset, num=args.num)
    
    if result is None:
        sys.exit(1)
    
    tasks = result if isinstance(result, list) else []
    
    if not tasks:
        print("No stopped downloads")
    else:
        print(f"Stopped downloads: {len(tasks)}")
        print("=" * 60)
        for task in tasks:
            print_task(task, verbose=args.verbose)


def cmd_tell_status(args):
    """Handle tell-status command"""
    rpc = Aria2RPC(rpc_url=args.rpc_url, rpc_secret=args.rpc_secret)
    
    result = rpc.tell_status(args.gid)
    
    if result is None:
        sys.exit(1)
    
    print_task(result, verbose=True)
    
    # Show error message if present
    if result.get('errorMessage'):
        print(f"\n⚠ Error: {result['errorMessage']}")


def cmd_pause(args):
    """Handle pause command"""
    rpc = Aria2RPC(rpc_url=args.rpc_url, rpc_secret=args.rpc_secret)
    
    if args.force:
        result = rpc.force_pause(args.gid)
    else:
        result = rpc.pause(args.gid)
    
    if result:
        print(f"✓ Download paused: {args.gid}")
    else:
        sys.exit(1)


def cmd_unpause(args):
    """Handle unpause command"""
    rpc = Aria2RPC(rpc_url=args.rpc_url, rpc_secret=args.rpc_secret)
    
    result = rpc.unpause(args.gid)
    
    if result:
        print(f"✓ Download resumed: {args.gid}")
    else:
        sys.exit(1)


def cmd_remove(args):
    """Handle remove command"""
    rpc = Aria2RPC(rpc_url=args.rpc_url, rpc_secret=args.rpc_secret)
    
    if args.force:
        result = rpc.force_remove(args.gid)
    else:
        result = rpc.remove(args.gid)
    
    if result:
        print(f"✓ Download removed: {args.gid}")
    else:
        sys.exit(1)


def cmd_global_stat(args):
    """Handle global-stat command"""
    rpc = Aria2RPC(rpc_url=args.rpc_url, rpc_secret=args.rpc_secret)
    
    result = rpc.get_global_stat()
    
    if result is None:
        sys.exit(1)
    
    print("aria2 Global Statistics")
    print("=" * 60)
    print(f"Active downloads:      {result.get('numActive', 0)}")
    print(f"Waiting downloads:     {result.get('numWaiting', 0)}")
    print(f"Stopped downloads:     {result.get('numStopped', 0)}")
    print(f"Total speed:           ↓ {format_speed(int(result.get('downloadSpeed', 0)))}")
    print(f"                       ↑ {format_speed(int(result.get('uploadSpeed', 0)))}")
    print(f"Files downloaded:      {result.get('numFilesDownloaded', 0)}")
    print(f"Files removed:         {result.get('numFilesRemoved', 0)}")
    
    # Get version
    version_result = rpc.get_version()
    if version_result:
        version = version_result.get('version', 'N/A')
        enabled_features = version_result.get('enabledFeatures', [])
        print(f"\naria2 Version:       {version}")
        if enabled_features:
            print(f"Enabled Features:    {', '.join(enabled_features[:5])}...")


def cmd_get_global_option(args):
    """Handle get-global-option command"""
    rpc = Aria2RPC(rpc_url=args.rpc_url, rpc_secret=args.rpc_secret)
    
    result = rpc.get_global_option()
    
    if result is None:
        sys.exit(1)
    
    print("aria2 Global Options")
    print("=" * 60)
    
    # Filter and display options
    if args.key:
        # Show specific option
        if args.key in result:
            print(f"{args.key}: {result[args.key]}")
        else:
            print(f"Option '{args.key}' not found")
            print(f"\nAvailable options:")
            for key in sorted(result.keys()):
                print(f"  {key}")
            sys.exit(1)
    else:
        # Show all options
        if args.verbose:
            # Show all options
            for key in sorted(result.keys()):
                print(f"{key}: {result[key]}")
        else:
            # Show common options only
            common_options = [
                'max-concurrent-downloads',
                'max-overall-download-limit',
                'max-overall-upload-limit',
                'max-connection-per-server',
                'split',
                'min-split-size',
                'dir',
                'continue',
                'enable-rpc',
                'rpc-listen-port',
                'save-session',
                'user-agent',
                'all-proxy',
                'timeout',
                'connect-timeout',
                'max-tries',
                'retry-wait'
            ]
            
            print("Common Options:")
            print("-" * 60)
            for key in common_options:
                if key in result:
                    value = result[key]
                    # Truncate long values
                    if len(str(value)) > 50:
                        value = str(value)[:47] + "..."
                    print(f"{key}: {value}")
            
            print(f"\nTotal: {len(result)} options")
            print("\nUse --verbose to show all options")
            print("Use --key <option-name> to show specific option")


def cmd_set_global_option(args):
    """Handle set-global-option command"""
    rpc = Aria2RPC(rpc_url=args.rpc_url, rpc_secret=args.rpc_secret)
    
    # Parse options
    options = {}
    if args.options:
        for opt in args.options:
            if '=' in opt:
                key, value = opt.split('=', 1)
                options[key] = value
            else:
                print(f"Error: Invalid option format '{opt}'. Use KEY=VALUE", file=sys.stderr)
                sys.exit(1)
    
    if not options:
        print("Error: No options specified. Use KEY=VALUE format", file=sys.stderr)
        print("Example: --set max-overall-download-limit=10M", file=sys.stderr)
        sys.exit(1)
    
    result = rpc.change_global_option(options)
    
    if result:
        print("✓ Global options updated successfully")
        print("\nChanged options:")
        for key, value in options.items():
            print(f"  {key}: {value}")
    else:
        sys.exit(1)


def cmd_get_option(args):
    """Handle get-option command"""
    rpc = Aria2RPC(rpc_url=args.rpc_url, rpc_secret=args.rpc_secret)
    
    result = rpc.get_option(args.gid)
    
    if result is None:
        sys.exit(1)
    
    print(f"Options for Task: {args.gid}")
    print("=" * 60)
    
    if args.verbose:
        # Show all options
        for key in sorted(result.keys()):
            print(f"{key}: {result[key]}")
    else:
        # Show common options only
        common_options = [
            'dir',
            'out',
            'split',
            'min-split-size',
            'max-connection-per-server',
            'dry-run',
            'resume',
            'all-proxy',
            'user-agent',
            'timeout',
            'connect-timeout'
        ]
        
        print("Common Options:")
        print("-" * 60)
        for key in common_options:
            if key in result:
                value = result[key]
                if len(str(value)) > 60:
                    value = str(value)[:57] + "..."
                print(f"{key}: {value}")
        
        print(f"\nTotal: {len(result)} options")
        print("\nUse --verbose to show all options")


def cmd_set_option(args):
    """Handle set-option command"""
    rpc = Aria2RPC(rpc_url=args.rpc_url, rpc_secret=args.rpc_secret)
    
    # Parse options
    options = {}
    if args.options:
        for opt in args.options:
            if '=' in opt:
                key, value = opt.split('=', 1)
                options[key] = value
            else:
                print(f"Error: Invalid option format '{opt}'. Use KEY=VALUE", file=sys.stderr)
                sys.exit(1)
    
    if not options:
        print("Error: No options specified. Use KEY=VALUE format", file=sys.stderr)
        print("Example: --set split=16", file=sys.stderr)
        sys.exit(1)
    
    result = rpc.change_option(args.gid, options)
    
    if result:
        print(f"✓ Options updated for task: {args.gid}")
        print("\nChanged options:")
        for key, value in options.items():
            print(f"  {key}: {value}")
    else:
        sys.exit(1)


def main():
    parser = argparse.ArgumentParser(
        description='aria2-rpc: Remote control for aria2 download service',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Environment Variables:
  ARIA2_RPC_URL     RPC endpoint URL (default: http://localhost:6800/jsonrpc)
  ARIA2_RPC_SECRET  RPC secret token (optional)

Examples:
  %(prog)s add-uri http://example.com/file.zip
  %(prog)s add-uri http://mirror1/file.zip http://mirror2/file.zip --dir /downloads
  %(prog)s tell-active
  %(prog)s tell-status abc12345
  %(prog)s pause abc12345
  %(prog)s remove abc12345
  %(prog)s global-stat
        """
    )
    
    parser.add_argument('--rpc-url', help='RPC endpoint URL')
    parser.add_argument('--rpc-secret', help='RPC secret token')
    parser.add_argument('-v', '--verbose', action='store_true', help='Verbose output')
    
    subparsers = parser.add_subparsers(dest='command', help='Commands')
    
    # add-uri command
    add_uri_parser = subparsers.add_parser('add-uri', help='Add HTTP/FTP/Magnet download')
    add_uri_parser.add_argument('uris', nargs='+', help='URIs to download')
    add_uri_parser.add_argument('--dir', help='Download directory')
    add_uri_parser.add_argument('--out', help='Output filename')
    add_uri_parser.add_argument('-s', '--split', type=int, help='Number of connections')
    add_uri_parser.add_argument('-x', '--max-connections', type=int, help='Max connections per server')
    add_uri_parser.set_defaults(func=cmd_add_uri)
    
    # tell-active command
    tell_active_parser = subparsers.add_parser('tell-active', help='Get active downloads')
    tell_active_parser.set_defaults(func=cmd_tell_active)
    
    # tell-waiting command
    tell_waiting_parser = subparsers.add_parser('tell-waiting', help='Get waiting downloads')
    tell_waiting_parser.add_argument('--offset', type=int, default=0, help='Offset')
    tell_waiting_parser.add_argument('--num', type=int, default=1000, help='Number of results')
    tell_waiting_parser.set_defaults(func=cmd_tell_waiting)
    
    # tell-stopped command
    tell_stopped_parser = subparsers.add_parser('tell-stopped', help='Get stopped downloads')
    tell_stopped_parser.add_argument('--offset', type=int, default=0, help='Offset')
    tell_stopped_parser.add_argument('--num', type=int, default=1000, help='Number of results')
    tell_stopped_parser.set_defaults(func=cmd_tell_stopped)
    
    # tell-status command
    tell_status_parser = subparsers.add_parser('tell-status', help='Get task status by GID')
    tell_status_parser.add_argument('gid', help='Task GID')
    tell_status_parser.set_defaults(func=cmd_tell_status)
    
    # pause command
    pause_parser = subparsers.add_parser('pause', help='Pause download')
    pause_parser.add_argument('gid', help='Task GID')
    pause_parser.add_argument('-f', '--force', action='store_true', help='Force pause')
    pause_parser.set_defaults(func=cmd_pause)
    
    # unpause command
    unpause_parser = subparsers.add_parser('unpause', help='Resume download')
    unpause_parser.add_argument('gid', help='Task GID')
    unpause_parser.set_defaults(func=cmd_unpause)
    
    # remove command
    remove_parser = subparsers.add_parser('remove', help='Remove download')
    remove_parser.add_argument('gid', help='Task GID')
    remove_parser.add_argument('-f', '--force', action='store_true', help='Force remove')
    remove_parser.set_defaults(func=cmd_remove)
    
    # global-stat command
    global_stat_parser = subparsers.add_parser('global-stat', help='Get global statistics')
    global_stat_parser.set_defaults(func=cmd_global_stat)
    
    # get-global-option command
    get_global_opt_parser = subparsers.add_parser('get-global-option', help='Get global options')
    get_global_opt_parser.add_argument('--key', help='Show specific option key')
    get_global_opt_parser.set_defaults(func=cmd_get_global_option)
    
    # set-global-option command
    set_global_opt_parser = subparsers.add_parser('set-global-option', help='Set global options')
    set_global_opt_parser.add_argument('options', nargs='*', help='Options in KEY=VALUE format')
    set_global_opt_parser.set_defaults(func=cmd_set_global_option)
    
    # get-option command
    get_opt_parser = subparsers.add_parser('get-option', help='Get options for a task')
    get_opt_parser.add_argument('gid', help='Task GID')
    get_opt_parser.set_defaults(func=cmd_get_option)
    
    # set-option command
    set_opt_parser = subparsers.add_parser('set-option', help='Set options for a task')
    set_opt_parser.add_argument('gid', help='Task GID')
    set_opt_parser.add_argument('options', nargs='*', help='Options in KEY=VALUE format')
    set_opt_parser.set_defaults(func=cmd_set_option)
    
    args = parser.parse_args()
    
    if not args.command:
        parser.print_help()
        sys.exit(1)
    
    args.func(args)


if __name__ == '__main__':
    main()


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)