# Developing Custom Search Plugins for qBittorrent: A Complete Technical Guide

> Learn to develop custom search plugins for qBittorrent with this technical guide. Create powerful Python-based search engines for your favorite torrent sites.

- Repository: [qBittorrent project/qBittorrent](https://github.com/qbittorrent/qBittorrent)
- Tags: how-to-guide
- Published: 2026-05-05

---

**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`](https://github.com/qbittorrent/qBittorrent/blob/main/src/base/search/searchpluginmanager.h) and [`src/base/search/searchpluginmanager.cpp`](https://github.com/qbittorrent/qBittorrent/blob/main/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`](https://github.com/qbittorrent/qBittorrent/blob/main/src/base/search/searchhandler.h) and [`src/base/search/searchhandler.cpp`](https://github.com/qbittorrent/qBittorrent/blob/main/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:

```bash
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`](https://github.com/qbittorrent/qBittorrent/blob/main/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:

```python

# 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:

```bash
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:

```python

# 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:

```cpp
#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:

```cpp
#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.h`](https://github.com/qbittorrent/qBittorrent/blob/main/src/base/search/searchpluginmanager.h)** and **[`searchpluginmanager.cpp`](https://github.com/qbittorrent/qBittorrent/blob/main/searchpluginmanager.cpp)**: Contains `getPluginVersion()`, `installPlugin()`, and the plugin discovery logic that scans the engines directory
- **[`src/base/search/searchhandler.h`](https://github.com/qbittorrent/qBittorrent/blob/main/src/base/search/searchhandler.h)** and **[`searchhandler.cpp`](https://github.com/qbittorrent/qBittorrent/blob/main/searchhandler.cpp)**: Implements the QProcess management and stdout parsing that converts tab-delimited strings to `SearchResult` objects
- **[`src/searchengine/nova3/nova2.py`](https://github.com/qbittorrent/qBittorrent/blob/main/src/searchengine/nova3/nova2.py)**: The Python bootstrap script that imports your plugin and calls its `search()` function
- **[`src/webui/www/private/views/searchplugins.html`](https://github.com/qbittorrent/qBittorrent/blob/main/src/webui/www/private/views/searchplugins.html)**: The Web UI template for the plugin management interface
- **[`src/webui/www/private/scripts/search.js`](https://github.com/qbittorrent/qBittorrent/blob/main/src/webui/www/private/scripts/search.js)**: JavaScript handling the REST API calls to `/api/v2/search/installPlugin` and `/api/v2/search/updatePlugins`

## Summary

- qBittorrent implements a **hybrid C++/Python architecture** where `SearchPluginManager` handles plugin metadata and [`nova2.py`](https://github.com/qbittorrent/qBittorrent/blob/main/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`](https://github.com/qbittorrent/qBittorrent/blob/main/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`](https://github.com/qbittorrent/qBittorrent/blob/main/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.