qbittorrent.cr v0.2.0

A Crystal client library for the qBittorrent WebUI API v2 (qBittorrent 5.x).

qbittorrent

A typed Crystal client for the qBittorrent WebUI API v2, targeting qBittorrent 5.x (WebAPI 2.11+, live-verified against 5.2.3 / WebAPI 2.15.1). There is no OpenAPI schema for this API — every endpoint method and JSON model in this shard is hand-modeled from the official wiki (distilled into ext/api-map.md) and cross-checked against a live container.

Highlights:

  • Session-based auth. qBittorrent's WebUI uses a cookie session, not an API key: Client#login posts credentials to auth/login, captures the dynamic QBT_SID_<port> session cookie, and attaches it (with a matching Referer/Origin header) to every later call. A 403 from an authenticated call triggers one transparent re-login + retry.
  • One class per endpoint group (app, log, sync, transfer, torrents, search, rss), each built on top of the shared Client.
  • Typed models and enums for JSON responses, with tolerant decoding for the partial payloads sync/maindata sends (see Usage: sync).

Installation

  1. Add the dependency to your shard.yml:

    dependencies:
      qbittorrent:
        github: mjblack/qbittorrent.cr
    
  2. Run shards install

Quick start

require "qbittorrent"

client = QBittorrent::Client.new("http://localhost:8080", "admin", "adminadmin")
client.login

version = QBittorrent::Api::App.new(client).version
# => "v5.2.3"

transfer_info = QBittorrent::Api::Transfer.new(client).info
transfer_info.dht_nodes
# => 67

downloading = QBittorrent::Api::Torrents.new(client).info(filter: QBittorrent::Model::TorrentFilter::Downloading)
# => Array(QBittorrent::Model::TorrentInfo)

client.logout

Every endpoint group is its own class taking a Client: QBittorrent::Api::<Group>.new(client). Client#login is called lazily on the first request if you skip it, so the explicit call above is optional but recommended — it fails fast on bad credentials instead of on the first real call.

Note: convenience accessors on Client itself (e.g. client.torrents instead of QBittorrent::Api::Torrents.new(client)) are planned but not implemented yet. For now, construct each group explicitly as shown below.

Usage by group

Every example below assumes:

client = QBittorrent::Client.new("http://localhost:8080", "admin", "adminadmin")
client.login

app

Application version/build info, global preferences, cookies, and network interfaces (QBittorrent::Api::App):

app = QBittorrent::Api::App.new(client)
app.version          # => "v5.2.3"
app.webapi_version    # => "2.15.1"
app.preferences.dht   # => true

app.set_preferences({"dht" => JSON::Any.new(false)})

set_preferences sends only the fields you pass — it's a partial update, not a full replace.

log

The main log and peer log, both with incremental fetching via last_known_id (QBittorrent::Api::Log):

log = QBittorrent::Api::Log.new(client)
log.main(warning: true, critical: true) # only warnings/criticals
log.peers(last_known_id: 42_i64)        # peer log entries since id 42

sync

The delta-based feeds that back the WebUI (QBittorrent::Api::Sync). Both methods take a response id (rid) from the previous call; omit it to get a full snapshot, then pass back the returned rid to fetch only what changed:

sync = QBittorrent::Api::Sync.new(client)
data = sync.maindata                 # full snapshot (rid defaults to 0)
delta = sync.maindata(rid: data.rid) # only what changed since `data`
sync.torrent_peers("<info-hash>")

Because deltas only carry changed keys, Model::MainData and the nested Model::TorrentInfo values it contains have every field nilable — don't assume a field is present on any given response.

transfer

Global transfer stats, the alternative speed-limits toggle, global rate limits, and peer banning (QBittorrent::Api::Transfer):

transfer = QBittorrent::Api::Transfer.new(client)
transfer.info.dht_nodes # => 67
transfer.download_limit # => 0 (unlimited)
transfer.set_download_limit(1_048_576_i64)

torrents

The largest group, spread across three files (torrents.cr for read/info endpoints, torrents_lifecycle.cr for add/start/stop/delete/queue, torrents_config.cr for per-torrent configuration) but all reopening the same QBittorrent::Api::Torrents class:

torrents = QBittorrent::Api::Torrents.new(client)
torrents.info(filter: QBittorrent::Model::TorrentFilter::Downloading)
torrents.properties("<info-hash>").save_path
torrents.categories["movies"].save_path

Adding torrents — from a magnet/HTTP URL, or from raw .torrent file content (which is sent as multipart/form-data; content may be a String or Bytes):

torrents.add(
  urls: "magnet:?xt=urn:btih:...",
  category: "movies",
  tags: %w[hd 4k],
)

torrent_bytes = File.read("ubuntu.torrent")
torrents.add(torrent: torrent_bytes, torrent_filename: "ubuntu.torrent", savepath: "/downloads")

Lifecycle and per-torrent configuration:

torrents.start("all")
torrents.stop(["hash1", "hash2"])
torrents.delete(["hash1"], delete_files: false)

torrents.set_download_limit("all", 1_048_576_i64)
torrents.set_category(%w[abc def], "movies")
torrents.add_tags("abc", %w[hd 4k])

Anywhere a hashes argument is accepted you can pass the string "all", a single hash, or an Enumerable(String) of hashes.

search

Start/poll/stop search jobs and manage search plugins (QBittorrent::Api::Search):

search = QBittorrent::Api::Search.new(client)
id = search.start("ubuntu")      # => 1
search.status(id).first.running? # => true
search.results(id).results.first.file_name
search.stop(id)
search.delete(id)

rss

RSS feeds/folders, their items, and the auto-downloading rules engine (QBittorrent::Api::Rss):

rss = QBittorrent::Api::Rss.new(client)
rss.add_feed("https://example.com/feed.xml", "News")
rss.items(with_data: true) # => Hash(String, JSON::Any) tree
rss.rules                  # => Hash(String, Model::RssRule)

rss/items returns a heterogeneous, recursively-nested tree (folders contain folders and feeds), so #items returns it untyped as Hash(String, JSON::Any). To work with a located feed node as a typed model:

tree = rss.items(with_data: true)
feed = QBittorrent::Model::RssFeed.from_json(tree["My Feed"].to_json)
feed.articles.try &.each { |a| puts a.title }

Development

$ shards install
$ crystal spec
$ crystal tool format
$ bin/ameba

crystal spec (no environment variables) runs the offline unit specs only — JSON parsing against recorded response samples — and stays green without Docker or network access.

Docker integration tests

Live integration coverage runs against a real qbittorrentofficial/qbittorrent-nox container, managed with scripts/qbit-testenv.sh:

$ scripts/qbit-testenv.sh up
$ scripts/qbit-testenv.sh wait
$ QBITTORRENT_INTEGRATION=1 \
    QBITTORRENT_PASSWORD="$(scripts/qbit-testenv.sh creds password)" \
    crystal spec spec/integration
$ scripts/qbit-testenv.sh down

The container's config volume is ephemeral, so on every fresh up qBittorrent 5.x generates a temporary WebUI password for the built-in admin user and prints it to the container log instead of using a pre-seeded credential. scripts/qbit-testenv.sh creds (or creds password / creds user) parses that line out of docker compose logs for you. Integration specs are gated on QBITTORRENT_INTEGRATION=1 and report as pending (not failed) when it's unset, so a plain crystal spec never needs Docker. scripts/qbit-testenv.sh down removes the container and its ephemeral config volume so the next up starts clean.

Development notes

  • The API surface has no schema to generate from; ext/api-map.md is kept as the working reference distilled from the wiki, cross-checked against a live 5.2.3 / WebAPI 2.15.1 container where the two disagree.
  • The qBittorrent 5.x session cookie is named QBT_SID_<port> (e.g. QBT_SID_8080), not the SID name older docs/tools use — Client reads the actual cookie name off Set-Cookie rather than assuming it.

Contributing

  1. Fork it (https://github.com/mjblack/qbittorrent.cr/fork)
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

Contributors

Repository

qbittorrent.cr

Owner
Statistic
  • 0
  • 0
  • 0
  • 1
  • 1
  • 2 days ago
  • July 19, 2026
License

MIT License

Links
Synced at

Mon, 20 Jul 2026 17:43:53 GMT

Languages