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
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ PORT=4000
# AMULE_SHARED_FILES_RELOAD_INTERVAL_HOURS=3
# AMULE_SHARED_DIR_DAT=/home/amule/.aMule/shareddir.dat

# slskd Configuration (Optional)
# SLSKD_ENABLED=true
# SLSKD_HOST=
# SLSKD_PORT=5030
# SLSKD_PATH=
# SLSKD_API_KEY=
# SLSKD_USE_SSL=false

# rTorrent Configuration (Optional)
# Connect to rTorrent for BitTorrent downloads via XML-RPC or SCGI
# At least one download client (aMule, rTorrent, or qBittorrent) must be enabled
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

<h1 align="center">aMuTorrent</h1>

A unified download manager for aMule, rTorrent, qBittorrent, Deluge, and Transmission. Manage ED2K and BitTorrent downloads from a single modern web interface. Features multi-instance support, user management with SSO, Prowlarr integration for torrent search, Torznab indexer and qBittorrent-compatible API for aMule (Sonarr/Radarr integration), push notifications via Apprise, and GeoIP peer location display. Built with Node.js, WebSockets, and React.
A unified download manager for aMule, rTorrent, qBittorrent, Deluge, Transmission, and slskd. Manage ED2K, BitTorrent, and Soulseek downloads from a single modern web interface. Features multi-instance support, user management with SSO, Prowlarr integration for torrent search, Torznab indexer and qBittorrent-compatible API for aMule (*arr integration), push notifications via Apprise, and GeoIP peer location display. Built with Node.js, WebSockets, and React.

![aMuTorrent](./docs/screenshots/home-desktop.png)

Expand All @@ -16,6 +16,7 @@ A unified download manager for aMule, rTorrent, qBittorrent, Deluge, and Transmi
- **qBittorrent Integration** - Connect to qBittorrent via WebUI API
- **Deluge Integration** - Connect to Deluge via WebUI JSON-RPC
- **Transmission Integration** - Connect to Transmission via HTTP RPC
- **Soulseek Integration** - Connect to slskd via HTTP API
- **Multi-Instance** - Run multiple instances of the same client type
- **Unified Interface** - Manage all clients from a single dashboard

Expand Down
27 changes: 26 additions & 1 deletion docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ When you first access the web interface (or if no configuration exists), an inte

1. **Welcome** - Introduction to the setup process
2. **Security** - Configure web interface authentication (password protection)
3. **Download Clients** - Configure aMule, rTorrent, qBittorrent, Deluge, and/or Transmission connections (with testing)
3. **Download Clients** - Configure aMule, rTorrent, qBittorrent, Deluge, Transmission, and/or slskd connections (with testing)
4. **Directories** - Set data, logs, and GeoIP directories
5. **Integrations** - Optionally enable Prowlarr, Sonarr, and Radarr
6. **Review & Save** - Test all settings and save configuration
Expand Down Expand Up @@ -91,6 +91,8 @@ Sensitive fields include:
- `QBITTORRENT_PASSWORD` - qBittorrent WebUI password
- `DELUGE_PASSWORD` - Deluge WebUI password
- `TRANSMISSION_PASSWORD` - Transmission RPC password
- `SLSKD_API_KEY` - slskd API key
- `SLSKD_PASSWORD` - slskd password (when not using API key)
- `PROWLARR_API_KEY` - Prowlarr API key
- `SONARR_API_KEY` - Sonarr API key
- `RADARR_API_KEY` - Radarr API key
Expand Down Expand Up @@ -196,6 +198,16 @@ services:
- TRANSMISSION_PASSWORD=pass # Locks UI editing
- TRANSMISSION_USE_SSL=false

# slskd Connection (optional)
- SLSKD_ENABLED=false
- SLSKD_HOST=slskd
- SLSKD_PORT=5030
- SLSKD_PATH=
- SLSKD_API_KEY=your_api_key # Locks UI editing
- SLSKD_USERNAME=
- SLSKD_PASSWORD= # Locks UI editing
- SLSKD_USE_SSL=false

# Prowlarr Integration (optional - requires a BitTorrent client)
- PROWLARR_ENABLED=true
- PROWLARR_URL=http://prowlarr:9696
Expand Down Expand Up @@ -302,6 +314,19 @@ services:
| `TRANSMISSION_PASSWORD` | - | RPC auth password (locks UI editing) |
| `TRANSMISSION_USE_SSL` | `false` | Use HTTPS for RPC connection |

#### slskd Connection

| Variable | Default | Description |
|----------|---------|-------------|
| `SLSKD_ENABLED` | `false` | Enable slskd integration |
| `SLSKD_HOST` | `localhost` | slskd API hostname |
| `SLSKD_PORT` | `5030` | slskd API port |
| `SLSKD_PATH` | - | URL base path for reverse proxy (e.g., `/slskd`) |
| `SLSKD_API_KEY` | - | API key (recommended, locks UI editing) |
| `SLSKD_USERNAME` | - | Username fallback when API key is not configured |
| `SLSKD_PASSWORD` | - | Password fallback when API key is not configured (locks UI editing) |
| `SLSKD_USE_SSL` | `false` | Use HTTPS for API connection |

#### Prowlarr Integration

| Variable | Default | Description |
Expand Down
70 changes: 68 additions & 2 deletions server/lib/clientMeta.js
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,62 @@ const CLIENT_TYPES = {
customSavePath: true // can set download directory per torrent
},
seedingStatuses: ['Seeding', 'Seed Pending']
},
slskd: {
networkType: 'soulseek',
displayName: 'Soulseek (slskd)',
metricsPrefix: 'slskd_',
hashLength: 36,
statusField: 'statusText',
statusMap: {
'Requested': 'active',
'Queued': 'active',
'Queued, Remotely': 'active',
'Queued, Locally': 'active',
'Initializing': 'active',
'InProgress': 'active',
'Succeeded': 'completed',
'Completed, Succeeded': 'completed',
'Cancelled': 'stopped',
'Aborted': 'stopped',
'Completed, Cancelled': 'stopped',
'Completed, Aborted': 'stopped',
'Completed, Aborted, Locally': 'stopped',
'Completed, Aborted, Remotely': 'stopped',
'Completed, TimedOut': 'error',
'Completed, Errored': 'error',
'Completed, Rejected': 'error'
},
connectionDefaults: {
host: '', port: 5030, path: '', apiKey: '', username: '', password: '', useSsl: false
},
defaults: {
message: null,
directory: null,
addedAt: null
},
capabilities: {
nativeMove: false,
categoryChangeAutoMoves: false,
stopReplacesPause: true,
multiFile: false,
sharedFiles: true,
sharedMeansComplete: true,
removeSharedMustDeleteFiles: false,
moveSharedForCategoryChange: false,
refreshSharedAfterMove: false,
moveActiveDownloads: false,
pauseBeforeMove: false,
trackers: false,
search: true,
cancelDeletesFiles: false,
apiDeletesFiles: false,
refreshSharedAfterDelete: false,
categories: false,
logs: true,
fileRatingComment: false,
customSavePath: false
}
}
};

Expand All @@ -321,7 +377,7 @@ function get(type) {
/**
* Get the network type for a client type.
* @param {string} type - Client type key
* @returns {'ed2k'|'bittorrent'}
* @returns {'ed2k'|'bittorrent'|'soulseek'}
*/
function getNetworkType(type) {
return get(type).networkType;
Expand All @@ -345,9 +401,18 @@ function isEd2k(type) {
return CLIENT_TYPES[type]?.networkType === 'ed2k';
}

/**
* Check if a client type is a Soulseek client.
* @param {string} type - Client type key
* @returns {boolean}
*/
function isSoulseek(type) {
return CLIENT_TYPES[type]?.networkType === 'soulseek';
}

/**
* Get all client type keys that belong to a given network type.
* @param {string} networkType - 'ed2k' or 'bittorrent'
* @param {string} networkType - 'ed2k' | 'bittorrent' | 'soulseek'
* @returns {string[]} Array of client type keys
*/
function getByNetworkType(networkType) {
Expand Down Expand Up @@ -460,6 +525,7 @@ module.exports = {
getNetworkType,
isBittorrent,
isEd2k,
isSoulseek,
getByNetworkType,
getAllTypes,
hasCapability,
Expand Down
152 changes: 127 additions & 25 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 SlskdClient = require('./slskd/SlskdClient');
const ProwlarrHandler = require('./prowlarr/ProwlarrHandler');
const { checkDirectoryAccess } = require('./pathUtils');
const logger = require('./logger');
Expand Down Expand Up @@ -408,37 +409,46 @@ async function testArrAPI(serviceName, url, apiKey) {
try {
// Remove trailing slash from URL
const baseUrl = url.replace(/\/$/, '');
const endpoint = `${baseUrl}/api/v3/system/status`;

const response = await fetch(endpoint, {
method: 'GET',
headers: {
'X-Api-Key': apiKey
},
signal: AbortSignal.timeout(10000) // 10 second timeout
});

result.reachable = true;
const endpoints = [
`${baseUrl}/api/v3/system/status`,
`${baseUrl}/api/v1/system/status`
];

let lastHttpError = null;
for (const endpoint of endpoints) {
const response = await fetch(endpoint, {
method: 'GET',
headers: {
'X-Api-Key': apiKey
},
signal: AbortSignal.timeout(10000) // 10 second timeout
});

result.reachable = true;

if (response.status === 401 || response.status === 403) {
result.error = 'Authentication failed - invalid API key';
return result;
}

if (response.status === 401 || response.status === 403) {
result.error = 'Authentication failed - invalid API key';
return result;
}
if (!response.ok) {
const text = await response.text();
lastHttpError = `HTTP ${response.status}: ${text}`;
continue;
}

if (!response.ok) {
const text = await response.text();
result.error = `HTTP ${response.status}: ${text}`;
return result;
}
const data = await response.json();
result.authenticated = true;

const data = await response.json();
result.authenticated = true;
if (data.version) {
result.version = data.version;
}

if (data.version) {
result.version = data.version;
result.success = true;
return result;
}

result.success = true;
result.error = lastHttpError || `${serviceName} API did not return a supported /system/status response`;
return result;
} catch (err) {
result.error = classifyNetworkError(err);
Expand Down Expand Up @@ -466,6 +476,26 @@ async function testRadarrAPI(url, apiKey) {
return testArrAPI('Radarr', url, apiKey);
}

/**
* Test Lidarr API connection
* @param {string} url - Lidarr URL
* @param {string} apiKey - Lidarr API key
* @returns {Promise<{success: boolean, reachable: boolean, authenticated: boolean, version: string|null, error: string|null}>}
*/
async function testLidarrAPI(url, apiKey) {
return testArrAPI('Lidarr', url, apiKey);
}

/**
* Test Readarr API connection
* @param {string} url - Readarr URL
* @param {string} apiKey - Readarr API key
* @returns {Promise<{success: boolean, reachable: boolean, authenticated: boolean, version: string|null, error: string|null}>}
*/
async function testReadarrAPI(url, apiKey) {
return testArrAPI('Readarr', url, apiKey);
}

/**
* Test Prowlarr API connection
* @param {string} url - Prowlarr URL
Expand Down Expand Up @@ -671,6 +701,75 @@ async function testTransmissionConnection(host, port, username, password, useSsl
}
}

/**
* Test slskd API connection
* @param {string} host - slskd host
* @param {number} port - slskd port
* @param {string} pathPrefix - Optional path prefix behind reverse proxy
* @param {string} apiKey - API key (preferred)
* @param {string} username - Optional username for session login fallback
* @param {string} password - Optional password for session login fallback
* @param {boolean} useSsl - Whether to use HTTPS
* @returns {Promise<{success: boolean, connected: boolean, version: string|null, error: string|null}>}
*/
async function testSlskdConnection(host, port, pathPrefix, apiKey, username, password, useSsl) {
const result = {
success: false,
connected: false,
version: null,
error: null
};

if (!host) {
result.error = 'Host is required';
return result;
}

let client = null;

try {
client = new SlskdClient({
host,
port: port || 5030,
path: pathPrefix || '',
useSsl: useSsl || false,
apiKey: apiKey || '',
username: username || '',
password: password || ''
});

const testPromise = client.testConnection();
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Connection timeout after 10 seconds')), 10000);
});

const testResult = await Promise.race([testPromise, timeoutPromise]);
if (testResult.success) {
result.connected = true;
result.version = testResult.version || null;
result.success = true;
result.message = `Connected to slskd${testResult.version ? ` ${testResult.version}` : ''}`;
} else {
result.error = testResult.error || 'Connection failed';
}

await client.disconnect();
return result;
} catch (err) {
result.error = classifyNetworkError(err);

if (client) {
try {
await client.disconnect();
} catch (cleanupErr) {
// Ignore cleanup errors
}
}

return result;
}
}

module.exports = {
testDirectoryAccess,
testGeoIPDatabase,
Expand All @@ -679,7 +778,10 @@ module.exports = {
testQbittorrentConnection,
testDelugeConnection,
testTransmissionConnection,
testSlskdConnection,
testSonarrAPI,
testRadarrAPI,
testLidarrAPI,
testReadarrAPI,
testProwlarrAPI
};
Loading