Skip to content

Instantly share code, notes, and snippets.

@Chrysaloid
Last active March 19, 2026 16:42
Show Gist options
  • Select an option

  • Save Chrysaloid/0bebd37e93a92922c6cd99bcf701e5dd to your computer and use it in GitHub Desktop.

Select an option

Save Chrysaloid/0bebd37e93a92922c6cd99bcf701e5dd to your computer and use it in GitHub Desktop.
import os
os.environ["NO_COLOR"] = "1"
import shutil
import subprocess
import sys
import traceback
torrentParams = ""
try:
import libtorrent as lt
from send2trash import send2trash as _send2trash
""" qBittorrent settings info
%N: Torrent name
%L: Category
%G: Tags (separated by comma)
%F: Content path (same as root path for multifile torrent)
%R: Root path (first torrent subdirectory path)
%D: Save path
%C: Number of files
%Z: Torrent size (bytes)
%T: Current tracker
%I: Info hash v1 (or '-' if unavailable)
%J: Info hash v2 (or ‘-' if unavailable)
%K: Torrent ID (either sha-1 info hash for v1 torrent or truncated sha-256 info hash for v2/hybrid torrent)
"""
class SimpleError(Exception): pass
# def send2trash(path: str): os.remove(path)
def send2trash(path: str): _send2trash(os.path.normpath(path))
load_torrent_limits = { # https://www.rasterbar.com/products/libtorrent/reference-Torrent_Info.html#load_torrent_limits
"max_buffer_size": 250 * 1024 * 1024 # 250 MiB
}
def getTorrentHashes(path: str):
info = lt.torrent_info(path, load_torrent_limits)
hashes = info.info_hashes()
v1 = hashes.v1.to_string().hex() if hashes.has_v1() else None
v2 = hashes.v2.to_string().hex() if hashes.has_v2() else None
return v1, v2
def showErrorInNewConsole(errorText: str):
subprocess.Popen(
["python", "-c", f"print({errorText!r}); input('Press ENTER to exit...')"],
creationflags=subprocess.CREATE_NEW_CONSOLE,
)
def searchFolderForMatchingTorrent(folder: str, v1Hash: str, v2Hash: str) -> os.DirEntry | None:
with os.scandir(folder) as it: # FileNotFoundError will be raised if folder does not exist
for entry in it:
if entry.name.endswith(".torrent") and entry.is_file():
v1, v2 = getTorrentHashes(entry.path)
if v2 == v2Hash or v1 == v1Hash: # if v2 is not available the comparison None == "-" will return False and v1 will be compared instead
return entry # only 1 .torrent file should have matching Torrent ID so we stop scanning
return None
paramNames = [
"Torrent name",
"Category",
"Tags",
"Content path",
"Root path",
"Save path",
"Number of files",
"Torrent size",
"Current tracker",
"Info hash v1",
"Info hash v2",
"Torrent ID",
"Download folder",
"Added torrents folder",
"Completed torrents folder",
]
maxWidth = len(max(paramNames, key=len))
params = dict()
for i, value in enumerate(sys.argv[1:]):
torrentParams += f"{paramNames[i].ljust(maxWidth)} : {value}\n" # for debbuging in case of an error
params[paramNames[i]] = value
# Root path is empty if torrent contains a single file and subfolder was not created
destDir = params["Root path"] if params["Root path"] else params["Save path"]
v1Hash = params["Info hash v1"]
v2Hash = params["Info hash v2"]
destTorrent = searchFolderForMatchingTorrent(destDir , v1Hash, v2Hash) if os.path.isdir(destDir) else None
downloadTorrent = searchFolderForMatchingTorrent(params["Download folder"] , v1Hash, v2Hash)
addedTorrent = searchFolderForMatchingTorrent(params["Added torrents folder"] , v1Hash, v2Hash)
completedTorrent = searchFolderForMatchingTorrent(params["Completed torrents folder"], v1Hash, v2Hash)
torrentFile = None
if destTorrent: # the torrent is already in the destination folder so move to trash the unneeded torrents
if downloadTorrent : send2trash(downloadTorrent .path)
if addedTorrent : send2trash(addedTorrent .path)
if completedTorrent: send2trash(completedTorrent.path)
elif downloadTorrent:
torrentFile = downloadTorrent
if addedTorrent : send2trash(addedTorrent .path)
if completedTorrent: send2trash(completedTorrent.path)
elif addedTorrent:
torrentFile = addedTorrent
if completedTorrent: send2trash(completedTorrent.path)
elif completedTorrent:
torrentFile = completedTorrent
elif v1Hash == "-" and v2Hash != "-":
# if a v2-only torrent was added via magnet link and only 1 small file was selected for
# download, torrent file is not created on completion. But if, after initial completion, you
# select a larger file for download and it completes, the torrent file will be created and the
# script will be called again
pass
else: # shouldn't happen
raise SimpleError("No mathing torrents found")
if torrentFile:
os.makedirs(destDir, exist_ok=True)
shutil.move(torrentFile.path, os.path.join(destDir, torrentFile.name))
except SimpleError as e:
showErrorInNewConsole(f"{torrentParams}\nERROR: {e}\n")
except Exception:
showErrorInNewConsole(f"{torrentParams}\n{traceback.format_exc()}")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment