ElixirTorrent

GitHub releaseChangelogHex.pmHexDocsHex.pm DownloadsLicense

buildcodecovBEPsLast commit

GitHubWeb UImacOS

OpenSSF ScorecardOpenSSF Best Practices

ElixirOTP

A complete BitTorrent client engine for Elixir/OTP — downloads, seeds, resumes, and traverses NAT, behind a small public API you can embed in your own application.

About

This is a fully functional BitTorrent client that actually downloads torrents. It started as a course project for Functional Programming with Elixir at Sofia University. After the course ended, development continued in spare time until it was ready to publish on Hex.

The whole stack is Elixir on OTP primitives — wire protocol, DHT, trackers, piece picking, storage, encryption — with 23 BEPs implemented, encryption on by default, IPv4/IPv6 dual stack, and interop verified against Transmission, qBittorrent and libtorrent.

{:ok, pid} = ElixirTorrent.download("/path/to/file.torrent")
{:ok, stats} = ElixirTorrent.stats(pid)

What you get

Full per-BEP status, including the known gaps: PROTOCOL.md.

Installation

def deps do
[
{:elixir_torrent, "~> 0.6.5"}
]
end
mix deps.get

Requires Elixir 1.20+. The engine is an OTP application — start it before first use (or list it in your supervision tree's dependencies):

Application.ensure_all_started(:elixir_torrent)

Quick start

From a .torrent file

{:ok, pid} = ElixirTorrent.download("/path/to/file.torrent")
[hash] = ElixirTorrent.list()
# Write files under a specific directory (session state still uses File.cwd!/0):
{:ok, pid} = ElixirTorrent.download("/path/to/file.torrent", download_dir: "/Downloads")
{:ok, stats} = ElixirTorrent.stats(pid, [:name, :speed, :downloaded, :bytes_size])
# stats.name, stats.speed.download, stats.speed.upload, …
files = ElixirTorrent.list_files(hash)
# Each entry has :path, :progress, :complete?, etc.

Poll stats/2 while the download runs. When you are done, stop_and_serialize/1 keeps the progress and remove/2 drops it.

{:ok, pid} =
ElixirTorrent.download_magnet(
"magnet:?xt=urn:btih:…&tr=udp%3A%2F%2Ftracker.example.com%3A1337%2Fannounce"
)

The engine parses the URI, announces to its tr= trackers and/or asks the DHT for peers, fetches the info dictionary over BEP 9, checks SHA1(bencode(info)) against the magnet's hash, and only then starts a normal session.

A magnet needs at least one tr= tracker or DHT enabled; a trackerless magnet with DHT off returns {:error, :missing_trackers}. Other common failures: :no_peers, :timeout, :metadata_unavailable, :info_hash_mismatch.

Session persistence

Progress survives restarts. Each session is a file under {File.cwd!()}/.elixir_torrent/state/{hex_info_hash}.term holding the bitfield, byte counters, and peer status. Call download/2 with the same .torrent and the engine loads the session, verifies pieces against what is on disk, and resumes from there.

ElixirTorrent.stop_and_serialize(hash) # one torrent
ElixirTorrent.stop_all_and_serialize() # everything, e.g. on application shutdown

Stopping this way is a protocol-clean shutdown, not a socket drop: active piece requests are cancelled, peers get BEP 3 cancel/not interested/choke before the connection closes, and each tracker receives an event=stopped announce so the swarm stops handing your address to other peers.

To drop a torrent instead of pausing it:

ElixirTorrent.remove(hash) # deletes the session file
ElixirTorrent.remove(hash, delete_data: true) # …and the downloaded files

Configuration

Every subsystem that talks to the network can be switched off independently — useful for embedded use, private-tracker-only setups, or a test suite that must not touch the wire.

config :elixir_torrent,
listen_port: 6881
config :elixir_torrent, :dht,
enabled: true, # BEP 5 DHT
routing_store: true, # persist the routing table between runs
bootstrap_routers: [{"router.bittorrent.com", 6881}]
config :elixir_torrent, :lsd, enabled: true # BEP 14 LAN multicast discovery
config :elixir_torrent, :nat, enabled: true # NAT-PMP/PCP/UPnP + STUN detection
config :elixir_torrent, :network, dial_scope: :any

dial_scope: :this_host restricts outbound connections to addresses this machine owns (loopback and its own interfaces), which is how the test suite runs the full engine against loopback fixtures without a single packet leaving the host.

Public API

Full reference: hexdocs.pm/elixir_torrent/ElixirTorrent.html

FunctionDescription
download/2Start a download from a local .torrent path; optional download_dir:
download_magnet/2Start from a magnet URI (metadata fetch + normal session)
stats/2Runtime stats map (:name, :speed, :downloaded, :bytes_size, …)
list/0Info hashes for all active torrent processes
list_files/1Per-file paths and download progress
stop_and_serialize/1Graceful stop + persist session
stop_all_and_serialize/0Graceful stop + persist for every torrent
remove/2Stop and drop from session; optional delete_data: true
get/2Low-level field access (prefer stats/2)
version/0Version-derived client peer ID prefix (ET0-6-5, BEP 20)

ElixirTorrent Web (desktop app)

Need a client rather than a library? ElixirTorrent Web is the official Phoenix LiveView UI for this engine, shipped as a native macOS app.

Development

mix test # full suite; runs with no network access
mix quality # compile --warnings-as-errors + dialyzer + credo --strict + sobelow

mix quality is the same gate CI runs. Sobelow does security-focused static analysis at --threshold medium, filtering low-confidence file-path findings from the engine's own already-sanitized path handling.

The package also builds a small escript for ad-hoc testing:

mix escript.build
./elixir_torrent
# then type: download /path/to/file.torrent

The escript's interactive loop does not expose magnet links yet — use download_magnet/2 from your own application for those.

Contributing

Contributions are welcome — see CONTRIBUTING.md. Security reports go through SECURITY.md, not public issues.

Released under the MIT License.