PHPackages                             unkybadtouch/qbbittorrent - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. unkybadtouch/qbbittorrent

ActiveLibrary

unkybadtouch/qbbittorrent
=========================

Reusable qBittorrent client with UTF-8 safe handling

v1.0.1(today)00PHPPHP &gt;=8.4

Since Jul 27Pushed todayCompare

[ Source](https://github.com/UnkyBadTouch/qbittorrent)[ Packagist](https://packagist.org/packages/unkybadtouch/qbbittorrent)[ RSS](/packages/unkybadtouch-qbbittorrent/feed)WikiDiscussions master Synced today

READMEChangelogDependencies (2)Versions (3)Used By (0)

unkybadtouch/qbbittorrent
=========================

[](#unkybadtouchqbbittorrent)

PHP client for the qBittorrent WebUI API v2 (5.0+), with a typed DTO layer over the raw JSON responses. UTF-8 safe, PHP 8.4 (uses property hooks).

```
use Blackout\Qbittorrent\Client;

$qb = new Client('http://localhost:8080', 'admin', 'adminadmin');

foreach ($qb->getTorrents(['filter' => 'downloading']) as $torrent) {
    echo "{$torrent->name}: {$torrent->progress_percent}%\n";
}
```

Requirements
------------

[](#requirements)

- PHP &gt;= 8.4 (property hooks are used throughout the DTO layer)
- ext-mbstring
- guzzlehttp/guzzle ^7.0
- hassankhan/config ^3.2

Install
-------

[](#install)

```
composer require unkybadtouch/qbbittorrent
```

Quick start
-----------

[](#quick-start)

```
use Blackout\Qbittorrent\Client;

$qb = new Client($baseUri, $username, $password);
```

`login()` is called automatically on first request. The auth cookie is cached to `cache/cookies.json` (relative to the package's `src/` dir) for an hour, so you don't re-authenticate on every script run; a 403 mid-session triggers one automatic re-login + retry. Call `$qb->logout()` to clear both the cookie and the cache file.

Core concepts
-------------

[](#core-concepts)

**DTOs hydrate from raw API arrays.** Every `Client` method that returns a single entity or list of entities returns `Base` subclasses (`Blackout\Qbittorrent\DTO\*`), not raw arrays. Hydration is reflection-based: public typed properties matching an array key get cast (scalars, backed enums, nested DTOs); anything else is skipped silently — a field the API doesn't return for a given call just stays uninitialized.

**DTOs carry behavior.** Most DTOs hold a reference to the `Client` that created them and expose action/mutator methods that call back into it, e.g. `$torrent->pause()` — no need to keep passing hashes/names back to `$qb`yourself.

**Lazy relations.** `Torrent::$files`/`$trackers`/`$peers`/`$pieces`/`$webseeds`are fetched on first access, not eagerly:

```
$torrent = $qb->getTorrents(['hashes' => $hash])[0];
foreach ($torrent->files as $file) { ... } // fires getTorrentFiles() here
foreach ($torrent->files as $file) { ... } // cached, no second request
```

**JSON round-trip.** Every DTO implements `JsonSerializable` — `json_encode($torrent)`emits only the initialized/loaded fields (uninitialized properties and un-fetched lazy relations are omitted).

**Wire conventions**: booleans go over the wire as the strings `'true'`/`'false'`. Any `Client` method taking a list (`$hashes`, `$tags`, `$urls`, ...) accepts either a bare string or an array — a single item doesn't need to be wrapped.

Usage examples
--------------

[](#usage-examples)

These walk through real end-to-end flows, with particular attention to **nested DTOs** — DTOs that hold other DTOs, either eagerly (hydrated inline from the same response) or lazily (fetched from a separate endpoint on first property access).

### 1. Add a magnet, poll until done, list its files

[](#1-add-a-magnet-poll-until-done-list-its-files)

```
use Blackout\Qbittorrent\Client;
use Blackout\Qbittorrent\Enum\TorrentState;

$qb = new Client('http://localhost:8080', 'admin', 'adminadmin');

$qb->addTorrentUrls($magnetUri, [
    'category' => 'linux',
    'paused'   => 'false',
    // never pass 'savepath' here (PROJECT.md gotcha — qBittorrent mishandles it
    // on /torrents/add); set the save path after the add instead:
    // $torrent->setSavePath('/data/downloads/linux')
]);

// addTorrentUrls()/addTorrentFile() don't return the new Torrent — the API
// doesn't echo it back — so fetch it once you know the hash (magnet URIs
// carry it in the `xt=urn:btih:...` param; for .torrent files, hash it
// yourself or grep the newest entry in the category)
$hash = strtolower(preg_match('/btih:([a-f0-9]{40})/i', $magnetUri, $m) ? $m[1] : '');

do {
    $torrent = $qb->getTorrents(['hashes' => $hash])[0] ?? null;
    usleep(500_000);
} while ($torrent === null || $torrent->state === TorrentState::META_DL);

while (!$torrent->is_complete) {
    sleep(2);
    $torrent = $qb->getTorrents(['hashes' => $hash])[0];
}

echo "{$torrent->name} finished ({$torrent->size_human})\n";

foreach ($torrent->files as $file) { // fires getTorrentFiles($hash) here, once
    echo "  {$file->basename} ({$file->size_human})\n";
}
```

### 2. Nested DTOs: walking every relation on a `Torrent`

[](#2-nested-dtos-walking-every-relation-on-a-torrent)

`Torrent` is the clearest nested-DTO case in the library: one hydrated object whose five collection properties are each backed by a *separate* API call, fetched lazily and cached per-instance.

```
$torrent = $qb->getTorrents(['hashes' => $hash])[0];

// Nothing below has fired a request yet — files/trackers/peers/pieces/webseeds
// are declared as hooked `get` properties (Torrent.php), not hydrated fields.

foreach ($torrent->files as $file) {          // -> Client::getTorrentFiles($hash)
    printf("  [%d] %-40s %6.2f%% prio=%s\n",
        $file->index, $file->basename, $file->progress_percent, $file->priority->label());
}

foreach ($torrent->trackers as $tracker) {    // -> Client::getTorrentTrackers($hash)
    printf("  %-50s %s (%s)\n", $tracker->url, $tracker->status->label(), $tracker->msg);
}

foreach ($torrent->peers as $peer) {          // -> Client::getTorrentPeers($hash)
    printf("  %-15s:%-5d %-20s %5.1f%%\n", $peer->ip, $peer->port, $peer->client, $peer->progress * 100);
}

$piecesHave = count(array_filter($torrent->pieces, fn ($p) => $p->is_downloaded)); // -> Client::getTorrentPiecesStates($hash)
echo "{$piecesHave}/{$torrent->pieces_num} pieces\n";

foreach ($torrent->webseeds as $seed) {       // -> Client::getTorrentWebSeeds($hash)
    echo "  {$seed->url}\n";
}

// Second access to any of these is free — cached in $torrent->_relations
$torrent->files; // no request

// Mutating through a relation invalidates its own cache so the next read refetches:
$torrent->addTrackers('udp://new-tracker.example.com:80');
$torrent->trackers; // -> Client::getTorrentTrackers($hash) again, includes the new one
```

`Torrent::$downloadable_files` is a computed nested-DTO filter — it reads `$this->files` (triggering the same lazy fetch) and drops anything with `FilePriority::DO_NOT_DOWNLOAD`:

```
$toGrab = array_sum(array_map(fn ($f) => $f->size, $torrent->downloadable_files));
echo "Will download " . \Blackout\Helper::filesize($toGrab) . "\n";
```

### 3. Nested DTOs inside a plain array: the RSS feed/folder tree

[](#3-nested-dtos-inside-a-plain-array-the-rss-feedfolder-tree)

Unlike `Torrent`'s relations (DTO holding DTOs via lazy fetch), `getRssFeeds()`returns one *eagerly* hydrated structure where `Feed` DTOs are interleaved with plain PHP arrays (folders have no DTO of their own) at arbitrary depth:

```
use Blackout\Qbittorrent\DTO\Rss\Feed;

function walkRssTree(array $node, string $indent = ''): void
{
    foreach ($node as $name => $item) {
        if ($item instanceof Feed) {
            // hasError/isLoading/title etc. are only initialized when fetched
            // with withData=true — accessing them otherwise throws (uninitialized
            // typed property), so guard with isInitialized-style try/catch or,
            // as here, only read them when you know withData was passed
            $status = $item->hasError ? 'ERROR' : 'ok';
            echo "{$indent}{$name}  [{$item->url}]  {$status}\n";
        } else {
            echo "{$indent}{$name}/\n";
            walkRssTree($item, $indent . '    '); // recurse into the folder
        }
    }
}

walkRssTree($qb->getRssFeeds(['withData' => true]));

// Direct access once you know the path:
$feed = $qb->getRssFeeds()['tech']['linux-distros'];
$feed->setRefreshInterval(1800);
```

Each `Feed`, however deep in the tree, still knows its own full backslash-joined `path` (e.g. `tech\linux-distros`) because `Client::hydrateRssItems()` threads it through the recursion during hydration — that's the one field on `Feed`that isn't a real API value (see the DTO reference below).

### 4. Two-level nested flow: search status → search results

[](#4-two-level-nested-flow-search-status--search-results)

```
use Blackout\Qbittorrent\Enum\SearchStatus;

$id = $qb->startSearch('debian netinst iso', plugins: 'all', category: 'all');

do {
    sleep(1);
    $status = $qb->getSearchStatus($id)[0]; // Search\Status
} while ($status->status === SearchStatus::RUNNING);

echo "{$status->total} results\n";

foreach ($status->results(limit: 25) as $result) { // Search\Result[], via Status::results()
    printf("%-60s %5d seeds  %s\n", $result->fileName, $result->nbSeeders, $result->engineName);

    if ($result->nbSeeders > 10 && str_contains($result->fileName, 'netinst')) {
        $result->download(); // -> Client::downloadSearchTorrent()
    }
}

$status->delete(); // after this, don't call getSearchStatus($id) again — the id 404s
```

### 5. RSS rules: reading and rewriting the nested `torrentParams` array

[](#5-rss-rules-reading-and-rewriting-the-nested-torrentparams-array)

`Rss\Rule::$torrentParams` is a raw nested array (the API's modern add-torrent-options payload), and four legacy top-level properties are one-way mirrors into it:

```
$rule = $qb->getRssRules()['tv-releases'];

var_dump($rule->torrentParams);
// ['category' => 'tv', 'save_path' => '/data/tv', 'content_layout' => 'Original', ...]

$rule->assignedCategory = 'anime';        // also sets $rule->torrentParams['category'] = 'anime'
$rule->torrentParams['save_path'] = '/data/anime'; // or write the nested array directly — same effect

$rule->save(); // POSTs jsonSerialize() (minus the synthesized `name`) to setRule
```

### 6. Categories and torrents together

[](#6-categories-and-torrents-together)

```
$qb->createCategory('archived', savePath: '/data/archive');

foreach ($qb->getTorrentsByCategory('linux') as $torrent) {
    if ($torrent->is_complete && $torrent->ratio >= 2.0) {
        $torrent->setCategory('archived');   // Client::setTorrentCategory + local field update
        $torrent->setSavePath('/data/archive');
    }
}

$categories = $qb->getCategories();          // string-keyed array of Category
echo $categories['archived']->savePath;      // "/data/archive"
$categories['archived']->edit(savePath: '/data/archive/linux'); // moves + updates local field
```

### 7. Serializing nested DTOs to JSON

[](#7-serializing-nested-dtos-to-json)

`jsonSerialize()` only emits properties that are actually initialized/loaded — lazy relations you never touched are simply absent, not `null`:

```
$torrent = $qb->getTorrents(['hashes' => $hash])[0];

echo json_encode($torrent, JSON_PRETTY_PRINT);
// { "hash": "...", "name": "...", ... }  files; // triggers the fetch

echo json_encode($torrent, JSON_PRETTY_PRINT);
// { "hash": "...", ..., "files": [ { "index": 0, "name": "...", ... }, ... ] }
// nested Torrent\File DTOs serialize themselves too — jsonSerialize() recurses naturally
```

### 8. Paging through the log instead of refetching everything

[](#8-paging-through-the-log-instead-of-refetching-everything)

```
$lastId = -1;
$allMessages = [];

do {
    $batch = $qb->getLog(['last_known_id' => $lastId]); // Log\Message[]
    $allMessages = [...$allMessages, ...$batch];
    $lastId = end($batch)?->id ?? $lastId;
} while (count($batch) > 0 && count($allMessages) < 5000);
```

### 9. Error handling

[](#9-error-handling)

Every network/auth failure surfaces as a plain `\Exception` (Guzzle exceptions are caught and rethrown, not passed through), and `addTorrentUrls()`/`addTorrentFile()`explicitly throw when qBittorrent reports zero torrents added:

```
try {
    $qb->addTorrentUrls($url);
} catch (\Exception $e) {
    // message is either the raw text/JSON body qBittorrent returned,
    // or an extracted error/message/reason field — see describeAddTorrentFailure()
    error_log("Add failed: {$e->getMessage()}");
}

try {
    $qb = (new Client($baseUri, $username, $wrongPassword))->login($username, $wrongPassword);
} catch (\Exception $e) {
    // "Authentication failed: ... (403)"
}
```

DTO reference
-------------

[](#dto-reference)

### `Torrent` (`DTO\Torrent`)

[](#torrent-dtotorrent)

From `getTorrents()` / `getTorrentsByCategory()` / `getTorrentsByHash()`. The main "everything about one torrent" object.

Key fields: `hash`, `infohash_v1`, `infohash_v2`, `name`, `magnet_uri`, `comment`, `state` (`Enum\TorrentState`), `progress` (0–1 float), `priority`, `size`, `total_size`, `downloaded`, `uploaded`, `dlspeed`, `upspeed`, `ratio`, `ratio_limit`, `share_limit_action` (`Enum\ShareLimitAction`), `share_limits_mode` (`Enum\ShareLimitsMode`), `num_seeds`, `num_leechs`, `eta`, `added_on`, `completion_on`, `category`, `tags` (comma-separated string), `save_path`, `download_path`, `tracker`, `dl_limit`, `up_limit`, `auto_tmm`, `super_seeding`, `seq_dl`, `force_start`, and more — see the file for the full list (~60 fields).

Computed (not real API fields): `progress_percent` (float, 0–100), `is_complete` (bool), `size_human` (string, e.g. `"1.2 GiB"`), `downloadable_files` (`File[]`, excludes files marked do-not-download).

Lazy relations: `files` (`File[]`), `trackers` (`Tracker[]`), `peers` (`Peer[]`), `pieces` (`Piece[]`), `webseeds` (`WebSeed[]`).

```
$torrent = $qb->getTorrents(['hashes' => $hash])[0];

$torrent->addTags(['linux-iso', 'archive']);      // Client::addTorrentTags + local update
$torrent->removeTags('archive');
$torrent->setCategory('linux');
$torrent->setSavePath('/data/downloads/linux');
$torrent->setDownloadPath('/data/incomplete/linux');
$torrent->setComment('verified checksum');
$torrent->setTags(['a', 'b']);                    // replaces, not merges

$torrent->stop();
$torrent->start();
$torrent->recheck();
$torrent->reannounce();
$torrent->delete(deleteFiles: true);

$torrent->setDownloadLimit(500_000);              // bytes/sec, local field updates immediately
$torrent->setUploadLimit(100_000);
$torrent->setShareLimits(ratioLimit: 2.0, seedingTimeLimit: 1440, inactiveSeedingTimeLimit: -1);

$torrent->increasePrio();
$torrent->topPrio();
$torrent->setFilePrio([0, 1], priority: 7);        // FilePriority::MAXIMUM

$torrent->setLocation('/mnt/nas/downloads');
$torrent->rename('New Name');
$torrent->renameFile('old/path.mkv', 'new/path.mkv');
$torrent->renameFolder('old-dir', 'new-dir');

$torrent->setAutoManagement(true);
$torrent->toggleSequentialDownload();
$torrent->toggleFirstLastPiecePrio();
$torrent->setForceStart(true);
$torrent->setSuperSeeding(true);

$torrent->addTrackers(['udp://tracker.example.com:80']);
$torrent->editTracker('udp://old', 'udp://new');
$torrent->removeTrackers('udp://dead-tracker.example.com:80');
$torrent->addPeers(['1.2.3.4:6881']);
$torrent->addWebSeeds('https://example.com/webseed/');

$bytes = $torrent->downloadFile($torrent->files[0]); // raw file bytes
$torrentFileBytes = $torrent->export();               // the .torrent itself
```

`pause`/`resume` are deliberately *not* wired onto `Torrent` (updating `state`locally would mean guessing the resulting enum value) — use `$qb->stop($hash)`/ `$qb->start($hash)` and refetch if you need the new state.

### `Torrent\Properties` (`DTO\Torrent\Properties`)

[](#torrentproperties-dtotorrentproperties)

From `getTorrentProperties($hash)`. A *different* field set from `Torrent`(different endpoint, `/torrents/properties`), not a subset — e.g. it has both `is_private` and `private`, and `dl_speed_avg`/`up_speed_avg` that `Torrent`lacks.

```
$props = $qb->getTorrentProperties($hash);
echo "{$props->seeds}/{$props->seeds_total} seeds, avg dl {$props->dl_speed_avg} B/s";
```

No action methods — read-only snapshot.

### `Torrent\File` (`DTO\Torrent\File`)

[](#torrentfile-dtotorrentfile)

From `Torrent::$files` or `getTorrentFiles($hash)`.

Fields: `index`, `name`, `size`, `progress`, `priority` (`Enum\FilePriority`), `is_seed`, `availability`, `piece_range`.

Computed: `progress_percent`, `is_complete`, `size_human`, `basename`, `dirname`, `url_path` (rawurlencode'd per path segment, no host — prefix your own download host).

```
foreach ($torrent->files as $file) {
    if ($file->priority->isDoNotDownload()) continue;
    echo "{$file->basename}: {$file->progress_percent}%\n";
}

$bytes = $qb->downloadFile($hash, $torrent->files[2]); // or pass the index directly
```

### `Torrent\Tracker` (`DTO\Torrent\Tracker`)

[](#torrenttracker-dtotorrenttracker)

From `Torrent::$trackers` or `getTorrentTrackers($hash)`.

Fields: `url`, `status` (`Enum\TrackerStatus`), `tier`, `num_peers`, `num_seeds`, `num_leeches`, `num_downloaded`, `msg`; multi-tracker (v2) entries also carry `name`, `updating`, `bt_version`, `next_announce`, `min_announce`, `endpoints`.

```
foreach ($torrent->trackers as $tracker) {
    if (!$tracker->status->isHealthy()) {
        echo "{$tracker->url}: {$tracker->msg}\n";
    }
}
```

No action methods — mutate via `Torrent::addTrackers()`/`editTracker()`/`removeTrackers()`.

### `Torrent\Peer` (`DTO\Torrent\Peer`)

[](#torrentpeer-dtotorrentpeer)

From `Torrent::$peers` or `getTorrentPeers($hash)` (backed by `/sync/torrentPeers`, not `/torrents/peers` — the latter 404s).

Fields: `ip`, `port`, `client`, `peer_id_client`, `country`, `country_code`, `progress`, `dl_speed`, `up_speed`, `downloaded`, `uploaded`, `connection`, `flags`, `flags_desc`, `relevance`, `files`, `contribution`, `host_name`, `i2p_dest`.

```
foreach ($torrent->peers as $peer) {
    echo "{$peer->ip}:{$peer->port} ({$peer->client}) {$peer->progress}\n";
}
```

### `Torrent\Piece` (`DTO\Torrent\Piece`)

[](#torrentpiece-dtotorrentpiece)

From `Torrent::$pieces` or `getTorrentPiecesStates($hash)` (the raw API returns a flat `[state, state, ...]` array; the client zips it into `{index, state}`pairs).

Fields: `index`, `state` (`Enum\PieceState`). Computed: `is_downloaded`, `is_downloading`.

```
$have = array_filter($torrent->pieces, fn ($p) => $p->is_downloaded);
```

### `Torrent\WebSeed` (`DTO\Torrent\WebSeed`)

[](#torrentwebseed-dtotorrentwebseed)

From `Torrent::$webseeds` or `getTorrentWebSeeds($hash)`. One field: `url`. Mutate via `Torrent::addWebSeeds()`/`editWebSeed()`/`removeWebSeeds()`.

### `Category` (`DTO\Category`)

[](#category-dtocategory)

From `getCategories()`.

Fields: `name`, `savePath` (legacy camelCase, always a string), `download_path`(snake\_case, nullable — null for the built-in `RSS` category), `ratio_limit`, `seeding_time_limit`, `inactive_seeding_time_limit`, `share_limit_action` (`Enum\ShareLimitAction`), `share_limits_mode` (`Enum\ShareLimitsMode`).

```
$categories = $qb->getCategories();
$categories['linux']->edit(savePath: '/data/linux');
$categories['old-stuff']->delete();
```

`edit()` only updates `name`/`savePath` — that's all `editCategory()` accepts server-side; share-limit fields aren't editable through this endpoint.

### `Cookie` (`DTO\Cookie`)

[](#cookie-dtocookie)

From `getCookies()`; also constructed manually for `setCookies()` / `importCookiesJson()` / `importCookieHeader()` / `importCurlCookies()` / `importBrowserCookies()`.

Fields: `name`, `domain`, `path`, `value`, `expirationDate` (seconds since epoch — the setter accepts an `int`, a date string, or any `DateTimeInterface`and normalizes it).

```
$qb->setCookies([
    new Cookie(['name' => 'session', 'value' => 'abc', 'domain' => 'example.com', 'path' => '/', 'expirationDate' => '+30 days']),
]);

// or import from a browser export / Netscape cookie file:
$qb->importBrowserCookies('/home/me/.mozilla/.../cookies.sqlite', domains: 'example.com');
$qb->importCurlCookies(file_get_contents('cookies.txt'), domains: ['example.com']);
$qb->clearCookies(); // empties the store
```

`setCookies()` merges into the existing store by `name+domain+path` by default; pass `merge: false` to replace it outright.

### `BuildInfo` (`DTO\BuildInfo`)

[](#buildinfo-dtobuildinfo)

From `getBuildInfo()`. Flat, read-only: `bitness`, `boost`, `libtorrent`, `openssl`, `platform`, `qt`, `zlib`.

```
$info = $qb->getBuildInfo();
echo "libtorrent {$info->libtorrent} on {$info->platform}";
```

### `Transfer` (`DTO\Transfer`)

[](#transfer-dtotransfer)

From `getTransferInfo()`. Global (not per-torrent) transfer state.

Fields: `connection_status`, `dht_nodes`, `dl_info_data`, `dl_info_speed`, `dl_rate_limit`, `up_info_data`, `up_info_speed`, `up_rate_limit`, `last_external_address_v4`, `last_external_address_v6`.

```
$t = $qb->getTransferInfo();

$t->speedLimitsMode();        // bool — alt speed limits active?
$t->toggleSpeedLimitsMode();
$t->setDownloadLimit(1_000_000);  // global, bytes/sec — distinct from per-torrent limits
$t->setUploadLimit(500_000);
$t->banPeers(['1.2.3.4:6881', '5.6.7.8:6882']);
```

Note: qBittorrent rounds a submitted byte limit to the nearest KiB server-side, but the local field is set to the exact value passed — it can drift by up to ~1023 bytes from the server until you refetch.

### `MainData` (`DTO\MainData`)

[](#maindata-dtomaindata)

From `syncMainData($query)` — the WebUI's polling endpoint, wraps everything in one diffable payload. Composite/untyped: `rid`, `full_update`, `torrents`, `torrents_removed`, `categories`, `categories_removed`, `tags`, `tags_removed`, `trackers`, `trackers_removed`, `server_state` are all raw arrays (no natural single-entity DTO home). Pass `['rid' => $lastRid]` to get an incremental diff instead of the full state.

### `Log\Message` (`DTO\Log\Message`)

[](#logmessage-dtologmessage)

From `getLog($query)` (`/log/main`). Fields: `id`, `message`, `timestamp`, `type` (`Enum\LogMessageType` — a bitmask on the wire, but each entry only ever carries one bit). Flat, no action methods. `getLog()` defaults to `last_known_id=-1` (returns everything, capped at 20000 entries) — pass `['last_known_id' => $id]` to page forward.

### `Log\Peer` (`DTO\Log\Peer`)

[](#logpeer-dtologpeer)

From `getPeerLog($query)` (`/log/peers`). Fields: `id`, `ip`, `timestamp`, `blocked`, `reason`. Flat, no action methods.

### `Search\Result` (`DTO\Search\Result`)

[](#searchresult-dtosearchresult)

From `Search\Status::results()` or `getSearchResults($id)`.

Fields: `fileName`, `fileUrl`, `fileSize`, `nbSeeders`, `nbLeechers`, `engineName`, `siteUrl`, `descrLink`, `pubDate`.

```
foreach ($status->results() as $result) {
    if ($result->nbSeeders > 5) $result->download(); // downloadSearchTorrent()
}
```

### `Search\Status` (`DTO\Search\Status`)

[](#searchstatus-dtosearchstatus)

From `getSearchStatus($id)`. Fields: `id`, `status` (`Enum\SearchStatus`: `RUNNING`/`STOPPED`), `total`.

```
$id = $qb->startSearch('debian iso', plugins: 'all', category: 'all');
$status = $qb->getSearchStatus($id)[0];

$status->results(limit: 20);
$status->stop();
$status->delete();
```

After `delete()`, don't call `getSearchStatus()` on that id again — the server 404s instead of returning an empty result once the id is gone.

### `Search\Plugin` (`DTO\Search\Plugin`)

[](#searchplugin-dtosearchplugin)

From `getSearchPlugins()`. Fields: `name`, `fullName`, `url`, `version`, `enabled`, `supportedCategories` (`{id, name}[]`).

```
foreach ($qb->getSearchPlugins() as $plugin) {
    if (!$plugin->enabled) $plugin->enable();
}
$qb->getSearchPlugins()['legittorrents']->uninstall();
$qb->installSearchPlugin('https://example.com/my-plugin.py');
$qb->updateSearchPlugins();
```

### `Rss\Feed` (`DTO\Rss\Feed`)

[](#rssfeed-dtorssfeed)

From `getRssFeeds($query)` — `/rss/items` returns a tree keyed by feed/folder name; a node with a `uid` key becomes a `Feed`, anything else is a plain nested array (no `Folder` DTO — recurse yourself).

Fields: `path` (synthesized from the node's position in the tree, not a real API field), `uid`, `url`, `refreshInterval` (only present when &gt; 0). With `withData=true`: `title`, `lastBuildDate`, `isLoading`, `hasError`, `articles`.

```
$feeds = $qb->getRssFeeds(['withData' => true]);
$linux = $feeds['tech']['linux-distros']; // nested by folder

$linux->setUrl('https://example.com/new-feed.xml');
$linux->setRefreshInterval(1800);
$linux->refresh();
$linux->markAsRead();
$linux->move('archive/linux-distros');
$linux->remove();

$qb->addRssFolder('tech');
$qb->addRssFeed('https://example.com/feed.xml', 'tech/new-feed');
```

### `Rss\Rule` (`DTO\Rss\Rule`)

[](#rssrule-dtorssrule)

From `getRssRules()`.

Fields: `name` (synthesized from the rule's key, not a real field), `enabled`, `priority`, `useRegex`, `mustContain`, `mustNotContain`, `episodeFilter`, `affectedFeeds`, `lastMatch`, `ignoreDays`, `smartFilter`, `previouslyMatchedEpisodes`, `torrentParams` (raw array — modern field, docs omit it). Deprecated but still-writable-through fields `addPaused`, `torrentContentLayout`, `savePath`, `assignedCategory` mirror themselves into `torrentParams` on every assignment (that's what the server actually reads).

Unlike other DTOs there's no per-field endpoint — only whole-object `setRule` — so this one gets `save()` instead of individual setters:

```
$rules = $qb->getRssRules();
$rule = $rules['new-releases'];

$rule->mustContain = '1080p|2160p';
$rule->assignedCategory = 'tv';       // also sets torrentParams['category']
$rule->save();                         // POSTs the whole object

$rule->rename('tv-releases');
$rule->clone('tv-releases-copy');      // undocumented endpoint, not live until qBittorrent ships it past 5.2.2
$rule->matchingArticles();
$rule->delete();

$qb->addRssRule('new-rule', [
    'enabled' => true,
    'mustContain' => 'S\d{2}E\d{2}',
    'affectedFeeds' => ['tech\\linux-distros'],
    'torrentParams' => ['category' => 'tv', 'save_path' => '/data/tv'],
]);
```

### `Preferences` (`DTO\Preferences`)

[](#preferences-dtopreferences)

From `getPreferences()`. Flat but huge (~220 fields covering every WebUI settings-page option: connection, BitTorrent, download paths, proxy, WebUI security, RSS, mail notifications, scheduler, etc). Generated from a live response rather than hand-typed — see the source file for the full list. Notable: `share_limits_mode` (`Enum\ShareLimitsMode`), `max_ratio` (float).

Read-only DTO — there's no per-field setter wired up, because the API itself has none. Push changes with the raw `Client` method instead, which PATCHes only the keys you pass:

```
$prefs = $qb->getPreferences();
echo $prefs->web_ui_port;

$qb->setPreferences([
    'dl_limit' => 5_000_000,
    'max_ratio' => 2.0,
    'max_ratio_enabled' => true,
]);
```

Enums
-----

[](#enums)

All under `Blackout\Qbittorrent\Enum`, all backed, all use the `HasLabel`trait (`->label()` → title-cased name, e.g. `TorrentState::STALLED_DL->label()`→ `"Stalled dl"`).

EnumBackingCases`TorrentState`string`ERROR`, `MISSING_FILES`, `DOWNLOADING`, `UPLOADING`, `STOPPED_DL`, `STOPPED_UP`, `QUEUED_DL`, `QUEUED_UP`, `STALLED_DL`, `STALLED_UP`, `CHECKING_DL`, `CHECKING_UP`, `CHECKING_RESUME_DATA`, `FORCED_DL`, `FORCED_UP`, `META_DL`, `FORCED_META_DL`, `MOVING`, `UNKNOWN``FilePriority`int`DO_NOT_DOWNLOAD`(0), `NORMAL`(1), `HIGH`(6), `MAXIMUM`(7) — plus `isDoNotDownload()`/`isNormal()`/`isHigh()``TrackerStatus`int`DISABLED`(0), `NOT_CONTACTED`(1), `WORKING`(2), `NOT_WORKING`(4), `TRACKER_ERROR`(5), `UNREACHABLE`(6) — plus `isHealthy()`/`isActive()``PieceState`int`NOT_DOWNLOADED`(0), `DOWNLOADING`(1), `DOWNLOADED`(2)`LogMessageType`int`NORMAL`(1), `INFO`(2), `WARNING`(4), `CRITICAL`(8)`SearchStatus`string`RUNNING`, `STOPPED``ShareLimitAction`string`DEFAULT`, `STOP`, `REMOVE`, `REMOVE_WITH_CONTENT`, `ENABLE_SUPER_SEEDING``ShareLimitsMode`string`DEFAULT`, `MATCH_ANY`, `MATCH_ALL`Client coverage
---------------

[](#client-coverage)

`Client` covers the full qBittorrent WebUI API v2 surface: `auth`, `app`(version/preferences/cookies/directory browsing), `log`, `sync`, `transfer`, `torrents` (info/add/delete/manage/trackers/peers/priority/limits/tags/ categories), `rss` (feeds/rules), `search`, and `torrentcreator`. Methods that return a single entity or entity list return the matching DTO above; the rest (composite/system-info endpoints with no natural single-entity shape) return raw arrays. See `src/Qbittorrent/Client.php` for the full method list.

Notes
-----

[](#notes)

- Coding style: tabs, Allman braces (see `phpcs.xml` in the `qbittorrent-tools`repo — not duplicated here).
- Consumed by `UnkyBadTouch/qbittorrent-tools` as a composer VCS dependency — a breaking change here (namespace/signature) needs a version bump and coordinated update there.

###  Health Score

42

—

FairBetter than 88% of packages

Maintenance100

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity52

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

2

Last Release

0d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/80658878?v=4)[UnkyBadTouch](/maintainers/UnkyBadTouch)[@UnkyBadTouch](https://github.com/UnkyBadTouch)

---

Top Contributors

[![UnkyBadTouch](https://avatars.githubusercontent.com/u/80658878?v=4)](https://github.com/UnkyBadTouch "UnkyBadTouch (21 commits)")

### Embed Badge

![Health badge](/badges/unkybadtouch-qbbittorrent/health.svg)

```
[![Health](https://phpackages.com/badges/unkybadtouch-qbbittorrent/health.svg)](https://phpackages.com/packages/unkybadtouch-qbbittorrent)
```

###  Alternatives

[aws/aws-sdk-php

AWS SDK for PHP - Use Amazon Web Services in your PHP project

6.2k543.5M2.7k](/packages/aws-aws-sdk-php)[neuron-core/neuron-ai

The PHP Agentic Framework.

2.0k656.1k46](/packages/neuron-core-neuron-ai)[tencentcloud/tencentcloud-sdk-php

TencentCloudApi php sdk

3661.3M47](/packages/tencentcloud-tencentcloud-sdk-php)[eslazarev/wildberries-sdk

Wildberries OpenAPI clients (generated).

293.1k](/packages/eslazarev-wildberries-sdk)[tempest/framework

The PHP framework that gets out of your way.

2.2k34.4k16](/packages/tempest-framework)[lion/bundle

Lion-framework configuration and initialization package

122.4k4](/packages/lion-bundle)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
