Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ A unified download manager for aMule, rTorrent, qBittorrent, Deluge, and Transmi

### Multi-Client Support
- **aMule Integration** - Control aMule via the EC (External Connection) protocol
- **Rucio Integration** - Connect to the Rucio P2P daemon (libp2p + eMule/Kad) via its REST API
- **rTorrent Integration** - Connect to rTorrent via XML-RPC (HTTP proxy, SCGI TCP, or Unix socket)
- **qBittorrent Integration** - Connect to qBittorrent via WebUI API
- **Deluge Integration** - Connect to Deluge via WebUI JSON-RPC
Expand Down Expand Up @@ -124,6 +125,7 @@ Open `http://localhost:4000` and complete the setup wizard.
|----------|-------------|
| [Configuration Guide](./docs/CONFIGURATION.md) | Setup wizard, settings, environment variables |
| [aMule Integration](./docs/AMULE.md) | Connect to aMule via EC protocol |
| [Rucio Integration](./docs/RUCIO.md) | Connect to Rucio via its REST API (P2P, libp2p + eMule/Kad) |
| [rTorrent Integration](./docs/RTORRENT.md) | Connect to rTorrent via XML-RPC (HTTP, SCGI, or Unix socket) |
| [qBittorrent Integration](./docs/QBITTORRENT.md) | Connect to qBittorrent via WebUI API |
| [Deluge Integration](./docs/DELUGE.md) | Connect to Deluge via WebUI JSON-RPC |
Expand Down
168 changes: 168 additions & 0 deletions docs/RUCIO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# Rucio Integration

aMuTorrent connects to [Rucio](https://github.com/ogarcia/rucio) over its REST API, letting you search the network, manage downloads and shared files, and organize everything with categories — all from the same dashboard you use for your other clients.

Rucio is a P2P daemon built on libp2p and BLAKE3, with eMule/Kad compatibility. It is treated as its own network in aMuTorrent (separate from aMule), with its own filter toggle, chart series and status badge.

## Requirements

- A running Rucio daemon (`ruciod`) with its REST API reachable from aMuTorrent
- The API port (default: `3003`)

> **No authentication:** Rucio's API has no built-in authentication — access control is meant to be delegated to a reverse proxy. If you put Rucio behind HTTP basic auth, aMuTorrent can pass credentials (see Username/Password below).

## Rucio API Setup

The REST API is always served by the daemon; you only need to make sure it listens on an address aMuTorrent can reach.

By default the daemon binds to `127.0.0.1:3003`, which is **only reachable from the same host**. If aMuTorrent runs in a container or on another machine, bind the API to all interfaces:

```bash
RUCIOD_API_LISTEN=0.0.0.0:3003 ruciod
```

Or in Rucio's `config.toml`:

```toml
[api]
listen = "0.0.0.0:3003"
```

> When exposing the API beyond localhost, put Rucio behind a reverse proxy and restrict access there — the daemon itself does not authenticate requests.

## Configuration

### Via Settings UI

1. Go to **Settings** → **Download Clients** and click **Add Client**
2. Choose **Rucio**
3. Configure connection settings:
- **Host**: Rucio daemon hostname or IP (e.g., `localhost`, `rucio`, or `host.docker.internal`)
- **Port**: API port (default: `3003`)
- **Base Path** *(optional)*: set this only when the daemon is served under a sub-path behind a reverse proxy (e.g., `/rucio`)
- **Username / Password** *(optional)*: only if the daemon is behind HTTP basic auth
- **Use SSL (HTTPS)**: enable when connecting through an HTTPS reverse proxy

### Via Environment Variables

```bash
RUCIO_ENABLED=true
RUCIO_HOST=localhost
RUCIO_PORT=3003
RUCIO_USE_SSL=false
RUCIO_BASE_PATH=
RUCIO_USERNAME=
RUCIO_PASSWORD=
```

### Via config.json

```json
{
"rucio": {
"enabled": true,
"host": "localhost",
"port": 3003,
"useSsl": false
}
}
```

## Docker Compose Example

### Rucio on Host Machine

```yaml
services:
amutorrent:
image: g0t3nks/amutorrent:latest
environment:
- RUCIO_ENABLED=true
- RUCIO_HOST=host.docker.internal
- RUCIO_PORT=3003
extra_hosts:
- "host.docker.internal:host-gateway"
ports:
- "4000:4000"
```

Make sure the daemon on the host binds to a reachable address (`RUCIOD_API_LISTEN=0.0.0.0:3003`).

### Rucio in Docker Container

```yaml
services:
rucio:
image: ghcr.io/ogarcia/rucio:latest
container_name: rucio
environment:
- RUCIOD_API_LISTEN=0.0.0.0:3003
ports:
- "4321:4321" # P2P (libp2p)
- "4321:4321/udp"
volumes:
- ./data/rucio:/data
restart: unless-stopped

amutorrent:
image: g0t3nks/amutorrent:latest
environment:
- RUCIO_ENABLED=true
- RUCIO_HOST=rucio
- RUCIO_PORT=3003
ports:
- "4000:4000"
restart: unless-stopped
```

> The Rucio API port (`3003`) does **not** need to be published to the host — aMuTorrent reaches it over the Docker network. Only publish it if you also want to access Rucio's own web UI directly.

## Features

### Search

Searches against Rucio run on the **unified search** alongside your other clients and surface in the same results view. Pick Rucio as the search target from the search widget; results can be queued straight to Rucio.

### Downloads

Add downloads via `rucio:` magnets or ED2K links, and pause, resume, rename, cancel and delete them from the UI like any other client.

### Shared Files

Rucio's shared files appear in the **Shared Files** view. Unsharing a file in aMuTorrent removes it from Rucio's share list without deleting the file on disk.

### Categories

Categories created in aMuTorrent are synced to Rucio, including their **color** and **download directory**. Editing a category in aMuTorrent updates it in Rucio, and downloads can be assigned to a category from the UI.

## Reverse Proxy / Sub-path

To serve Rucio under a sub-path (e.g., `https://example.com/rucio`), set `RUCIOD_BASE_PATH=/rucio/` on the daemon and enter `/rucio` as the **Base Path** in aMuTorrent. Enable **Use SSL** if the proxy terminates HTTPS.

## Troubleshooting

### Connection Failed

- Verify the daemon is running and the API is listening: `curl http://host:3003/health`
- If aMuTorrent runs in a container, the daemon must bind to `0.0.0.0` (not the default `127.0.0.1`) — set `RUCIOD_API_LISTEN=0.0.0.0:3003`
- Check the host/port and any firewall rules between aMuTorrent and Rucio

### Docker: Can't reach Rucio on host

- Add `extra_hosts` to your docker-compose.yml:
```yaml
extra_hosts:
- "host.docker.internal:host-gateway"
```
- Use `host.docker.internal` as the Rucio host

### 401 / Authentication errors

- Rucio has no built-in auth — a 401 comes from a reverse proxy in front of it
- Enter the proxy's basic-auth Username/Password in the client settings

### Downloads or Shared Files Not Appearing

- Ensure the Rucio client is enabled in Settings
- Check the aMuTorrent logs for connection errors
- Confirm Rucio has active downloads or shared files of its own
61 changes: 61 additions & 0 deletions server/lib/clientMeta.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,67 @@ const CLIENT_TYPES = {
customSavePath: false // ed2k uses category paths only
}
},
rucio: {
// Rucio is its own P2P network (libp2p, BLAKE3) that also bridges eMule/Kad.
// It gets its own networkType so it is a first-class entity everywhere
// (charts, filters, metrics, footer) rather than being conflated with aMule.
// Its capability profile still matches aMule (search + shared files +
// categories, no trackers, single-file); the unified item builder applies
// the source-based (non-BitTorrent) shape to it the same as ed2k.
networkType: 'rucio',
displayName: 'Rucio',
metricsPrefix: 'ru_', // ru_upload_speed, ru_total_uploaded
hashLength: 64, // BLAKE3 root hash (eMule MD4 results are 32; only used by demo data)
statusField: 'state', // resolveStatus reads the daemon's `state`
// Accept both casings: Rucio ≤0.32 serialized DownloadState in PascalCase
// (a missing serde rename_all, since fixed to snake_case to match the rest
// of its API). Keeping both keys makes the integration version-agnostic.
statusMap: {
'finding_providers': 'active', 'FindingProviders': 'active',
'queued': 'active', 'Queued': 'active',
'downloading': 'active', 'Downloading': 'active',
'stalled': 'active', 'Stalled': 'active',
'paused': 'paused', 'Paused': 'paused',
'completed': 'seeding', 'Completed': 'seeding', // completed files are seeded back
'failed': 'error', 'Failed': 'error',
'cancelled': 'stopped', 'Cancelled': 'stopped'
},
connectionDefaults: {
host: '', port: 3003, useSsl: false, basePath: '', username: '', password: ''
},
defaults: {
downloadPriority: null,
partStatus: null,
gapStatus: null,
reqStatus: null,
lastSeenComplete: 0,
ed2kLink: null,
addedAt: null
},
capabilities: {
nativeMove: false, // dir is category-driven, no move-by-path API
categoryChangeAutoMoves: false,
multiFile: false, // one file per root hash
sharedFiles: true, // has a shared-files concept (GET /shares/files)
sharedMeansComplete: true, // a shared file is a complete file
removeSharedMustDeleteFiles: false, // can un-share via API without deleting the file
moveSharedForCategoryChange: false,
refreshSharedAfterMove: false,
moveActiveDownloads: false,
pauseBeforeMove: false,
trackers: false, // DHT/libp2p, no trackers
search: true, // unified rucio + eMule/Kad search
cancelDeletesFiles: true, // cancel discards the partial download
apiDeletesFiles: false, // deleting from the list never wipes the on-disk file
refreshSharedAfterDelete: false,
categories: true, // full category CRUD in the daemon
logs: false,
renameFile: true, // can rename a not-yet-complete download
fileRatingComment: false,
customSavePath: false // path follows the category, not per-download
},
seedingStatuses: ['completed', 'Completed']
},
rtorrent: {
networkType: 'bittorrent',
displayName: 'rTorrent',
Expand Down
33 changes: 33 additions & 0 deletions server/lib/configTester.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const RtorrentHandler = require('./rtorrent/RtorrentHandler');
const QBittorrentClient = require('./qbittorrent/QBittorrentClient');
const DelugeClient = require('./deluge/DelugeClient');
const TransmissionClient = require('./transmission/TransmissionClient');
const RucioClient = require('./rucio/RucioClient');
const ProwlarrHandler = require('./prowlarr/ProwlarrHandler');
const { checkDirectoryAccess } = require('./pathUtils');
const logger = require('./logger');
Expand Down Expand Up @@ -671,10 +672,42 @@ async function testTransmissionConnection(host, port, username, password, useSsl
}
}

/**
* Test a Rucio daemon connection via its /health endpoint.
* @param {string} host
* @param {number} port
* @param {boolean} [useSsl]
* @param {string} [basePath] - sub-path behind a reverse proxy
* @param {string} [username] - optional HTTP basic auth
* @param {string} [password] - optional HTTP basic auth
* @returns {Promise<{success, connected, version, error, message}>}
*/
async function testRucioConnection(host, port, useSsl, basePath, username, password) {
const result = { success: false, connected: false, version: null, error: null };
try {
const client = new RucioClient({ host, port, useSsl, basePath, username, password, timeoutMs: 10000 });
const r = await client.testConnection();
if (r.success) {
result.connected = true;
result.success = true;
result.version = r.version;
result.message = `Connected to Rucio ${r.version}`;
} else {
result.error = r.error;
result.message = `Connection failed: ${r.error}`;
}
} catch (err) {
result.error = err.message;
result.message = `Connection failed: ${err.message}`;
}
return result;
}

module.exports = {
testDirectoryAccess,
testGeoIPDatabase,
testAmuleConnection,
testRucioConnection,
testRtorrentConnection,
testQbittorrentConnection,
testDelugeConnection,
Expand Down
77 changes: 77 additions & 0 deletions server/lib/downloadNormalizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,81 @@ function normalizeTransmissionDownload(torrent) {
};
}

// ============================================================================
// RUCIO NORMALIZERS
// ============================================================================

// Rucio is modelled under the ed2k networkType, so these emit the field names
// the ed2k branch of unifiedItemBuilder reads (category id + categoryName,
// sourceCount/sourceCountXfer, ed2kLink, state for the status map).

/**
* Normalize a Rucio download (GET /api/v1/downloads item) to unified format.
* @param {Object} d - raw Rucio download
* @param {Function} resolveCategoryName - (categoryId) => category name string
* @returns {Object} Normalized download
*/
function normalizeRucioDownload(d, resolveCategoryName = () => 'Default') {
const size = d.size || 0;
const downloaded = d.bytes_done || 0;
return {
clientType: 'rucio',
hash: d.root_hash,
name: d.name || '',
rawName: d.name || '',
size,
downloaded,
progress: size > 0 ? Math.round((downloaded / size) * 100) : 0,
speed: d.speed_bps || 0,
// Accept both casings (see statusMap note in clientMeta.js)
isComplete: d.state === 'completed' || d.state === 'Completed',
// Drives resolveStatus() via clientMeta statusField: 'state'
state: d.state,

// Organization (ed2k branch reads `category` as the id + `categoryName`)
category: d.category_id ?? null,
categoryName: resolveCategoryName(d.category_id),

// Sources
sourceCount: d.sources_total || 0,
sourceCountXfer: d.sources_active || 0,
sourceCountA4AF: 0,
sourceCountNotCurrent: 0,

// No segment/part visualization data from the daemon (yet)
priority: null,
partStatus: null,
gapStatus: null,
reqStatus: null,

// The "copy link" value — rucio: or ed2k: depending on the source network
ed2kLink: d.link || null,

raw: { clientType: 'rucio', ...d }
};
}

/**
* Normalize a Rucio shared file (GET /api/v1/shares/files item) to unified
* format. Shared files are complete files the node is providing.
* @param {Object} s - raw Rucio shared file
* @returns {Object} Normalized shared file
*/
function normalizeRucioSharedFile(s) {
return {
clientType: 'rucio',
hash: s.root_hash,
name: s.name || '',
rawName: s.name || '',
size: s.size || 0,
uploadSpeed: 0,
// ed2k branch marks shared files complete/seeding and uses these
ed2kLink: s.magnet || null,
path: s.path || null,
raw: { clientType: 'rucio', ...s }
};
}

module.exports = {
normalizeAmuleDownload,
normalizeAmuleSharedFile,
Expand All @@ -645,5 +720,7 @@ module.exports = {
normalizeQBittorrentDownload,
normalizeDelugeDownload,
normalizeTransmissionDownload,
normalizeRucioDownload,
normalizeRucioSharedFile,
extractTrackerDomain
};
Loading