qbt-kit 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qbt_kit-0.1.0/LICENSE +21 -0
- qbt_kit-0.1.0/PKG-INFO +288 -0
- qbt_kit-0.1.0/README.md +259 -0
- qbt_kit-0.1.0/pyproject.toml +117 -0
- qbt_kit-0.1.0/pyproject.toml.orig +116 -0
- qbt_kit-0.1.0/src/qbtkit/__init__.py +0 -0
- qbt_kit-0.1.0/src/qbtkit/cli.py +450 -0
- qbt_kit-0.1.0/src/qbtkit/client.py +403 -0
- qbt_kit-0.1.0/src/qbtkit/compat.py +131 -0
- qbt_kit-0.1.0/src/qbtkit/coverage.py +128 -0
- qbt_kit-0.1.0/src/qbtkit/errors.py +141 -0
- qbt_kit-0.1.0/src/qbtkit/inventory/master.json +667 -0
- qbt_kit-0.1.0/src/qbtkit/inventory/release-4.1.9.json +397 -0
- qbt_kit-0.1.0/src/qbtkit/inventory/release-4.4.5.json +472 -0
- qbt_kit-0.1.0/src/qbtkit/inventory/release-5.2.3.json +622 -0
- qbt_kit-0.1.0/src/qbtkit/mcp/__init__.py +31 -0
- qbt_kit-0.1.0/src/qbtkit/mcp/server.py +752 -0
- qbt_kit-0.1.0/src/qbtkit/py.typed +0 -0
- qbt_kit-0.1.0/src/qbtkit/spec/__init__.py +31 -0
- qbt_kit-0.1.0/src/qbtkit/spec/_types.py +217 -0
- qbt_kit-0.1.0/src/qbtkit/spec/app.py +224 -0
- qbt_kit-0.1.0/src/qbtkit/spec/auth.py +33 -0
- qbt_kit-0.1.0/src/qbtkit/spec/clientdata.py +33 -0
- qbt_kit-0.1.0/src/qbtkit/spec/log.py +30 -0
- qbt_kit-0.1.0/src/qbtkit/spec/rss.py +167 -0
- qbt_kit-0.1.0/src/qbtkit/spec/search.py +151 -0
- qbt_kit-0.1.0/src/qbtkit/spec/sync.py +31 -0
- qbt_kit-0.1.0/src/qbtkit/spec/torrentcreator.py +79 -0
- qbt_kit-0.1.0/src/qbtkit/spec/torrents.py +732 -0
- qbt_kit-0.1.0/src/qbtkit/spec/transfer.py +89 -0
qbt_kit-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mt
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
qbt_kit-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: qbt-kit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Spec-driven client, 1:1 CLI and MCP server for the qBittorrent WebUI API
|
|
5
|
+
Keywords: qbittorrent,torrent,cli,mcp,webui-api
|
|
6
|
+
Author: mt
|
|
7
|
+
Author-email: mt <manuel+git@matm.xyz>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: System Administrators
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Topic :: Communications :: File Sharing
|
|
18
|
+
Requires-Dist: httpx>=0.28
|
|
19
|
+
Requires-Dist: typer>=0.27
|
|
20
|
+
Requires-Dist: mcp>=2.3 ; extra == 'mcp'
|
|
21
|
+
Requires-Dist: pydantic>=2.0 ; extra == 'mcp'
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Project-URL: Homepage, https://github.com/manueltarouca/qbt-kit
|
|
24
|
+
Project-URL: Issues, https://github.com/manueltarouca/qbt-kit/issues
|
|
25
|
+
Project-URL: Changelog, https://github.com/manueltarouca/qbt-kit/blob/main/CHANGELOG.md
|
|
26
|
+
Project-URL: Documentation, https://github.com/manueltarouca/qbt-kit#readme
|
|
27
|
+
Provides-Extra: mcp
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# qbt-kit
|
|
31
|
+
|
|
32
|
+
<a href="https://github.com/manueltarouca/qbt-kit/actions/workflows/ci.yaml"><img src="https://github.com/manueltarouca/qbt-kit/actions/workflows/ci.yaml/badge.svg" alt="CI status" /></a>
|
|
33
|
+
<a href="https://pypi.org/project/qbt-kit/"><img src="https://img.shields.io/pypi/v/qbt-kit.svg" alt="Latest PyPI version" /></a>
|
|
34
|
+
<a href="https://pypi.org/project/qbt-kit/"><img src="https://img.shields.io/pypi/pyversions/qbt-kit.svg" alt="Supported Python versions" /></a>
|
|
35
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/licence-MIT-blue.svg" alt="MIT licence" /></a>
|
|
36
|
+
|
|
37
|
+
<p align="center">
|
|
38
|
+
<picture>
|
|
39
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/manueltarouca/qbt-kit/main/assets/logo-dark.png">
|
|
40
|
+
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/manueltarouca/qbt-kit/main/assets/logo.png">
|
|
41
|
+
<img alt="qbt-kit" src="https://raw.githubusercontent.com/manueltarouca/qbt-kit/main/assets/logo.png" width="180">
|
|
42
|
+
</picture>
|
|
43
|
+
</p>
|
|
44
|
+
|
|
45
|
+
A complete, typed Python client for the qBittorrent WebUI API — with a CLI that
|
|
46
|
+
covers every endpoint one-to-one, and an MCP server so coding agents can drive
|
|
47
|
+
it without deleting your data.
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
$ qbt coverage
|
|
51
|
+
|
|
52
|
+
qbt-kit coverage — target release-5.2.3
|
|
53
|
+
|
|
54
|
+
scope n spec progress
|
|
55
|
+
torrents 61 61 ████████████████████ 61/61
|
|
56
|
+
app 16 16 ████████████████████ 16/16
|
|
57
|
+
rss 14 14 ████████████████████ 14/14
|
|
58
|
+
search 11 11 ████████████████████ 11/11
|
|
59
|
+
transfer 9 9 ████████████████████ 9/9
|
|
60
|
+
torrentcreator 4 4 ████████████████████ 4/4
|
|
61
|
+
auth 2 2 ████████████████████ 2/2
|
|
62
|
+
clientdata 2 2 ████████████████████ 2/2
|
|
63
|
+
log 2 2 ████████████████████ 2/2
|
|
64
|
+
sync 2 2 ████████████████████ 2/2
|
|
65
|
+
----
|
|
66
|
+
TOTAL 123 123 ████████████████████ 100.0%
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
<p align="center">
|
|
70
|
+
<i>The ledger is generated from qBittorrent's own source, and CI fails if it slips.</i>
|
|
71
|
+
</p>
|
|
72
|
+
|
|
73
|
+
## Highlights
|
|
74
|
+
|
|
75
|
+
- **All 123 endpoints** of qBittorrent 5.2.3, each one a spec entry that
|
|
76
|
+
generates the client method, the CLI command and the docs together.
|
|
77
|
+
- **A verifiable coverage claim.** The endpoint list is extracted from the
|
|
78
|
+
qBittorrent C++ source per release tag and committed, so CI fails both when
|
|
79
|
+
the spec misses an endpoint and when it invents one.
|
|
80
|
+
- **An MCP server** with 15 tools — 13 task-shaped, plus `qbt_call` and
|
|
81
|
+
`list_endpoints` — and safety gates that hold even through the escape hatch.
|
|
82
|
+
- **Compatibility measured, not assumed** — tested against real 4.6, 5.0, 5.1
|
|
83
|
+
and 5.2 containers, with a rename table so `torrents/stop` resolves to
|
|
84
|
+
`pause` on servers that predate the rename.
|
|
85
|
+
- **Typed throughout**, no runtime dependency beyond `httpx` and `typer`.
|
|
86
|
+
|
|
87
|
+
That coverage claim is worth a sentence of context. The wiki page most clients
|
|
88
|
+
are written against documents **78** endpoints. qBittorrent 5.2.3 ships
|
|
89
|
+
**123**, and two whole scopes — `torrentcreator` and `clientdata` — appear in
|
|
90
|
+
no wiki page at all.
|
|
91
|
+
|
|
92
|
+
## Installation
|
|
93
|
+
|
|
94
|
+
PyPI's `qbt` name belongs to an unrelated package, so the CLI is run via
|
|
95
|
+
`--from`:
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
uvx --from qbt-kit qbt doctor
|
|
99
|
+
uvx qbt-mcp --help # the MCP server ships under its own name
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Or add it to a project:
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
uv add qbt-kit # library and CLI
|
|
106
|
+
uv add "qbt-kit[mcp]" # with the MCP server
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### As a Claude Code plugin
|
|
110
|
+
|
|
111
|
+
This repository is also a plugin marketplace, so one line installs the MCP
|
|
112
|
+
server and the skill together:
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
/plugin install qbt-kit --marketplace manueltarouca/qbt-kit
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The bundled MCP server runs from the plugin's own checkout, so it needs `uv`
|
|
119
|
+
but not a published package, and it always matches the version you installed.
|
|
120
|
+
Point it at your server with `QBT_URL`, and see [Configuration](#configuration)
|
|
121
|
+
for credentials.
|
|
122
|
+
|
|
123
|
+
## Quickstart
|
|
124
|
+
|
|
125
|
+
```sh
|
|
126
|
+
qbt doctor # version, auth mode, endpoint availability
|
|
127
|
+
qbt torrents info --category tv # the API calls this `info`, not `list`
|
|
128
|
+
qbt torrents add --urls <magnet> --category tv --auto-tmm
|
|
129
|
+
qbt health # swarm health across the queue
|
|
130
|
+
qbt coverage # the endpoint coverage ledger
|
|
131
|
+
qbt call torrents/pieceStates -p hash=<h> # raw, reaches all 123
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Scope commands mirror the API exactly — `torrents info`, not a friendlier
|
|
135
|
+
`list` — so anything you read in qBittorrent's own docs translates directly.
|
|
136
|
+
`--json` works everywhere, so every command composes with `jq`.
|
|
137
|
+
|
|
138
|
+
`qbt ready` is also there, but it inspects the local filesystem to decide
|
|
139
|
+
whether a download has really settled, so it only means anything when the CLI
|
|
140
|
+
runs on the same machine as qBittorrent.
|
|
141
|
+
|
|
142
|
+
## MCP server
|
|
143
|
+
|
|
144
|
+
```jsonc
|
|
145
|
+
{
|
|
146
|
+
"mcpServers": {
|
|
147
|
+
"qbittorrent": {
|
|
148
|
+
"command": "uvx",
|
|
149
|
+
"args": ["qbt-mcp", "--url", "http://127.0.0.1:8080"]
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Fifteen tools rather than 123 — a tool list that long costs context
|
|
156
|
+
on every turn and measurably worsens which tool a model picks. `qbt_call` keeps
|
|
157
|
+
every endpoint reachable and `list_endpoints` lets a model discover them, so the
|
|
158
|
+
small surface stays complete.
|
|
159
|
+
|
|
160
|
+
### Configuration
|
|
161
|
+
|
|
162
|
+
Flags first, then the environment, then the macOS Keychain:
|
|
163
|
+
|
|
164
|
+
| variable | meaning |
|
|
165
|
+
|---|---|
|
|
166
|
+
| `QBT_URL` | base URL, default `http://127.0.0.1:8080` |
|
|
167
|
+
| `QBT_API_KEY` | API key (qBittorrent 5.2+); preferred when present |
|
|
168
|
+
| `QBT_USERNAME` | WebUI username, for cookie login on any version |
|
|
169
|
+
| `QBT_PASSWORD` | WebUI password |
|
|
170
|
+
| `QBT_READ_ONLY` | `1`/`true`/`yes` to refuse everything that changes state |
|
|
171
|
+
|
|
172
|
+
Keychain fallback uses service `qbittorrent`, account `api-key` for a key and
|
|
173
|
+
`webui` for the password:
|
|
174
|
+
|
|
175
|
+
```sh
|
|
176
|
+
security add-generic-password -s qbittorrent -a api-key -w '<key>'
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**qBittorrent has no OAuth**, so there is no token flow to delegate and the
|
|
180
|
+
secret is long-lived. The reference MCP servers put credentials straight into
|
|
181
|
+
the client's `env` block — `GITHUB_PERSONAL_ACCESS_TOKEN` is the canonical
|
|
182
|
+
example — but that file is plaintext and frequently committed. Prefer the
|
|
183
|
+
Keychain, and if you do use `env`, reference a variable instead of pasting the
|
|
184
|
+
secret:
|
|
185
|
+
|
|
186
|
+
```jsonc
|
|
187
|
+
"env": {
|
|
188
|
+
"QBT_URL": "${QBT_URL:-http://127.0.0.1:8080}",
|
|
189
|
+
"QBT_API_KEY": "${QBT_API_KEY:-}" // empty falls through to the Keychain
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The `:-` default matters. Claude Code passes an unset `${VAR}` through as the
|
|
194
|
+
*literal text* `${VAR}` rather than as empty, which otherwise surfaces as a
|
|
195
|
+
confusing 401 much later — so `qbt-mcp` detects that case at startup and says
|
|
196
|
+
so plainly instead of starting.
|
|
197
|
+
|
|
198
|
+
On startup it reports, on stderr, where the credentials came from and never
|
|
199
|
+
what they are:
|
|
200
|
+
|
|
201
|
+
```
|
|
202
|
+
qbt-mcp: http://127.0.0.1:8080 · WebAPI 2.15.1 · auth=bearer via macOS Keychain
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Safety
|
|
206
|
+
|
|
207
|
+
An agent driving a torrent client can destroy data that took weeks to collect,
|
|
208
|
+
so the gates are code rather than guidance:
|
|
209
|
+
|
|
210
|
+
- Destructive endpoints require `confirm`; `delete_files` is never implied.
|
|
211
|
+
- `--read-only` (or `QBT_READ_ONLY=1`) blocks mutations, **including via
|
|
212
|
+
`qbt_call`**.
|
|
213
|
+
- `app/setPreferences` refuses the WebUI, auth and network keys that would
|
|
214
|
+
sever the connection or weaken security. There is no override.
|
|
215
|
+
- Refusals return a structured cause and remedy, so an agent can correct itself
|
|
216
|
+
instead of seeing an opaque tool error.
|
|
217
|
+
|
|
218
|
+
`qbt-mcp` speaks stdio by default, which keeps it a child process with nothing
|
|
219
|
+
listening. The `sse` and `streamable-http` transports expose an
|
|
220
|
+
*unauthenticated* control surface holding your qBittorrent credentials — bound
|
|
221
|
+
to loopback, but do not forward those ports. See [SECURITY.md](https://github.com/manueltarouca/qbt-kit/blob/main/SECURITY.md).
|
|
222
|
+
|
|
223
|
+
## Documentation
|
|
224
|
+
|
|
225
|
+
| | |
|
|
226
|
+
|---|---|
|
|
227
|
+
| [`docs/TRACEABILITY.md`](https://github.com/manueltarouca/qbt-kit/blob/main/docs/TRACEABILITY.md) | how the coverage claim is built and gated |
|
|
228
|
+
| [`docs/TESTING.md`](https://github.com/manueltarouca/qbt-kit/blob/main/docs/TESTING.md) | the container harness, and behaviours that cost real time to learn |
|
|
229
|
+
| [`docs/PLAN.md`](https://github.com/manueltarouca/qbt-kit/blob/main/docs/PLAN.md) | the design and the reasoning behind it |
|
|
230
|
+
| [`docs/ROADMAP.md`](https://github.com/manueltarouca/qbt-kit/blob/main/docs/ROADMAP.md) | what is done and what is not |
|
|
231
|
+
| [`docs/REVIEW-1.md`](https://github.com/manueltarouca/qbt-kit/blob/main/docs/REVIEW-1.md) | an independent review of the plan, with every finding dispositioned |
|
|
232
|
+
| [`docs/dependencies.md`](https://github.com/manueltarouca/qbt-kit/blob/main/docs/dependencies.md) | every dependency, and why it is justified |
|
|
233
|
+
| [`SECURITY.md`](https://github.com/manueltarouca/qbt-kit/blob/main/SECURITY.md) | threat model, credential handling, how to report a vulnerability |
|
|
234
|
+
|
|
235
|
+
A Claude skill ships in [`skills/qbt-kit/`](https://github.com/manueltarouca/qbt-kit/tree/main/skills/qbt-kit/) covering CLI usage
|
|
236
|
+
and the swarm-health rule.
|
|
237
|
+
|
|
238
|
+
## Field notes
|
|
239
|
+
|
|
240
|
+
Things that cost us time, documented so they cost you none.
|
|
241
|
+
|
|
242
|
+
- **Login success cannot be read from the status code.** qBittorrent ≤5.1
|
|
243
|
+
answers a *failed* login with HTTP 200 and the body `Fails.`; 5.2 returns an
|
|
244
|
+
empty 204. The only rule that holds across versions is whether a session
|
|
245
|
+
cookie was issued.
|
|
246
|
+
- **`app/webapiVersion` requires authentication.** Only `auth/login` is public,
|
|
247
|
+
so you cannot read the version to decide how to authenticate. Probe instead.
|
|
248
|
+
- **The session cookie is not always `SID`.** On 5.2 it is `QBT_SID_<port>`;
|
|
249
|
+
on 5.0/5.1 it is user-configurable. Never match on the name.
|
|
250
|
+
- **A bare 401 is often Host-header validation, not bad credentials.** It is on
|
|
251
|
+
by default and rejects any request whose `Host` port differs from the port
|
|
252
|
+
qBittorrent listens on — which is what happens behind mapped container ports,
|
|
253
|
+
SSH tunnels and most reverse proxies.
|
|
254
|
+
- **API keys exist only from qBittorrent 5.2.0.** Sending `Authorization:
|
|
255
|
+
Bearer` to 5.0/5.1 gets a 403 that looks like a credentials problem.
|
|
256
|
+
- **`pause`/`resume` were removed in 5.0**, not aliased; they 404 there.
|
|
257
|
+
- **`progress: 1.0` does not mean every file is on disk.** Progress is computed
|
|
258
|
+
over the *selected* files, so a torrent with files at priority 0 reports 100%
|
|
259
|
+
having never fetched them. `size < total_size` is the cheap flag, from the
|
|
260
|
+
same listing call.
|
|
261
|
+
- **A category alone does not place a torrent.** qBittorrent only applies a
|
|
262
|
+
category's save path under automatic torrent management, so `torrents/add`
|
|
263
|
+
with a category but without `autoTMM` files it in the global save path.
|
|
264
|
+
- **A stalled torrent is usually not slow but stuck.** `availability` is how
|
|
265
|
+
many complete copies exist across reachable peers; when it does not exceed
|
|
266
|
+
`progress`, no peer has the missing pieces. But a *queued* torrent reports
|
|
267
|
+
availability 0 simply for not being connected — judging those as dead is how
|
|
268
|
+
you delete healthy downloads.
|
|
269
|
+
|
|
270
|
+
## Contributing
|
|
271
|
+
|
|
272
|
+
See [CONTRIBUTING.md](https://github.com/manueltarouca/qbt-kit/blob/main/CONTRIBUTING.md). The short version: endpoints are added
|
|
273
|
+
to the spec under `src/qbtkit/spec/`, never by hand to the CLI or MCP, and
|
|
274
|
+
`tools/build_coverage.py --strict` has to stay green.
|
|
275
|
+
|
|
276
|
+
## Acknowledgements
|
|
277
|
+
|
|
278
|
+
- [qBittorrent](https://github.com/qbittorrent/qBittorrent) — and specifically
|
|
279
|
+
its `src/webui/api/*controller.cpp`, which is the only complete and accurate
|
|
280
|
+
description of this API that exists.
|
|
281
|
+
- [`qbittorrent-api`](https://pypi.org/project/qbittorrent-api/) is the mature,
|
|
282
|
+
well-maintained Python library for this API, and if a library is all you
|
|
283
|
+
need, use it. qbt-kit exists for the CLI, the MCP server and the verifiable
|
|
284
|
+
coverage claim, not because anything is wrong with it.
|
|
285
|
+
|
|
286
|
+
## Licence
|
|
287
|
+
|
|
288
|
+
MIT — see [LICENSE](https://github.com/manueltarouca/qbt-kit/blob/main/LICENSE).
|
qbt_kit-0.1.0/README.md
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# qbt-kit
|
|
2
|
+
|
|
3
|
+
<a href="https://github.com/manueltarouca/qbt-kit/actions/workflows/ci.yaml"><img src="https://github.com/manueltarouca/qbt-kit/actions/workflows/ci.yaml/badge.svg" alt="CI status" /></a>
|
|
4
|
+
<a href="https://pypi.org/project/qbt-kit/"><img src="https://img.shields.io/pypi/v/qbt-kit.svg" alt="Latest PyPI version" /></a>
|
|
5
|
+
<a href="https://pypi.org/project/qbt-kit/"><img src="https://img.shields.io/pypi/pyversions/qbt-kit.svg" alt="Supported Python versions" /></a>
|
|
6
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/licence-MIT-blue.svg" alt="MIT licence" /></a>
|
|
7
|
+
|
|
8
|
+
<p align="center">
|
|
9
|
+
<picture>
|
|
10
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/manueltarouca/qbt-kit/main/assets/logo-dark.png">
|
|
11
|
+
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/manueltarouca/qbt-kit/main/assets/logo.png">
|
|
12
|
+
<img alt="qbt-kit" src="https://raw.githubusercontent.com/manueltarouca/qbt-kit/main/assets/logo.png" width="180">
|
|
13
|
+
</picture>
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
A complete, typed Python client for the qBittorrent WebUI API — with a CLI that
|
|
17
|
+
covers every endpoint one-to-one, and an MCP server so coding agents can drive
|
|
18
|
+
it without deleting your data.
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
$ qbt coverage
|
|
22
|
+
|
|
23
|
+
qbt-kit coverage — target release-5.2.3
|
|
24
|
+
|
|
25
|
+
scope n spec progress
|
|
26
|
+
torrents 61 61 ████████████████████ 61/61
|
|
27
|
+
app 16 16 ████████████████████ 16/16
|
|
28
|
+
rss 14 14 ████████████████████ 14/14
|
|
29
|
+
search 11 11 ████████████████████ 11/11
|
|
30
|
+
transfer 9 9 ████████████████████ 9/9
|
|
31
|
+
torrentcreator 4 4 ████████████████████ 4/4
|
|
32
|
+
auth 2 2 ████████████████████ 2/2
|
|
33
|
+
clientdata 2 2 ████████████████████ 2/2
|
|
34
|
+
log 2 2 ████████████████████ 2/2
|
|
35
|
+
sync 2 2 ████████████████████ 2/2
|
|
36
|
+
----
|
|
37
|
+
TOTAL 123 123 ████████████████████ 100.0%
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
<p align="center">
|
|
41
|
+
<i>The ledger is generated from qBittorrent's own source, and CI fails if it slips.</i>
|
|
42
|
+
</p>
|
|
43
|
+
|
|
44
|
+
## Highlights
|
|
45
|
+
|
|
46
|
+
- **All 123 endpoints** of qBittorrent 5.2.3, each one a spec entry that
|
|
47
|
+
generates the client method, the CLI command and the docs together.
|
|
48
|
+
- **A verifiable coverage claim.** The endpoint list is extracted from the
|
|
49
|
+
qBittorrent C++ source per release tag and committed, so CI fails both when
|
|
50
|
+
the spec misses an endpoint and when it invents one.
|
|
51
|
+
- **An MCP server** with 15 tools — 13 task-shaped, plus `qbt_call` and
|
|
52
|
+
`list_endpoints` — and safety gates that hold even through the escape hatch.
|
|
53
|
+
- **Compatibility measured, not assumed** — tested against real 4.6, 5.0, 5.1
|
|
54
|
+
and 5.2 containers, with a rename table so `torrents/stop` resolves to
|
|
55
|
+
`pause` on servers that predate the rename.
|
|
56
|
+
- **Typed throughout**, no runtime dependency beyond `httpx` and `typer`.
|
|
57
|
+
|
|
58
|
+
That coverage claim is worth a sentence of context. The wiki page most clients
|
|
59
|
+
are written against documents **78** endpoints. qBittorrent 5.2.3 ships
|
|
60
|
+
**123**, and two whole scopes — `torrentcreator` and `clientdata` — appear in
|
|
61
|
+
no wiki page at all.
|
|
62
|
+
|
|
63
|
+
## Installation
|
|
64
|
+
|
|
65
|
+
PyPI's `qbt` name belongs to an unrelated package, so the CLI is run via
|
|
66
|
+
`--from`:
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
uvx --from qbt-kit qbt doctor
|
|
70
|
+
uvx qbt-mcp --help # the MCP server ships under its own name
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Or add it to a project:
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
uv add qbt-kit # library and CLI
|
|
77
|
+
uv add "qbt-kit[mcp]" # with the MCP server
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### As a Claude Code plugin
|
|
81
|
+
|
|
82
|
+
This repository is also a plugin marketplace, so one line installs the MCP
|
|
83
|
+
server and the skill together:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
/plugin install qbt-kit --marketplace manueltarouca/qbt-kit
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The bundled MCP server runs from the plugin's own checkout, so it needs `uv`
|
|
90
|
+
but not a published package, and it always matches the version you installed.
|
|
91
|
+
Point it at your server with `QBT_URL`, and see [Configuration](#configuration)
|
|
92
|
+
for credentials.
|
|
93
|
+
|
|
94
|
+
## Quickstart
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
qbt doctor # version, auth mode, endpoint availability
|
|
98
|
+
qbt torrents info --category tv # the API calls this `info`, not `list`
|
|
99
|
+
qbt torrents add --urls <magnet> --category tv --auto-tmm
|
|
100
|
+
qbt health # swarm health across the queue
|
|
101
|
+
qbt coverage # the endpoint coverage ledger
|
|
102
|
+
qbt call torrents/pieceStates -p hash=<h> # raw, reaches all 123
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Scope commands mirror the API exactly — `torrents info`, not a friendlier
|
|
106
|
+
`list` — so anything you read in qBittorrent's own docs translates directly.
|
|
107
|
+
`--json` works everywhere, so every command composes with `jq`.
|
|
108
|
+
|
|
109
|
+
`qbt ready` is also there, but it inspects the local filesystem to decide
|
|
110
|
+
whether a download has really settled, so it only means anything when the CLI
|
|
111
|
+
runs on the same machine as qBittorrent.
|
|
112
|
+
|
|
113
|
+
## MCP server
|
|
114
|
+
|
|
115
|
+
```jsonc
|
|
116
|
+
{
|
|
117
|
+
"mcpServers": {
|
|
118
|
+
"qbittorrent": {
|
|
119
|
+
"command": "uvx",
|
|
120
|
+
"args": ["qbt-mcp", "--url", "http://127.0.0.1:8080"]
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Fifteen tools rather than 123 — a tool list that long costs context
|
|
127
|
+
on every turn and measurably worsens which tool a model picks. `qbt_call` keeps
|
|
128
|
+
every endpoint reachable and `list_endpoints` lets a model discover them, so the
|
|
129
|
+
small surface stays complete.
|
|
130
|
+
|
|
131
|
+
### Configuration
|
|
132
|
+
|
|
133
|
+
Flags first, then the environment, then the macOS Keychain:
|
|
134
|
+
|
|
135
|
+
| variable | meaning |
|
|
136
|
+
|---|---|
|
|
137
|
+
| `QBT_URL` | base URL, default `http://127.0.0.1:8080` |
|
|
138
|
+
| `QBT_API_KEY` | API key (qBittorrent 5.2+); preferred when present |
|
|
139
|
+
| `QBT_USERNAME` | WebUI username, for cookie login on any version |
|
|
140
|
+
| `QBT_PASSWORD` | WebUI password |
|
|
141
|
+
| `QBT_READ_ONLY` | `1`/`true`/`yes` to refuse everything that changes state |
|
|
142
|
+
|
|
143
|
+
Keychain fallback uses service `qbittorrent`, account `api-key` for a key and
|
|
144
|
+
`webui` for the password:
|
|
145
|
+
|
|
146
|
+
```sh
|
|
147
|
+
security add-generic-password -s qbittorrent -a api-key -w '<key>'
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**qBittorrent has no OAuth**, so there is no token flow to delegate and the
|
|
151
|
+
secret is long-lived. The reference MCP servers put credentials straight into
|
|
152
|
+
the client's `env` block — `GITHUB_PERSONAL_ACCESS_TOKEN` is the canonical
|
|
153
|
+
example — but that file is plaintext and frequently committed. Prefer the
|
|
154
|
+
Keychain, and if you do use `env`, reference a variable instead of pasting the
|
|
155
|
+
secret:
|
|
156
|
+
|
|
157
|
+
```jsonc
|
|
158
|
+
"env": {
|
|
159
|
+
"QBT_URL": "${QBT_URL:-http://127.0.0.1:8080}",
|
|
160
|
+
"QBT_API_KEY": "${QBT_API_KEY:-}" // empty falls through to the Keychain
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The `:-` default matters. Claude Code passes an unset `${VAR}` through as the
|
|
165
|
+
*literal text* `${VAR}` rather than as empty, which otherwise surfaces as a
|
|
166
|
+
confusing 401 much later — so `qbt-mcp` detects that case at startup and says
|
|
167
|
+
so plainly instead of starting.
|
|
168
|
+
|
|
169
|
+
On startup it reports, on stderr, where the credentials came from and never
|
|
170
|
+
what they are:
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
qbt-mcp: http://127.0.0.1:8080 · WebAPI 2.15.1 · auth=bearer via macOS Keychain
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Safety
|
|
177
|
+
|
|
178
|
+
An agent driving a torrent client can destroy data that took weeks to collect,
|
|
179
|
+
so the gates are code rather than guidance:
|
|
180
|
+
|
|
181
|
+
- Destructive endpoints require `confirm`; `delete_files` is never implied.
|
|
182
|
+
- `--read-only` (or `QBT_READ_ONLY=1`) blocks mutations, **including via
|
|
183
|
+
`qbt_call`**.
|
|
184
|
+
- `app/setPreferences` refuses the WebUI, auth and network keys that would
|
|
185
|
+
sever the connection or weaken security. There is no override.
|
|
186
|
+
- Refusals return a structured cause and remedy, so an agent can correct itself
|
|
187
|
+
instead of seeing an opaque tool error.
|
|
188
|
+
|
|
189
|
+
`qbt-mcp` speaks stdio by default, which keeps it a child process with nothing
|
|
190
|
+
listening. The `sse` and `streamable-http` transports expose an
|
|
191
|
+
*unauthenticated* control surface holding your qBittorrent credentials — bound
|
|
192
|
+
to loopback, but do not forward those ports. See [SECURITY.md](https://github.com/manueltarouca/qbt-kit/blob/main/SECURITY.md).
|
|
193
|
+
|
|
194
|
+
## Documentation
|
|
195
|
+
|
|
196
|
+
| | |
|
|
197
|
+
|---|---|
|
|
198
|
+
| [`docs/TRACEABILITY.md`](https://github.com/manueltarouca/qbt-kit/blob/main/docs/TRACEABILITY.md) | how the coverage claim is built and gated |
|
|
199
|
+
| [`docs/TESTING.md`](https://github.com/manueltarouca/qbt-kit/blob/main/docs/TESTING.md) | the container harness, and behaviours that cost real time to learn |
|
|
200
|
+
| [`docs/PLAN.md`](https://github.com/manueltarouca/qbt-kit/blob/main/docs/PLAN.md) | the design and the reasoning behind it |
|
|
201
|
+
| [`docs/ROADMAP.md`](https://github.com/manueltarouca/qbt-kit/blob/main/docs/ROADMAP.md) | what is done and what is not |
|
|
202
|
+
| [`docs/REVIEW-1.md`](https://github.com/manueltarouca/qbt-kit/blob/main/docs/REVIEW-1.md) | an independent review of the plan, with every finding dispositioned |
|
|
203
|
+
| [`docs/dependencies.md`](https://github.com/manueltarouca/qbt-kit/blob/main/docs/dependencies.md) | every dependency, and why it is justified |
|
|
204
|
+
| [`SECURITY.md`](https://github.com/manueltarouca/qbt-kit/blob/main/SECURITY.md) | threat model, credential handling, how to report a vulnerability |
|
|
205
|
+
|
|
206
|
+
A Claude skill ships in [`skills/qbt-kit/`](https://github.com/manueltarouca/qbt-kit/tree/main/skills/qbt-kit/) covering CLI usage
|
|
207
|
+
and the swarm-health rule.
|
|
208
|
+
|
|
209
|
+
## Field notes
|
|
210
|
+
|
|
211
|
+
Things that cost us time, documented so they cost you none.
|
|
212
|
+
|
|
213
|
+
- **Login success cannot be read from the status code.** qBittorrent ≤5.1
|
|
214
|
+
answers a *failed* login with HTTP 200 and the body `Fails.`; 5.2 returns an
|
|
215
|
+
empty 204. The only rule that holds across versions is whether a session
|
|
216
|
+
cookie was issued.
|
|
217
|
+
- **`app/webapiVersion` requires authentication.** Only `auth/login` is public,
|
|
218
|
+
so you cannot read the version to decide how to authenticate. Probe instead.
|
|
219
|
+
- **The session cookie is not always `SID`.** On 5.2 it is `QBT_SID_<port>`;
|
|
220
|
+
on 5.0/5.1 it is user-configurable. Never match on the name.
|
|
221
|
+
- **A bare 401 is often Host-header validation, not bad credentials.** It is on
|
|
222
|
+
by default and rejects any request whose `Host` port differs from the port
|
|
223
|
+
qBittorrent listens on — which is what happens behind mapped container ports,
|
|
224
|
+
SSH tunnels and most reverse proxies.
|
|
225
|
+
- **API keys exist only from qBittorrent 5.2.0.** Sending `Authorization:
|
|
226
|
+
Bearer` to 5.0/5.1 gets a 403 that looks like a credentials problem.
|
|
227
|
+
- **`pause`/`resume` were removed in 5.0**, not aliased; they 404 there.
|
|
228
|
+
- **`progress: 1.0` does not mean every file is on disk.** Progress is computed
|
|
229
|
+
over the *selected* files, so a torrent with files at priority 0 reports 100%
|
|
230
|
+
having never fetched them. `size < total_size` is the cheap flag, from the
|
|
231
|
+
same listing call.
|
|
232
|
+
- **A category alone does not place a torrent.** qBittorrent only applies a
|
|
233
|
+
category's save path under automatic torrent management, so `torrents/add`
|
|
234
|
+
with a category but without `autoTMM` files it in the global save path.
|
|
235
|
+
- **A stalled torrent is usually not slow but stuck.** `availability` is how
|
|
236
|
+
many complete copies exist across reachable peers; when it does not exceed
|
|
237
|
+
`progress`, no peer has the missing pieces. But a *queued* torrent reports
|
|
238
|
+
availability 0 simply for not being connected — judging those as dead is how
|
|
239
|
+
you delete healthy downloads.
|
|
240
|
+
|
|
241
|
+
## Contributing
|
|
242
|
+
|
|
243
|
+
See [CONTRIBUTING.md](https://github.com/manueltarouca/qbt-kit/blob/main/CONTRIBUTING.md). The short version: endpoints are added
|
|
244
|
+
to the spec under `src/qbtkit/spec/`, never by hand to the CLI or MCP, and
|
|
245
|
+
`tools/build_coverage.py --strict` has to stay green.
|
|
246
|
+
|
|
247
|
+
## Acknowledgements
|
|
248
|
+
|
|
249
|
+
- [qBittorrent](https://github.com/qbittorrent/qBittorrent) — and specifically
|
|
250
|
+
its `src/webui/api/*controller.cpp`, which is the only complete and accurate
|
|
251
|
+
description of this API that exists.
|
|
252
|
+
- [`qbittorrent-api`](https://pypi.org/project/qbittorrent-api/) is the mature,
|
|
253
|
+
well-maintained Python library for this API, and if a library is all you
|
|
254
|
+
need, use it. qbt-kit exists for the CLI, the MCP server and the verifiable
|
|
255
|
+
coverage claim, not because anything is wrong with it.
|
|
256
|
+
|
|
257
|
+
## Licence
|
|
258
|
+
|
|
259
|
+
MIT — see [LICENSE](https://github.com/manueltarouca/qbt-kit/blob/main/LICENSE).
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "qbt-kit"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Spec-driven client, 1:1 CLI and MCP server for the qBittorrent WebUI API"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
keywords = [
|
|
10
|
+
"qbittorrent",
|
|
11
|
+
"torrent",
|
|
12
|
+
"cli",
|
|
13
|
+
"mcp",
|
|
14
|
+
"webui-api",
|
|
15
|
+
]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Intended Audience :: System Administrators",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Programming Language :: Python :: 3.14",
|
|
24
|
+
"Topic :: Communications :: File Sharing",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"httpx>=0.28",
|
|
28
|
+
"typer>=0.27",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[[project.authors]]
|
|
32
|
+
name = "mt"
|
|
33
|
+
email = "manuel+git@matm.xyz"
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
mcp = [
|
|
37
|
+
"mcp>=2.3",
|
|
38
|
+
"pydantic>=2.0",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[project.urls]
|
|
42
|
+
Homepage = "https://github.com/manueltarouca/qbt-kit"
|
|
43
|
+
Issues = "https://github.com/manueltarouca/qbt-kit/issues"
|
|
44
|
+
Changelog = "https://github.com/manueltarouca/qbt-kit/blob/main/CHANGELOG.md"
|
|
45
|
+
Documentation = "https://github.com/manueltarouca/qbt-kit#readme"
|
|
46
|
+
|
|
47
|
+
[project.scripts]
|
|
48
|
+
qbt = "qbtkit.cli:app"
|
|
49
|
+
qbt-mcp = "qbtkit.mcp:main"
|
|
50
|
+
|
|
51
|
+
[build-system]
|
|
52
|
+
requires = ["uv_build>=0.8,<0.14"]
|
|
53
|
+
build-backend = "uv_build"
|
|
54
|
+
|
|
55
|
+
[tool.uv]
|
|
56
|
+
required-version = ">=0.12"
|
|
57
|
+
|
|
58
|
+
[tool.uv.build-backend]
|
|
59
|
+
module-name = "qbtkit"
|
|
60
|
+
|
|
61
|
+
[tool.ruff]
|
|
62
|
+
line-length = 96
|
|
63
|
+
target-version = "py311"
|
|
64
|
+
|
|
65
|
+
[tool.ruff.format]
|
|
66
|
+
exclude = ["*.md"]
|
|
67
|
+
|
|
68
|
+
[tool.ruff.lint]
|
|
69
|
+
select = [
|
|
70
|
+
"E",
|
|
71
|
+
"F",
|
|
72
|
+
"W",
|
|
73
|
+
"I",
|
|
74
|
+
"UP",
|
|
75
|
+
"B",
|
|
76
|
+
"SIM",
|
|
77
|
+
"RUF",
|
|
78
|
+
"S",
|
|
79
|
+
"ANN",
|
|
80
|
+
]
|
|
81
|
+
ignore = [
|
|
82
|
+
"ANN401",
|
|
83
|
+
"S105",
|
|
84
|
+
"S106",
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
[tool.ruff.lint.per-file-ignores]
|
|
88
|
+
"tests/**" = [
|
|
89
|
+
"S101",
|
|
90
|
+
"ANN",
|
|
91
|
+
]
|
|
92
|
+
"tools/**" = ["S310"]
|
|
93
|
+
"tests/contract/harness.py" = [
|
|
94
|
+
"S310",
|
|
95
|
+
"S603",
|
|
96
|
+
"S607",
|
|
97
|
+
]
|
|
98
|
+
"tests/contract/**" = ["S310"]
|
|
99
|
+
|
|
100
|
+
[tool.ty.rules]
|
|
101
|
+
possibly-unresolved-reference = "warn"
|
|
102
|
+
|
|
103
|
+
[tool.pytest.ini_options]
|
|
104
|
+
testpaths = ["tests"]
|
|
105
|
+
markers = [
|
|
106
|
+
"contract: needs a qBittorrent container",
|
|
107
|
+
"realworld: needs outbound internet; opt-in only",
|
|
108
|
+
]
|
|
109
|
+
addopts = "-m 'not contract and not realworld'"
|
|
110
|
+
|
|
111
|
+
[dependency-groups]
|
|
112
|
+
dev = [
|
|
113
|
+
"ruff>=0.17",
|
|
114
|
+
"ty>=0.0.89",
|
|
115
|
+
"pytest>=8.3",
|
|
116
|
+
"pytest-asyncio>=0.25",
|
|
117
|
+
]
|