qBittorrent: Add torrents with all files set to “Do not download” and optionally delete payload files

Help with operating systems, apps, and software-related issues.
User avatar
ccb056
Site Administrator
Posts: 1009
Joined: January 14th, 2004, 11:36 pm
Location: Texas

qBittorrent: Add torrents with all files set to “Do not download” and optionally delete payload files

Post by ccb056 »

This guide shows how to use qBittorrent’s Web API to:
  • Add a .torrent file to qBittorrent
  • Set all files in the torrent to priority 0, meaning “Do not download”
  • Start and reannounce the torrent
  • Optionally delete already-downloaded payload files from disk
  • Batch process many .torrent files
Important: Only use this on torrents and trackers where you are authorized to do so.

If every file is set to “Do not download,” qBittorrent may still show the torrent as 100 percent complete. That is expected because the torrent has 0 wanted bytes.

Note: In the examples below, TORRENT_HASH means the torrent info-hash filename without the .torrent extension.

For example, if your torrent file is named:

Code: Select all

0123456789abcdef0123456789abcdef01234567.torrent
then the hash argument is:

Code: Select all

0123456789abcdef0123456789abcdef01234567
Requirements

This guide assumes:
  • qBittorrent WebUI is enabled
  • You know your qBittorrent WebUI username/password
  • You have shell access
  • Python 3.12 is available at /usr/local/bin/python3.12
  • The Python requests module is installed
On FreeBSD, install requests with:

Code: Select all

pkg install -y py312-requests
Check Python:

Code: Select all

/usr/local/bin/python3.12 --version
Script 1: Add torrent and set all files to “Do not download”

Create the script:

Code: Select all

ee /root/add_zero_priority_torrent.py
Paste this:

Code: Select all

#!/usr/local/bin/python3.12

import sys
import time
import uuid
import requests
from pathlib import Path

QBIT_URL = "http://127.0.0.1:8080"
QBIT_USER = "YOUR_QBIT_USERNAME"
QBIT_PASS = "YOUR_QBIT_PASSWORD"

CATEGORY = "zero-priority"
START_AFTER = True

WAIT_TIMEOUT = 60
WAIT_INTERVAL = 2

session = requests.Session()


def api(path):
    return f"{QBIT_URL.rstrip('/')}/api/v2{path}"


def login():
    r = session.post(
        api("/auth/login"),
        data={
            "username": QBIT_USER,
            "password": QBIT_PASS,
        },
        timeout=15,
    )
    r.raise_for_status()

    cookies = session.cookies.get_dict()

    if ("Ok." not in r.text) and (not any("SID" in key for key in cookies.keys())):
        raise RuntimeError(f"qBittorrent login failed: {r.text}; cookies={cookies}")

    print("Logged in to qBittorrent")


def add_torrent_paused(torrent_path, tag):
    with open(torrent_path, "rb") as f:
        files = {
            "torrents": (
                torrent_path.name,
                f,
                "application/x-bittorrent",
            )
        }

        data = {
            "paused": "true",
            "skip_checking": "true",
            "category": CATEGORY,
            "tags": tag,
        }

        r = session.post(
            api("/torrents/add"),
            data=data,
            files=files,
            timeout=30,
        )

    r.raise_for_status()
    print(f"Added torrent paused: {torrent_path.name}")


def get_torrents_by_tag(tag):
    r = session.get(
        api("/torrents/info"),
        params={"tag": tag},
        timeout=15,
    )
    r.raise_for_status()
    return r.json()


def get_files(torrent_hash):
    r = session.get(
        api("/torrents/files"),
        params={"hash": torrent_hash},
        timeout=15,
    )

    if r.status_code == 404:
        return []

    r.raise_for_status()
    return r.json()


def wait_for_added_torrent(tag):
    deadline = time.time() + WAIT_TIMEOUT

    while time.time() < deadline:
        torrents = get_torrents_by_tag(tag)

        if torrents:
            torrents.sort(key=lambda t: t.get("added_on", 0), reverse=True)
            torrent = torrents[0]
            torrent_hash = torrent["hash"]
            files = get_files(torrent_hash)

            if files:
                return torrent_hash, torrent, files

        time.sleep(WAIT_INTERVAL)

    raise TimeoutError("Timed out waiting for qBittorrent to expose the torrent file list")


def set_all_files_do_not_download(torrent_hash, files):
    ids = []

    for position, file_info in enumerate(files):
        ids.append(str(file_info.get("index", position)))

    if not ids:
        raise RuntimeError("Torrent has no files to set priority on")

    r = session.post(
        api("/torrents/filePrio"),
        data={
            "hash": torrent_hash,
            "id": "|".join(ids),
            "priority": "0",
        },
        timeout=30,
    )
    r.raise_for_status()

    print(f"Set {len(ids)} file(s) to priority 0: Do not download")


def start_torrent(torrent_hash):
    r = session.post(
        api("/torrents/start"),
        data={"hashes": torrent_hash},
        timeout=15,
    )
    r.raise_for_status()
    print("Started torrent")


def reannounce_torrent(torrent_hash):
    r = session.post(
        api("/torrents/reannounce"),
        data={"hashes": torrent_hash},
        timeout=15,
    )
    r.raise_for_status()
    print("Reannounced torrent")


def main():
    if len(sys.argv) != 2:
        print("Usage:")
        print("  /usr/local/bin/python3.12 /root/add_zero_priority_torrent.py /path/to/file.torrent")
        sys.exit(1)

    torrent_path = Path(sys.argv[1]).expanduser().resolve()

    if not torrent_path.exists():
        raise FileNotFoundError(f"Torrent file not found: {torrent_path}")

    if torrent_path.suffix.lower() != ".torrent":
        raise ValueError(f"File does not look like a .torrent file: {torrent_path}")

    tag = f"zero-priority-{uuid.uuid4().hex[:12]}"

    login()

    try:
        add_torrent_paused(torrent_path, tag)
        torrent_hash, torrent, files = wait_for_added_torrent(tag)
    except requests.exceptions.HTTPError as e:
        if e.response is not None and e.response.status_code == 409:
            print("Torrent already exists in qBittorrent. Using hash from filename.")
            torrent_hash = torrent_path.stem.lower()
            files = get_files(torrent_hash)
            torrent = {"name": torrent_hash}

            if not files:
                raise RuntimeError(f"Torrent exists but no files found for hash: {torrent_hash}")
        else:
            raise

    print(f"Matched torrent: {torrent.get('name', torrent_hash)}")
    print(f"Hash: {torrent_hash}")

    set_all_files_do_not_download(torrent_hash, files)

    if START_AFTER:
        start_torrent(torrent_hash)
        reannounce_torrent(torrent_hash)

    print("Done")


if __name__ == "__main__":
    main()
Make it executable:

Code: Select all

chmod +x /root/add_zero_priority_torrent.py
Edit these values before using:

Code: Select all

QBIT_URL = "http://127.0.0.1:8080"
QBIT_USER = "YOUR_QBIT_USERNAME"
QBIT_PASS = "YOUR_QBIT_PASSWORD"
Run Script 1 on one torrent

Code: Select all

/usr/local/bin/python3.12 /root/add_zero_priority_torrent.py /root/TORRENT_HASH.torrent
Expected output:

Code: Select all

Logged in to qBittorrent
Torrent already exists in qBittorrent. Using hash from filename.
Matched torrent: TORRENT_HASH
Hash: TORRENT_HASH
Set 69 file(s) to priority 0: Do not download
Started torrent
Reannounced torrent
Done
Batch set all .torrent files in /root to Do not download

If all .torrent files are in /root, run:

Code: Select all

for f in /root/*.torrent; do
  echo "=== Processing $f ==="
  /usr/local/bin/python3.12 /root/add_zero_priority_torrent.py "$f"
done
Script 2: Delete payload files after setting Do not download

The first script changes qBittorrent file priority, but it does not delete payload files that already exist on disk.

If you also want to delete those files, create this second script:

Code: Select all

ee /root/delete_zero_priority_files.py
Paste this:

Code: Select all

#!/usr/local/bin/python3.12

import sys
import importlib.util
from pathlib import Path

QBIT_HELPER = "/root/add_zero_priority_torrent.py"


def load_qbit_helper():
    spec = importlib.util.spec_from_file_location("z", QBIT_HELPER)
    z = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(z)
    return z


def main():
    if len(sys.argv) < 2:
        print("Usage:")
        print("  /usr/local/bin/python3.12 /root/delete_zero_priority_files.py HASH [--delete]")
        print("")
        print("Default mode is dry-run. Add --delete to actually delete files.")
        sys.exit(1)

    torrent_hash = sys.argv[1].lower()
    do_delete = "--delete" in sys.argv

    z = load_qbit_helper()
    z.login()

    info_resp = z.session.get(
        z.api("/torrents/info"),
        params={"hashes": torrent_hash},
        timeout=15,
    )
    info_resp.raise_for_status()

    torrents = info_resp.json()

    if not torrents:
        raise RuntimeError(f"Torrent not found in qBittorrent: {torrent_hash}")

    torrent = torrents[0]
    base = Path(torrent["save_path"]).resolve()
    files = z.get_files(torrent_hash)

    print("Torrent:", torrent.get("name"))
    print("Hash:", torrent_hash)
    print("Base:", base)
    print("Files in torrent:", len(files))
    print("Mode:", "DELETE" if do_delete else "DRY RUN")
    print("")

    z.set_all_files_do_not_download(torrent_hash, files)
    print("Set all files to Do not download")
    print("")

    deleted = 0
    missing = 0
    skipped = 0
    bytes_deleted = 0
    dirs = set()

    for file_info in files:
        rel_name = file_info["name"]
        target = (base / rel_name).resolve()

        try:
            target.relative_to(base)
        except ValueError:
            print("SKIP outside base:", target)
            skipped += 1
            continue

        if target.exists() and target.is_file():
            size = target.stat().st_size

            if do_delete:
                target.unlink()
                print("DELETED:", target)
            else:
                print("WOULD DELETE:", target)

            deleted += 1
            bytes_deleted += size
            dirs.add(target.parent)
        else:
            missing += 1

    if do_delete:
        for directory in sorted(dirs, key=lambda p: len(str(p)), reverse=True):
            current = directory

            while current != base and str(current).startswith(str(base)):
                try:
                    current.rmdir()
                    print("REMOVED EMPTY DIR:", current)
                    current = current.parent
                except OSError:
                    break

        z.session.post(
            z.api("/torrents/start"),
            data={"hashes": torrent_hash},
            timeout=15,
        )

        z.session.post(