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

  1. Open qBittorrent and navigate to Search → Search Plugins
  2. Click Install a new plugin
  3. 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)
  4. The SearchPluginManager::installPlugin() method downloads the file, validates it via getPluginVersion(), 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:

Summary

  • qBittorrent implements a hybrid C++/Python architecture where SearchPluginManager handles plugin metadata and nova2.py executes 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 .py files to the engines directory or using the UI's install function which triggers SearchPluginManager::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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →