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
| BEP | Feature | Notes |
|---|---|---|
| 3 | BitTorrent protocol | TCP peer wire, rarest-first with endgame |
| 4 | Reserved handshake bits | Advertises the extension protocol, DHT, Fast, and v2 bits |
| 5 | DHT | Continuous get_peers and announce_peer per torrent |
| 6 | Fast Extension | HaveAll/HaveNone, Reject, Suggest, allowed-fast sets |
| 7 | IPv6 tracker extension | Announces over IPv4 and IPv6 separately and merges the peers; accepts peers6 |
| 8 | Tracker peer obfuscation | See Trackers |
| 9 | Metadata exchange | ut_metadata for magnets; x.pe peers are contacted directly |
| 10 | Extension protocol | Advertises the client as Risuko <version> |
| 11 | Peer exchange | ut_pex with encryption, seed, µTP, hole-punch, and reachable flags |
| 12 | Multitracker | Private torrents try tiers in order, shuffled within a tier, with a responding tracker moved to the front; public torrents announce to every tracker concurrently |
| 14 | Local Service Discovery | Announces every info-hash the torrent is known by |
| 15 | UDP trackers | IPv4 and IPv6 |
| 19 | WebSeeds | See WebSeeds |
| 20 | Peer ID | -RS + one character each for major, minor, and patch version + 0-, e.g. -RS0700- |
| 21 | Partial seeds | A peer's upload_only marks it as a seed in PEX; Risuko advertises upload_only once complete |
| 23 | Compact peer lists | |
| 27 | Private torrents | See the callout under WebSeeds |
| 29 | µTP | See Connections and Encryption |
| 32 | IPv6 DHT | When an IPv6 socket can be bound |
| 41 | UDP tracker URL data | The path and query of udp:// URLs (passkeys) are sent to the tracker |
| 43 | Read-only DHT nodes | Read-only queriers are answered but kept out of the routing table |
| 47 | Padding files | Hidden from file lists, never written to disk, not counted by select-file |
| 52 | BitTorrent v2 | Merkle verification, piece layers, and hash requests with proofs |
| 53 | Magnet so= | Used as the initial file selection |
| 55 | Hole punching | ut_holepunch rendezvous for NATed PEX peers |
Magnet Links
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 ~/DownloadsNode.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-53so=parameter is used, and the task'sselect-fileis 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.txtFor 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 falseThe 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.0await 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 60await 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:
risuko.onBtDownloadComplete— Download finished, now seedingrisuko.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:
| Value | Outbound | Inbound |
|---|---|---|
plaintext | Plaintext handshake only | MSE 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 first | Plaintext and MSE |
require | MSE only | Plaintext 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.