qbittorrent-orphaned is a lightweight utility that identifies orphaned files -- files that exist on disk but are not tracked by any torrent in your qBittorrent instance. It connects to the qBittorrent Web API v2, walks the directories you configure, cross-references every file against every torrent, and reports what does not belong.
What Are Orphaned Files?
When you remove a torrent from qBittorrent but keep the data on disk, or when external tools (transcoders, renaming scripts, etc.) create files that were never part of a torrent, those files become orphans. They consume storage without being seeded or managed. This tool finds them so you can decide what to keep and what to reclaim.
Features
- Single-file, pure Python -- no build step, no complex dependencies, just
requests. - Web API v2 -- authenticates and queries qBittorrent over HTTP; works locally or across a network.
- Case-insensitive matching -- handles mixed-case filenames on Windows and Linux alike.
- Category-aware grouping -- results are organized by qBittorrent category, with uncategorized torrents collected under
__UNCATEGORIZED__. - Human-readable sizes -- every orphan is printed alongside its size in KiB, MiB, GiB, etc.
- Configurable metadata ignore list -- common metadata files (
.nfo,.jpg,.png,.srt,.sub,.idx,.txt,.bin,.svg) are skipped by default. You can extend this list. - Exclude patterns -- filter out known non-torrent files (e.g., transcoded 720p copies) by substring match.
- macOS-safe -- automatically skips
._resource fork files.
Quick Start
Install from PyPI
pip install qbittorrent-orphaned
Standalone
QBIT_HOST=http://localhost:8080 \
QBIT_USER=admin \
QBIT_PASS=yourpassword \
CATEGORY_FOLDERS="Films=/mnt/media/films;Shows=/mnt/media/shows" \
qbittorrent-orphaned
You can also run the script directly without installing:
pip install requests
QBIT_HOST=http://localhost:8080 \
QBIT_USER=admin \
QBIT_PASS=yourpassword \
CATEGORY_FOLDERS="Films=/mnt/media/films;Shows=/mnt/media/shows" \
python orphan_detector.py
Docker
There is no pre-built image yet, but you can run it easily with a one-liner:
docker run --rm \
-e QBIT_HOST=http://qbittorrent:8080 \
-e QBIT_USER=admin \
-e QBIT_PASS=yourpassword \
-e CATEGORY_FOLDERS="Films=/media/films;Shows=/media/shows" \
-v /mnt/media:/media:ro \
--network=host \
python:3-alpine sh -c "pip install --quiet requests && python /app/orphan_detector.py"
Mount the script into the container if you prefer a cleaner approach:
docker run --rm \
-v "$(pwd)/orphan_detector.py:/app/orphan_detector.py:ro" \
-v /mnt/media:/media:ro \
-e QBIT_HOST=http://qbittorrent:8080 \
-e QBIT_USER=admin \
-e QBIT_PASS=yourpassword \
-e CATEGORY_FOLDERS="Films=/media/films;Shows=/media/shows" \
python:3-alpine sh -c "pip install --quiet requests && python /app/orphan_detector.py"
Tip: If qBittorrent runs in its own container, make sure both containers share a Docker network (or use
--network=host) so the hostname resolves.
Configuration
All configuration is done through environment variables.
| Variable | Default | Description |
|---|---|---|
QBIT_HOST |
http://qbittorrent:8080 |
qBittorrent Web UI URL |
QBIT_USER |
admin |
Username for Web UI authentication |
QBIT_PASS |
password |
Password for Web UI authentication |
CATEGORY_FOLDERS |
Films=W:\Films;Shows=X:\Series |
Semicolon-separated Category=Path pairs. Categories must match those configured in qBittorrent. |
EXCLUDE_PATTERNS |
(empty) | Comma-separated substrings. Any file whose relative path contains one of these patterns (case-insensitive) is skipped. A comma inside a pattern is escaped as \, — see below. |
IGNORE_SUFFIXES |
(empty) | Comma-separated file extensions to ignore in addition to the built-in list. Leading dots are optional (e.g., ass,ssa or .ass,.ssa). |
Category Folders Format
CATEGORY_NAME=ABSOLUTE_PATH;CATEGORY_NAME2=ABSOLUTE_PATH2
Each category name must match exactly what is configured in qBittorrent. Torrents with no category are grouped under the key __UNCATEGORIZED__.
Category names and paths may contain spaces and commas — write them literally, and quote only the variable as a whole:
CATEGORY_FOLDERS="Books, Comics & Manga=F:\Downloads\Books, Comics & Manga;TV & Movies=F:\Downloads\TV & Movies"
One category folder may sit inside another — mapping __UNCATEGORIZED__ to qBittorrent's default save path, which is the parent of the per-category folders, is a common setup. Files under a nested folder are scanned as part of the category that owns that folder, and the overlap is noted on stderr, so it stays out of a redirected report.
Patterns Containing Commas
EXCLUDE_PATTERNS is comma-separated, so a pattern that itself contains a comma is escaped with a backslash:
EXCLUDE_PATTERNS="Books\, Comics & Manga,sample"
Quote the whole value. Unquoted, the shell splits on the spaces and runs Comics and Manga,sample as commands, and Python receives just Books, — which is the very mistake this escape exists to prevent.
Note the difference from CATEGORY_FOLDERS above: that variable is semicolon-separated, so commas inside a category name or path need no escape. EXCLUDE_PATTERNS is comma-separated, so they do.
That is two patterns — Books, Comics & Manga and sample. Without the escape it would be three, and the stray Books would silently skip every path containing "books" — Audiobooks/Dune.m4b, for instance — while the stray Comics & Manga would skip Comics & Manga Weekly.mkv. Values with no \, in them are unaffected.
Example Output
===== Films =====
/mnt/media/films/Some.Movie.2023/Some.Movie.2023.mkv (4,215 MiB)
/mnt/media/films/Old.Film.1999/Old.Film.1999.avi (702 MiB)
===== Shows =====
/mnt/media/shows/Series.Name.S01/Episode.05.mkv (1,102 MiB)
When no orphans are found the output is simply:
No orphaned files found.
How It Works
- Authenticate -- the script logs in to qBittorrent via
/api/v2/auth/loginand obtains a session cookie. - Fetch torrents -- it retrieves the full torrent list from
/api/v2/torrents/info, then for each torrent calls/api/v2/torrents/filesto get every file path the torrent manages. - Index by category -- all torrent file paths are normalized (forward slashes, lowercase) and grouped into a lookup set per category.
- Walk the filesystem -- for each configured category folder, the script recursively enumerates files, skipping ignored suffixes, macOS resource forks, and exclude-pattern matches.
- Cross-reference -- every disk file is checked against the corresponding category set. Files not present in any torrent are reported as orphans with their absolute path and human-readable size.
License
Metadata
Release files for qbittorrent-orphaned 1.1.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| qbittorrent_orphaned-1.1.2.tar.gz | 27.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| qbittorrent_orphaned-1.1.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 49.2 kB
Release files / qbittorrent_orphaned-1.1.2.tar.gz
| Download URL | qbittorrent_orphaned-1.1.2.tar.gz |
|---|---|
| Size | 27.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f387c3b92c603797f6012567402b568c5f2abcc2cf2bde01792200fc6fc123f8
|
|
BLAKE2b-256 checksum How to use checksums |
91a7f536d3d390343e493f9dd0e8224ab23874680f681b062d16ea4066e9fd7c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / qbittorrent_orphaned-1.1.2-py3-none-any.whl
| Download URL | qbittorrent_orphaned-1.1.2-py3-none-any.whl |
|---|---|
| Size | 21.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1eb58654650c6bc83230cb9b43fb52dca3c17f383669aa1adc3ad12c4791b305
|
|
BLAKE2b-256 checksum How to use checksums |
8e25226877771ee323109fb510a5951929491e58109e66ec902b65fa699a6d7e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|