Risuko
Guides

BitTorrent Downloads

Guide to downloading torrents with Risuko.

Risuko includes a full BitTorrent client powered by risuko-bt, an in-tree BitTorrent engine that supports both BEP-3 (v1) and BEP-52 (v2) torrents, including hybrid torrents that carry both info-hashes. Public torrents can also use BEP-19 WebSeed mirrors to fill missing pieces over HTTP(S). Peer discovery uses dual-stack DHT (BEP-5/32), trackers (HTTP and UDP), LSD (BEP-14), and PEX (BEP-11), with µTP (BEP-29) fallback and BEP-55 hole punching for NATed peers.

For v2 and hybrid torrents the task exposes an additional infoHashV2 (SHA-256) field alongside infoHash, plus metaVersion (v1, v2, or hybrid).

Supported BEPs

BEPFeatureNotes
3BitTorrent protocolTCP peer wire, rarest-first with endgame
4Reserved handshake bitsAdvertises the extension protocol, DHT, Fast, and v2 bits
5DHTContinuous get_peers and announce_peer per torrent
6Fast ExtensionHaveAll/HaveNone, Reject, Suggest, allowed-fast sets
7IPv6 tracker extensionAnnounces over IPv4 and IPv6 separately and merges the peers; accepts peers6
8Tracker peer obfuscationSee Trackers
9Metadata exchangeut_metadata for magnets; x.pe peers are contacted directly
10Extension protocolAdvertises the client as Risuko <version>
11Peer exchangeut_pex with encryption, seed, µTP, hole-punch, and reachable flags
12MultitrackerPrivate torrents try tiers in order, shuffled within a tier, with a responding tracker moved to the front; public torrents announce to every tracker concurrently
14Local Service DiscoveryAnnounces every info-hash the torrent is known by
15UDP trackersIPv4 and IPv6
19WebSeedsSee WebSeeds
20Peer ID-RS + one character each for major, minor, and patch version + 0-, e.g. -RS0700-
21Partial seedsA peer's upload_only marks it as a seed in PEX; Risuko advertises upload_only once complete
23Compact peer lists
27Private torrentsSee the callout under WebSeeds
29µTPSee Connections and Encryption
32IPv6 DHTWhen an IPv6 socket can be bound
41UDP tracker URL dataThe path and query of udp:// URLs (passkeys) are sent to the tracker
43Read-only DHT nodesRead-only queriers are answered but kept out of the routing table
47Padding filesHidden from file lists, never written to disk, not counted by select-file
52BitTorrent v2Merkle verification, piece layers, and hash requests with proofs
53Magnet so=Used as the initial file selection
55Hole punchingut_holepunch rendezvous for NATed PEX peers

Add a magnet link from the CLI, the desktop app, or the Node.js API. The CLI passes it to risuko.addUri, which sniffs the magnet: scheme and routes it to the torrent handler.

CLI

risuko download "magnet:?xt=urn:btih:abc123def456..."

Node.js

import risuko from "@risuko/risuko-js";

const { addMagnet } = risuko;

const gid = await addMagnet("magnet:?xt=urn:btih:abc123def456...", {
  dir: "/downloads",
});

Besides the info-hash (xt=urn:btih: and/or xt=urn:btmh:), Risuko reads dn= (display name), tr= (trackers), x.pe= (peers to dial directly while fetching the metadata; IP literals or hostnames), and so= (BEP-53 file selection, used when the task sets no select-file).

.torrent Files

CLI

risuko download ./ubuntu-24.04.torrent -d ~/Downloads

Node.js

import { readFile } from "node:fs/promises";
import risuko from "@risuko/risuko-js";

const { addTorrent } = risuko;

const data = await readFile("./ubuntu-24.04.torrent");
const gid = await addTorrent(data, { dir: "/downloads" });

File Selection

By default every file in a torrent is downloaded. The select-file option limits the download to a subset using aria2's syntax: 1-based file indices, separated by commas, with a-b ranges.

const gid = await addTorrent(data, { dir: "/downloads", "select-file": "1-3,7" });

The desktop app sets it from the file list in the Add Task dialog for both .torrent files and magnets.

  • Indices follow the file list shown by getFiles, which hides BEP-47 padding files, so padding entries are never counted.
  • For a magnet without select-file, the BEP-53 so= parameter is used, and the task's select-file is filled in from it.
  • changeOption(gid, { "select-file": "2" }) applies a new selection to a running torrent immediately; an empty string selects every file again. Pieces needed only by deselected files are no longer requested, and newly selected files are downloaded from where they left off.
  • Out-of-range indices are ignored.

A piece can span the end of one file and the start of the next. When a selected file shares a piece with a deselected one, the deselected file's bytes from that piece are kept in a part file instead of creating the deselected file on disk. Part files live under .risuko-parts/<info-hash>/ in the torrent's root folder, are moved into place if the file is selected later, and are removed with the task when files are deleted.

WebSeeds

Risuko reads BEP-19 url-list entries from .torrent metadata automatically. Each URL is treated as a WebSeed mirror for a public torrent; no extra task option is required.

WebSeeds must be HTTP or HTTPS URLs and the server must support byte-range requests. Risuko requests missing pieces with Range, checks the returned Content-Range, and verifies every piece against the torrent metadata before writing it to disk. WebSeed transfers run alongside peer transfers, and a failed mirror is skipped so another mirror or the peer swarm can continue the download. Mirrors back off on transient errors and are only disabled (or just the affected file) on permanent ones.

For a multi-file torrent, each WebSeed URL is a base URL and must end with a slash. Risuko appends the torrent name and each file's relative path:

https://cdn.example.com/ubuntu/ -> https://cdn.example.com/ubuntu/ubuntu-24.04/readme.txt

For a single-file torrent, a URL can point directly to the file, or a base URL ending in / can have the torrent filename appended. URL fragments are ignored, and duplicate or unsupported URLs are discarded.

Private torrents are not allowed to use WebSeeds. Their private flag also disables DHT, LSD, PEX, and hole punching; peer connections come from the torrent's trackers only. Magnet resolution may use discovery to obtain the metadata, but once the private flag is known the download follows the private torrent rules.

WebSeed requests use the P2P proxy profile when one is configured. Direct connections reject loopback, private, and link-local destinations.

On-Disk Layout

By default, multi-file torrents are written under <dir>/<torrent-name>/.... Single-file torrents are always written directly under <dir>/.

To flatten multi-file torrents into the download directory, disable bt-create-subfolder:

await changeGlobalOption({ "bt-create-subfolder": false });
risuko config set bt-create-subfolder false

The same option is accepted per-task and overrides the global default. Toggling the global setting only affects newly added torrents; existing tasks keep the layout chosen at add time. When a task is removed with "delete files" enabled, Risuko reads the per-task layout flag (mirrored on the bt session handle) so it deletes the right paths regardless of the current global value.

Seeding Control

By default, Risuko does not seed after a torrent completes (keep-seeding is false and seed-time / seed-ratio default to 0). To enable seeding, turn on keep-seeding, or set a finite seed-time / seed-ratio:

Keep Seeding

Seed indefinitely until the task is stopped manually:

await changeGlobalOption({ "keep-seeding": true });

When keep-seeding is enabled, the seed-time and seed-ratio limits are ignored.

Seed Ratio

Stop seeding when the upload/download ratio reaches a threshold:

risuko download ./ubuntu.torrent --seed-ratio 1.0
await addMagnet("magnet:?xt=...", { "seed-ratio": "1.0" });

A ratio of 1.0 means upload the same amount as downloaded.

Seed Time

Seed for a number of minutes after download completes:

risuko download ./ubuntu.torrent --seed-time 60
await addMagnet("magnet:?xt=...", { "seed-time": 60 });

Per-task seed-time and seed-ratio accept numbers or numeric strings and override the global values for that task, including when changed later with changeOption.

When a finished torrent reaches its seeding goal, or keep-seeding is off, the engine stops it instead of leaving it seeding invisibly. After a restart, seed-time is counted from when seeding resumes.

Seeding starts whenever keep-seeding is true, or seed-time > 0, or seed-ratio > 0. If keep-seeding is off, seeding stops when seed-time elapses or seed-ratio is reached, whichever comes first.

Combined Limits

When both seed-time and seed-ratio are set (and keep-seeding is off), seeding stops when either limit is reached first.

Events

BitTorrent downloads emit two completion events:

  1. risuko.onBtDownloadComplete — Download finished, now seeding
  2. risuko.onDownloadComplete — Seeding finished (or limits reached)
await onEvent((event, gid) => {
  if (event === "risuko.onBtDownloadComplete") {
    console.log("Download done, seeding...");
  }
  if (event === "risuko.onDownloadComplete") {
    console.log("Fully complete");
  }
});

Peer Information

Check connected peers for a torrent download:

risuko peers <gid>
const peers = await getPeers(gid);

Connections and Encryption

Outbound peers are dialled over TCP first, falling back to µTP on the same UDP port as the listener when the TCP dial or handshake fails. Peers reached through a BEP-55 hole-punch rendezvous are dialled over µTP first. Inbound TCP and µTP connections are both accepted.

The µTP transport implements LEDBAT congestion control, selective ACKs, fast retransmit, Nagle-style coalescing of small writes, and path-MTU discovery: packets start at a conservative size and grow using don't-fragment probes where the OS allows toggling that flag.

Message Stream Encryption (MSE/PE, RC4) works over both TCP and µTP and is controlled by bt-encryption-policy:

ValueOutboundInbound
plaintextPlaintext handshake onlyMSE connections are rejected
prefer (default)Plaintext first, MSE on a fresh connection if that fails; peers learned from an obfuscated tracker are tried with MSE firstPlaintext and MSE
requireMSE onlyPlaintext connections are rejected

Unknown values fall back to prefer. Unless the policy is plaintext, the extension handshake sets e=1 to tell peers this client prefers encryption.

Trackers

HTTP(S) and UDP trackers from announce, announce-list, tr= magnet parameters, and bt-tracker are used together. announce-list tiers follow BEP-12. Without a proxy or fixed source address, each tracker is announced over IPv4 and IPv6 separately and the peer lists are merged (BEP-7). The path and query of a udp:// tracker URL, such as a passkey, are sent with UDP announces (BEP-41).

Public torrents announce to every tracker in every tier concurrently, so trackers from bt-tracker, tr=, and announce-list are on equal footing. Private torrents keep BEP-12 per-tier failover and only use the trackers in their metainfo.

Torrents with an obfuscate-announce-list (BEP-8) announce to its HTTP(S) trackers first, sending sha_ih (the SHA-1 of the info-hash) instead of the info-hash and the listen port XOR-ed with an RC4 keystream. Peer lists in the reply are decrypted with the same keystream, and those peers are dialled with MSE first. UDP entries in that list are skipped because UDP announces cannot carry sha_ih.

P2P Proxy

BitTorrent traffic uses the P2P proxy profile, not the HTTP download proxy. Configure it in Preferences -> Advanced -> Proxy. SOCKS5 or SOCKS5H is required for DHT, UDP trackers, and µTP; an HTTP proxy only covers TCP peers and HTTP tracker announces. See the Proxy Support guide.

Speed Limits and Corrupt Peers

max-overall-download-limit is one budget shared with every other protocol, and max-overall-upload-limit caps BitTorrent uploads. The per-task max-download-limit and max-upload-limit apply to a single torrent. bt-upload-rate-limit is also a BitTorrent upload cap; the smaller non-zero of it and max-overall-upload-limit applies. All of these, and size suffixes such as 512K, take effect on running torrents through changeGlobalOption and changeOption without a restart.

A peer whose data fails a piece hash check gets a strike only when Risuko is certain it was to blame: it was the only contributor to the failed piece, or its blocks differ from the later verified copy. At bt-ban-corrupt-strikes (default 3) its IP is banned for the rest of the session in every torrent. WebSeeds never get strikes. Set bt-ban-corrupt-peers to false to turn bans off. Both keys need an engine restart.

On shutdown every torrent is stopped in parallel (10 s overall bound), so trackers receive stopped. All torrents share a budget of live peer connections plus pending dials (bt-max-connections, default 400, range 20-5000; Preferences -> Advanced -> BitTorrent -> Max Peer Connections). It applies live without an engine restart, is never above half the open-file limit, and lowering it never drops open connections, it only holds back new ones. A small reserve is kept for magnet metadata fetches, which wait for a slot instead of failing. Running out of file descriptors slows dialling instead of counting as a peer failure.

The resume hash check runs in the background, so pause and stop stay responsive; nothing unverified is served or announced until it finishes. Repeated disk write failures stop the torrent with an error until you resume it.

Peer blocklist and PeerBanHelper

risuko.setBtPeerBlocklist (also aria2.setBtPeerBlocklist) full-replaces a process-wide IPv4/IPv6 CIDR blocklist. Matching connected peers are disconnected immediately.

To drive that list from PeerBanHelper, enable PeerBanHelper in Preferences -> Advanced, set a listen port (default 16801) and RPC token, then add an Aria2Next downloader in PeerBanHelper pointed at http://127.0.0.1:16801/jsonrpc. The extra listener is startup-only and reports product: "aria2-next" from getVersion. See the RPC Protocol.

On this page