Developing Custom Search Plugins for qBittorrent: A Complete Technical Guide
qBittorrent uses a Python-based search engine framework where custom plugins are simple Python modules placed in the engines directory that implement VERSION, NAME, CATEGORIES, and a search() function yielding tab-delimited results.
The qbittorrent/qBittorrent repository ships with a flexible search plugin architecture that separates the C++ management layer from the Python execution environment. Developing custom search plugins requires understanding how the SearchPluginManager discovers Python files, how SearchHandler spawns isolated Python processes, and the exact output format expected from your search() function. This guide walks through the complete implementation using actual source file paths and function signatures from the qBittorrent codebase.
Architecture of the qBittorrent Search Plugin System
The search plugin system bridges Qt/C++ components with Python execution through a well-defined IPC protocol. Understanding these three core components is essential for developing functional custom search plugins.
SearchPluginManager: The C++ Registry
The SearchPluginManager class serves as the central registry for all search plugin operations. Located in src/base/search/searchpluginmanager.h and src/base/search/searchpluginmanager.cpp, this singleton handles discovery, installation, updates, and lifecycle management.
When qBittorrent starts, SearchPluginManager scans the engines directory (returned by engineLocation()/engines/) for .py files. For each file discovered, it calls the static method SearchPluginManager::getPluginVersion() to import the Python module and extract the VERSION constant, building a PluginInfo record stored in m_plugins (a QHash<QString, PluginInfo*>).
The manager exposes installPlugin() for downloading new plugins from URLs or local file paths, enablePlugin(name, bool) for toggling plugin state, and updatePlugin() for fetching newer versions from remote update servers. All plugin metadata persists through the Preferences system via Preferences::setSearchEngDisabled().
SearchHandler and Python Process Execution
When users initiate a search, SearchPluginManager::startSearch(pattern, category, usedPlugins) instantiates a SearchHandler object (defined in src/base/search/searchhandler.h and src/base/search/searchhandler.cpp). This class wraps a single search session and manages the external process communication.
SearchHandler launches the Python interpreter using Utils::ForeignApps::pythonInfo().executablePath with specific isolation flags:
python3 -I --utf8 <path>/nova2.py <comma-separated-plugin-names> <category> <pattern-tokens...>
The -I flag runs Python in isolated mode to prevent environment contamination. As the Python engine streams results via stdout, SearchHandler::readSearchOutput() parses each line, constructs SearchResult objects, and emits the newSearchResults signal to the UI layer.
The Python Engine (nova2.py)
The actual plugin execution occurs in src/searchengine/nova3/nova2.py. This script interprets command-line arguments, dynamically imports each specified plugin from the engines directory, and invokes their search(query, category) functions.
The engine expects plugins to yield or print results in a strict tab-delimited format:
<download_url>\t<title>\t<size>\t<seeds>\t<leeches>\t<engine_url>\t<desc_url>\t<pub_date>
Each field must be separated by literal tab characters (\t), with one result per line. The C++ layer splits these lines in SearchHandler::parseSearchResult() to populate the search results model.
How to Create a Custom Search Plugin for qBittorrent
Creating a functional search plugin requires implementing a minimal Python contract. The file must reside in the engines directory and expose specific module-level constants and a generator function.
Required Plugin Interface
Every qBittorrent search plugin must implement three mandatory constants and one function:
# mycustom.py - placed in <qBittorrent>/plugins/engines/
VERSION = "1.0" # String used for update version comparison
NAME = "MyCustom" # Display name shown in the UI
CATEGORIES = ["all", "movies", "music"] # Supported search categories
def search(query: str, cat: str):
"""
Generator function that yields search results.
Args:
query: The raw search string entered by the user
cat: The selected category filter
Yields:
str: Tab-delimited result lines in the format:
download_url\t title\t size\t seeds\t leeches\t engine_url\t desc_url\t pub_date
"""
# Implementation example returning static data
yield "https://example.com/file.torrent\tExample File\t1048576\t100\t10\thttps://example.com\t\t"
The search() function receives the raw query string and category code. It must yield one string per result, with fields separated by tab characters. Empty fields are allowed but must still include the tab separators to maintain column alignment.
Plugin Installation Methods
There are two primary methods for installing your custom search plugin:
Method 1: UI-Based Installation
- Open qBittorrent and navigate to Search → Search Plugins
- Click Install a new plugin
- Provide either a remote URL (e.g.,
https://raw.githubusercontent.com/user/repo/mycustom.py) or a local file URL (e.g.,file:///home/user/mycustom.py) - The
SearchPluginManager::installPlugin()method downloads the file, validates it viagetPluginVersion(), and copies it to the engines directory
Method 2: Manual Installation
Copy your .py file directly to the engines directory:
- Windows:
%LOCALAPPDATA%\qBittorrent\nova3\engines\ - Linux/macOS:
~/.local/share/qBittorrent/nova3/engines/or~/.config/qBittorrent/nova3/engines/
Restart qBittorrent or trigger a manual update to load the plugin.
Testing Plugins from Command Line
Before installing through the UI, test your plugin using the Python engine directly:
python3 -I /path/to/qBittorrent/src/searchengine/nova3/nova2.py mycustom all "test query"
This executes the search function and prints tab-delimited output to stdout, allowing you to verify parsing logic and data formatting without launching the full application.
Complete Code Examples
Minimal Python Search Plugin
This example demonstrates a plugin that queries a fictional JSON API:
# simpletracker.py
VERSION = "2.1"
NAME = "SimpleTracker"
CATEGORIES = ["all", "software", "movies"]
import json
import urllib.request
import urllib.parse
API_ENDPOINT = "https://api.simpletracker.org/search"
def search(query, cat):
if cat == "all":
cat = ""
params = urllib.parse.urlencode({"q": query, "category": cat})
url = f"{API_ENDPOINT}?{params}"
req = urllib.request.Request(
url,
headers={"User-Agent": "qBittorrent/4.0"}
)
with urllib.request.urlopen(req, timeout=10) as response:
data = json.loads(response.read().decode("utf-8"))
for item in data.get("results", []):
download_url = item.get("magnet", item.get("torrent_url", ""))
title = item.get("title", "Unknown")
size = str(item.get("size", 0))
seeds = str(item.get("seeders", 0))
leeches = str(item.get("leechers", 0))
site_url = item.get("site_url", "")
desc = item.get("description_url", "")
pub_date = item.get("added", "")
# Critical: Use tabs (\t) as delimiters
result_line = f"{download_url}\t{title}\t{size}\t{seeds}\t{leeches}\t{site_url}\t{desc}\t{pub_date}"
print(result_line)
Programmatic Plugin Installation (C++)
To trigger plugin installation from within the qBittorrent codebase:
#include "base/search/searchpluginmanager.h"
// Download and install from remote URL
QString pluginUrl = u"https://raw.githubusercontent.com/username/qbittorrent-plugins/main/simpletracker.py"_qs;
SearchPluginManager::instance()->installPlugin(pluginUrl);
// The installPlugin() method internally calls:
// 1. Net::DownloadManager to fetch the file
// 2. installPlugin_impl() to copy to engines directory
// 3. update() to refresh the plugin list
Starting Searches Programmatically
To initiate searches from custom UI components or automation scripts:
#include "base/search/searchpluginmanager.h"
#include "base/search/searchhandler.h"
QString pattern = u"ubuntu 24.04 iso"_qs;
QString category = u"software"_qs;
QStringList selectedPlugins = {u"SimpleTracker"_qs, u"AnotherEngine"_qs};
SearchHandler *handler = SearchPluginManager::instance()->startSearch(
pattern, category, selectedPlugins);
// Connect to results signal
QObject::connect(handler, &SearchHandler::newSearchResults,
[](const QList<SearchResult> &results) {
for (const SearchResult &result : results) {
qDebug() << "Found:" << result.name
<< "Size:" << result.size
<< "Seeds:" << result.seeds;
}
});
// Monitor completion
QObject::connect(handler, &SearchHandler::searchFinished,
[](const int resultsCount) {
qDebug() << "Search completed with" << resultsCount << "results";
});
Key Source Files and Implementation Details
Understanding these specific source files accelerates plugin development:
src/base/search/searchpluginmanager.handsearchpluginmanager.cpp: ContainsgetPluginVersion(),installPlugin(), and the plugin discovery logic that scans the engines directorysrc/base/search/searchhandler.handsearchhandler.cpp: Implements the QProcess management and stdout parsing that converts tab-delimited strings toSearchResultobjectssrc/searchengine/nova3/nova2.py: The Python bootstrap script that imports your plugin and calls itssearch()functionsrc/webui/www/private/views/searchplugins.html: The Web UI template for the plugin management interfacesrc/webui/www/private/scripts/search.js: JavaScript handling the REST API calls to/api/v2/search/installPluginand/api/v2/search/updatePlugins
Summary
- qBittorrent implements a hybrid C++/Python architecture where
SearchPluginManagerhandles plugin metadata andnova2.pyexecutes the actual search logic - Custom plugins require only a single Python file implementing VERSION, NAME, CATEGORIES, and a search() generator function
- Results must be formatted as tab-delimited strings in the exact order: download URL, title, size, seeds, leeches, engine URL, description URL, publish date
- Plugins install by copying
.pyfiles to the engines directory or using the UI's install function which triggersSearchPluginManager::installPlugin() - Use
python3 -I nova2.py <plugin> <category> <query>for command-line debugging before UI installation
Frequently Asked Questions
What programming language are qBittorrent search plugins written in?
qBittorrent search plugins are written in Python 3. The C++ layer spawns a Python interpreter process (found via Utils::ForeignApps::pythonInfo()) in isolated mode (-I flag) to execute the nova2.py engine, which dynamically imports your plugin module and calls its search() function.
Where do I install custom search plugins in qBittorrent?
Install plugins in the engines subdirectory of your qBittorrent plugin location. Find the exact path by checking SearchPluginManager::engineLocation() in the source, which typically resolves to %LOCALAPPDATA%\qBittorrent\nova3\engines\ on Windows or ~/.local/share/qBittorrent/nova3/engines/ on Linux. Alternatively, use the Web UI's "Install new plugin" feature which calls installPlugin() to handle the file placement automatically.
How does qBittorrent parse search results from plugins?
The SearchHandler class reads the Python process stdout line-by-line in readSearchOutput(), then calls parseSearchResult() to split each line on tab characters (\t). The expected column order is: download URL, title, size, seeds, leeches, engine URL, description URL, and publish date. If your plugin outputs malformed lines or wrong field counts, the search will fail silently or display corrupted data in the results table.
Can I update search plugins automatically in qBittorrent?
Yes. The SearchPluginManager::checkForUpdates() method fetches a versions.txt file from the configured update server and compares version strings with locally installed plugins. When updates are available, updatePlugin(name) downloads the new .py file and replaces the old version using installPlugin_impl(). Plugin developers should increment the VERSION string constant to enable this detection mechanism.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →