- 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
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
Code: Select all
0123456789abcdef0123456789abcdef01234567
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
Code: Select all
pkg install -y py312-requests
Code: Select all
/usr/local/bin/python3.12 --version
Create the script:
Code: Select all
ee /root/add_zero_priority_torrent.py
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()
Code: Select all
chmod +x /root/add_zero_priority_torrent.py
Code: Select all
QBIT_URL = "http://127.0.0.1:8080"
QBIT_USER = "YOUR_QBIT_USERNAME"
QBIT_PASS = "YOUR_QBIT_PASSWORD"
Code: Select all
/usr/local/bin/python3.12 /root/add_zero_priority_torrent.py /root/TORRENT_HASH.torrent
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
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
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
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(