547 lines
16 KiB
Go
547 lines
16 KiB
Go
package aria2
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"sync/atomic"
|
|
)
|
|
|
|
var (
|
|
ErrInvalidURL = errors.New("aria2: invalid URL")
|
|
ErrRPCFailed = errors.New("aria2: RPC call failed")
|
|
ErrInvalidResponse = errors.New("aria2: invalid response")
|
|
)
|
|
|
|
// Client represents an aria2 JSON-RPC client
|
|
type Client struct {
|
|
url string
|
|
secret string
|
|
client *http.Client
|
|
id atomic.Int64
|
|
}
|
|
|
|
// rpcRequest represents a JSON-RPC 2.0 request
|
|
type rpcRequest struct {
|
|
Jsonrpc string `json:"jsonrpc"`
|
|
ID string `json:"id"`
|
|
Method string `json:"method"`
|
|
Params []any `json:"params"`
|
|
}
|
|
|
|
// rpcResponse represents a JSON-RPC 2.0 response
|
|
type rpcResponse struct {
|
|
Jsonrpc string `json:"jsonrpc"`
|
|
ID string `json:"id"`
|
|
Result json.RawMessage `json:"result,omitempty"`
|
|
Error *rpcError `json:"error,omitempty"`
|
|
}
|
|
|
|
// rpcError represents a JSON-RPC 2.0 error
|
|
type rpcError struct {
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
func (e *rpcError) Error() string {
|
|
return fmt.Sprintf("aria2 RPC error %d: %s", e.Code, e.Message)
|
|
}
|
|
|
|
// Options for download
|
|
type Options map[string]any
|
|
|
|
// Status represents the status of a download
|
|
type Status struct {
|
|
GID string `json:"gid"`
|
|
Status string `json:"status"`
|
|
TotalLength string `json:"totalLength"`
|
|
CompletedLength string `json:"completedLength"`
|
|
UploadLength string `json:"uploadLength"`
|
|
Bitfield string `json:"bitfield,omitempty"`
|
|
DownloadSpeed string `json:"downloadSpeed"`
|
|
UploadSpeed string `json:"uploadSpeed"`
|
|
InfoHash string `json:"infoHash,omitempty"`
|
|
NumSeeders string `json:"numSeeders,omitempty"`
|
|
Seeder string `json:"seeder,omitempty"`
|
|
PieceLength string `json:"pieceLength,omitempty"`
|
|
NumPieces string `json:"numPieces,omitempty"`
|
|
Connections string `json:"connections"`
|
|
ErrorCode string `json:"errorCode,omitempty"`
|
|
ErrorMessage string `json:"errorMessage,omitempty"`
|
|
FollowedBy []string `json:"followedBy,omitempty"`
|
|
Following string `json:"following,omitempty"`
|
|
BelongsTo string `json:"belongsTo,omitempty"`
|
|
Dir string `json:"dir"`
|
|
Files []File `json:"files"`
|
|
BitTorrent struct {
|
|
AnnounceList [][]string `json:"announceList,omitempty"`
|
|
Comment string `json:"comment,omitempty"`
|
|
CreationDate int64 `json:"creationDate,omitempty"`
|
|
Mode string `json:"mode,omitempty"`
|
|
Info struct {
|
|
Name string `json:"name,omitempty"`
|
|
} `json:"info"`
|
|
} `json:"bittorrent"`
|
|
VerifiedLength string `json:"verifiedLength,omitempty"`
|
|
VerifyIntegrityPending string `json:"verifyIntegrityPending,omitempty"`
|
|
}
|
|
|
|
// File represents a file in the download
|
|
type File struct {
|
|
Index string `json:"index"`
|
|
Path string `json:"path"`
|
|
Length string `json:"length"`
|
|
CompletedLength string `json:"completedLength"`
|
|
Selected string `json:"selected"`
|
|
URIs []URI `json:"uris"`
|
|
}
|
|
|
|
// URI represents a URI for a file
|
|
type URI struct {
|
|
URI string `json:"uri"`
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
// GlobalStat represents global statistics
|
|
type GlobalStat struct {
|
|
DownloadSpeed string `json:"downloadSpeed"`
|
|
UploadSpeed string `json:"uploadSpeed"`
|
|
NumActive string `json:"numActive"`
|
|
NumWaiting string `json:"numWaiting"`
|
|
NumStopped string `json:"numStopped"`
|
|
NumStoppedTotal string `json:"numStoppedTotal"`
|
|
}
|
|
|
|
// Version represents aria2 version information
|
|
type Version struct {
|
|
Version string `json:"version"`
|
|
EnabledFeatures []string `json:"enabledFeatures"`
|
|
}
|
|
|
|
// NewClient creates a new aria2 client
|
|
// url: aria2 RPC URL (e.g., "http://localhost:6800/jsonrpc")
|
|
// secret: aria2 RPC secret token (optional, use empty string if not set)
|
|
func NewClient(url, secret string) (*Client, error) {
|
|
if url == "" {
|
|
return nil, ErrInvalidURL
|
|
}
|
|
|
|
return &Client{
|
|
url: url,
|
|
secret: secret,
|
|
client: &http.Client{},
|
|
}, nil
|
|
}
|
|
|
|
// NewClientWithHTTPClient creates a new aria2 client with custom HTTP client
|
|
func NewClientWithHTTPClient(url, secret string, httpClient *http.Client) (*Client, error) {
|
|
if url == "" {
|
|
return nil, ErrInvalidURL
|
|
}
|
|
|
|
if httpClient == nil {
|
|
httpClient = &http.Client{}
|
|
}
|
|
|
|
return &Client{
|
|
url: url,
|
|
secret: secret,
|
|
client: httpClient,
|
|
}, nil
|
|
}
|
|
|
|
// call makes a JSON-RPC call to aria2
|
|
func (c *Client) call(ctx context.Context, method string, params []any, result any) error {
|
|
// Prepare params with secret token if set
|
|
var rpcParams []any
|
|
if c.secret != "" {
|
|
rpcParams = append([]any{fmt.Sprintf("token:%s", c.secret)}, params...)
|
|
} else {
|
|
rpcParams = params
|
|
}
|
|
|
|
// Create request
|
|
reqID := fmt.Sprintf("%d", c.id.Add(1))
|
|
req := &rpcRequest{
|
|
Jsonrpc: "2.0",
|
|
ID: reqID,
|
|
Method: method,
|
|
Params: rpcParams,
|
|
}
|
|
|
|
// Marshal request
|
|
reqBody, err := json.Marshal(req)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: failed to marshal request: %v", ErrRPCFailed, err)
|
|
}
|
|
|
|
// Create HTTP request
|
|
httpReq, err := http.NewRequestWithContext(ctx, "POST", c.url, bytes.NewReader(reqBody))
|
|
if err != nil {
|
|
return fmt.Errorf("%w: failed to create request: %v", ErrRPCFailed, err)
|
|
}
|
|
|
|
httpReq.Header.Set("Content-Type", "application/json")
|
|
|
|
// Send request
|
|
resp, err := c.client.Do(httpReq)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: failed to send request: %v", ErrRPCFailed, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Read response
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: failed to read response: %v", ErrRPCFailed, err)
|
|
}
|
|
|
|
// Check HTTP status
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("%w: HTTP %d: %s", ErrRPCFailed, resp.StatusCode, string(body))
|
|
}
|
|
|
|
// Parse response
|
|
var rpcResp rpcResponse
|
|
if err := json.Unmarshal(body, &rpcResp); err != nil {
|
|
return fmt.Errorf("%w: failed to unmarshal response: %v", ErrInvalidResponse, err)
|
|
}
|
|
|
|
// Check for RPC error
|
|
if rpcResp.Error != nil {
|
|
return rpcResp.Error
|
|
}
|
|
|
|
// Check response ID
|
|
if rpcResp.ID != reqID {
|
|
return fmt.Errorf("%w: response ID mismatch", ErrInvalidResponse)
|
|
}
|
|
|
|
// Unmarshal result if needed
|
|
if result != nil {
|
|
if err := json.Unmarshal(rpcResp.Result, result); err != nil {
|
|
return fmt.Errorf("%w: failed to unmarshal result: %v", ErrInvalidResponse, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// AddURI adds a new download with URIs
|
|
func (c *Client) AddURI(ctx context.Context, uris []string, options Options) (string, error) {
|
|
var gid string
|
|
params := []any{uris}
|
|
if options != nil {
|
|
params = append(params, options)
|
|
}
|
|
err := c.call(ctx, "aria2.addUri", params, &gid)
|
|
return gid, err
|
|
}
|
|
|
|
// AddTorrent adds a new download with torrent file content
|
|
func (c *Client) AddTorrent(ctx context.Context, torrent []byte, uris []string, options Options) (string, error) {
|
|
var gid string
|
|
params := []any{torrent}
|
|
if len(uris) > 0 {
|
|
params = append(params, uris)
|
|
}
|
|
if options != nil {
|
|
params = append(params, options)
|
|
}
|
|
err := c.call(ctx, "aria2.addTorrent", params, &gid)
|
|
return gid, err
|
|
}
|
|
|
|
// AddMetalink adds a new download with metalink file content
|
|
func (c *Client) AddMetalink(ctx context.Context, metalink []byte, options Options) ([]string, error) {
|
|
var gids []string
|
|
params := []any{metalink}
|
|
if options != nil {
|
|
params = append(params, options)
|
|
}
|
|
err := c.call(ctx, "aria2.addMetalink", params, &gids)
|
|
return gids, err
|
|
}
|
|
|
|
// Remove removes the download denoted by gid
|
|
func (c *Client) Remove(ctx context.Context, gid string) (string, error) {
|
|
var result string
|
|
err := c.call(ctx, "aria2.remove", []any{gid}, &result)
|
|
return result, err
|
|
}
|
|
|
|
// ForceRemove removes the download denoted by gid forcefully
|
|
func (c *Client) ForceRemove(ctx context.Context, gid string) (string, error) {
|
|
var result string
|
|
err := c.call(ctx, "aria2.forceRemove", []any{gid}, &result)
|
|
return result, err
|
|
}
|
|
|
|
// Pause pauses the download denoted by gid
|
|
func (c *Client) Pause(ctx context.Context, gid string) (string, error) {
|
|
var result string
|
|
err := c.call(ctx, "aria2.pause", []any{gid}, &result)
|
|
return result, err
|
|
}
|
|
|
|
// PauseAll pauses all downloads
|
|
func (c *Client) PauseAll(ctx context.Context) (string, error) {
|
|
var result string
|
|
err := c.call(ctx, "aria2.pauseAll", []any{}, &result)
|
|
return result, err
|
|
}
|
|
|
|
// ForcePause pauses the download denoted by gid forcefully
|
|
func (c *Client) ForcePause(ctx context.Context, gid string) (string, error) {
|
|
var result string
|
|
err := c.call(ctx, "aria2.forcePause", []any{gid}, &result)
|
|
return result, err
|
|
}
|
|
|
|
// ForcePauseAll pauses all downloads forcefully
|
|
func (c *Client) ForcePauseAll(ctx context.Context) (string, error) {
|
|
var result string
|
|
err := c.call(ctx, "aria2.forcePauseAll", []any{}, &result)
|
|
return result, err
|
|
}
|
|
|
|
// Unpause unpauses the download denoted by gid
|
|
func (c *Client) Unpause(ctx context.Context, gid string) (string, error) {
|
|
var result string
|
|
err := c.call(ctx, "aria2.unpause", []any{gid}, &result)
|
|
return result, err
|
|
}
|
|
|
|
// UnpauseAll unpauses all downloads
|
|
func (c *Client) UnpauseAll(ctx context.Context) (string, error) {
|
|
var result string
|
|
err := c.call(ctx, "aria2.unpauseAll", []any{}, &result)
|
|
return result, err
|
|
}
|
|
|
|
// TellStatus returns the progress of the download denoted by gid
|
|
func (c *Client) TellStatus(ctx context.Context, gid string, keys ...string) (*Status, error) {
|
|
var status Status
|
|
params := []any{gid}
|
|
if len(keys) > 0 {
|
|
params = append(params, keys)
|
|
}
|
|
err := c.call(ctx, "aria2.tellStatus", params, &status)
|
|
return &status, err
|
|
}
|
|
|
|
// GetURIs returns the URIs used in the download denoted by gid
|
|
func (c *Client) GetURIs(ctx context.Context, gid string) ([]URI, error) {
|
|
var uris []URI
|
|
err := c.call(ctx, "aria2.getUris", []any{gid}, &uris)
|
|
return uris, err
|
|
}
|
|
|
|
// GetFiles returns the file list of the download denoted by gid
|
|
func (c *Client) GetFiles(ctx context.Context, gid string) ([]File, error) {
|
|
var files []File
|
|
err := c.call(ctx, "aria2.getFiles", []any{gid}, &files)
|
|
return files, err
|
|
}
|
|
|
|
// GetPeers returns a list of peers of the download denoted by gid
|
|
func (c *Client) GetPeers(ctx context.Context, gid string) ([]any, error) {
|
|
var peers []any
|
|
err := c.call(ctx, "aria2.getPeers", []any{gid}, &peers)
|
|
return peers, err
|
|
}
|
|
|
|
// GetServers returns currently connected HTTP(S)/FTP/SFTP servers of the download denoted by gid
|
|
func (c *Client) GetServers(ctx context.Context, gid string) ([]any, error) {
|
|
var servers []any
|
|
err := c.call(ctx, "aria2.getServers", []any{gid}, &servers)
|
|
return servers, err
|
|
}
|
|
|
|
// TellActive returns a list of active downloads
|
|
func (c *Client) TellActive(ctx context.Context, keys ...string) ([]Status, error) {
|
|
var statuses []Status
|
|
params := []any{}
|
|
if len(keys) > 0 {
|
|
params = append(params, keys)
|
|
}
|
|
err := c.call(ctx, "aria2.tellActive", params, &statuses)
|
|
return statuses, err
|
|
}
|
|
|
|
// TellWaiting returns a list of waiting downloads
|
|
func (c *Client) TellWaiting(ctx context.Context, offset, num int, keys ...string) ([]Status, error) {
|
|
var statuses []Status
|
|
params := []any{offset, num}
|
|
if len(keys) > 0 {
|
|
params = append(params, keys)
|
|
}
|
|
err := c.call(ctx, "aria2.tellWaiting", params, &statuses)
|
|
return statuses, err
|
|
}
|
|
|
|
// TellStopped returns a list of stopped downloads
|
|
func (c *Client) TellStopped(ctx context.Context, offset, num int, keys ...string) ([]Status, error) {
|
|
var statuses []Status
|
|
params := []any{offset, num}
|
|
if len(keys) > 0 {
|
|
params = append(params, keys)
|
|
}
|
|
err := c.call(ctx, "aria2.tellStopped", params, &statuses)
|
|
return statuses, err
|
|
}
|
|
|
|
// ChangePosition changes the position of the download denoted by gid
|
|
func (c *Client) ChangePosition(ctx context.Context, gid string, pos int, how string) (int, error) {
|
|
var result int
|
|
err := c.call(ctx, "aria2.changePosition", []any{gid, pos, how}, &result)
|
|
return result, err
|
|
}
|
|
|
|
// ChangeURI changes the URI of the download denoted by gid
|
|
func (c *Client) ChangeURI(ctx context.Context, gid string, fileIndex int, delURIs []string, addURIs []string) ([]int, error) {
|
|
var result []int
|
|
params := []any{gid, fileIndex, delURIs, addURIs}
|
|
err := c.call(ctx, "aria2.changeUri", params, &result)
|
|
return result, err
|
|
}
|
|
|
|
// GetOption returns options of the download denoted by gid
|
|
func (c *Client) GetOption(ctx context.Context, gid string) (Options, error) {
|
|
var options Options
|
|
err := c.call(ctx, "aria2.getOption", []any{gid}, &options)
|
|
return options, err
|
|
}
|
|
|
|
// ChangeOption changes options of the download denoted by gid dynamically
|
|
func (c *Client) ChangeOption(ctx context.Context, gid string, options Options) (string, error) {
|
|
var result string
|
|
err := c.call(ctx, "aria2.changeOption", []any{gid, options}, &result)
|
|
return result, err
|
|
}
|
|
|
|
// GetGlobalOption returns the global options
|
|
func (c *Client) GetGlobalOption(ctx context.Context) (Options, error) {
|
|
var options Options
|
|
err := c.call(ctx, "aria2.getGlobalOption", []any{}, &options)
|
|
return options, err
|
|
}
|
|
|
|
// ChangeGlobalOption changes global options dynamically
|
|
func (c *Client) ChangeGlobalOption(ctx context.Context, options Options) (string, error) {
|
|
var result string
|
|
err := c.call(ctx, "aria2.changeGlobalOption", []any{options}, &result)
|
|
return result, err
|
|
}
|
|
|
|
// GetGlobalStat returns global statistics such as the overall download and upload speed
|
|
func (c *Client) GetGlobalStat(ctx context.Context) (*GlobalStat, error) {
|
|
var stat GlobalStat
|
|
err := c.call(ctx, "aria2.getGlobalStat", []any{}, &stat)
|
|
return &stat, err
|
|
}
|
|
|
|
// PurgeDownloadResult purges completed/error/removed downloads
|
|
func (c *Client) PurgeDownloadResult(ctx context.Context) (string, error) {
|
|
var result string
|
|
err := c.call(ctx, "aria2.purgeDownloadResult", []any{}, &result)
|
|
return result, err
|
|
}
|
|
|
|
// RemoveDownloadResult removes a completed/error/removed download denoted by gid
|
|
func (c *Client) RemoveDownloadResult(ctx context.Context, gid string) (string, error) {
|
|
var result string
|
|
err := c.call(ctx, "aria2.removeDownloadResult", []any{gid}, &result)
|
|
return result, err
|
|
}
|
|
|
|
// GetVersion returns the version of aria2 and the list of enabled features
|
|
func (c *Client) GetVersion(ctx context.Context) (*Version, error) {
|
|
var version Version
|
|
err := c.call(ctx, "aria2.getVersion", []any{}, &version)
|
|
return &version, err
|
|
}
|
|
|
|
// GetSessionInfo returns session information
|
|
func (c *Client) GetSessionInfo(ctx context.Context) (map[string]any, error) {
|
|
var info map[string]any
|
|
err := c.call(ctx, "aria2.getSessionInfo", []any{}, &info)
|
|
return info, err
|
|
}
|
|
|
|
// Shutdown shuts down aria2
|
|
func (c *Client) Shutdown(ctx context.Context) (string, error) {
|
|
var result string
|
|
err := c.call(ctx, "aria2.shutdown", []any{}, &result)
|
|
return result, err
|
|
}
|
|
|
|
// ForceShutdown shuts down aria2 forcefully
|
|
func (c *Client) ForceShutdown(ctx context.Context) (string, error) {
|
|
var result string
|
|
err := c.call(ctx, "aria2.forceShutdown", []any{}, &result)
|
|
return result, err
|
|
}
|
|
|
|
// SaveSession saves the current session to a file
|
|
func (c *Client) SaveSession(ctx context.Context) (string, error) {
|
|
var result string
|
|
err := c.call(ctx, "aria2.saveSession", []any{}, &result)
|
|
return result, err
|
|
}
|
|
|
|
// MultiCall executes multiple method calls in a single request (system.multicall)
|
|
func (c *Client) MultiCall(ctx context.Context, calls []map[string]any) ([]any, error) {
|
|
var results []any
|
|
err := c.call(ctx, "system.multicall", []any{calls}, &results)
|
|
return results, err
|
|
}
|
|
|
|
// ListMethods lists all available RPC methods
|
|
func (c *Client) ListMethods(ctx context.Context) ([]string, error) {
|
|
var methods []string
|
|
err := c.call(ctx, "system.listMethods", []any{}, &methods)
|
|
return methods, err
|
|
}
|
|
|
|
// ListNotifications lists all available RPC notifications
|
|
func (c *Client) ListNotifications(ctx context.Context) ([]string, error) {
|
|
var notifications []string
|
|
err := c.call(ctx, "system.listNotifications", []any{}, ¬ifications)
|
|
return notifications, err
|
|
}
|
|
|
|
// IsDownloadComplete checks if the download is complete
|
|
func (s *Status) IsDownloadComplete() bool {
|
|
return s.Status == "complete"
|
|
}
|
|
|
|
// IsDownloadActive checks if the download is active
|
|
func (s *Status) IsDownloadActive() bool {
|
|
return s.Status == "active"
|
|
}
|
|
|
|
// IsDownloadWaiting checks if the download is waiting
|
|
func (s *Status) IsDownloadWaiting() bool {
|
|
return s.Status == "waiting"
|
|
}
|
|
|
|
// IsDownloadPaused checks if the download is paused
|
|
func (s *Status) IsDownloadPaused() bool {
|
|
return s.Status == "paused"
|
|
}
|
|
|
|
// IsDownloadError checks if the download has an error
|
|
func (s *Status) IsDownloadError() bool {
|
|
return s.Status == "error"
|
|
}
|
|
|
|
// IsDownloadRemoved checks if the download is removed
|
|
func (s *Status) IsDownloadRemoved() bool {
|
|
return s.Status == "removed"
|
|
}
|