From c199fe1c3647591452daa57a47798cc954a54f5c Mon Sep 17 00:00:00 2001 From: Abdussamet Kocak Date: Thu, 25 Jun 2026 08:46:52 +0300 Subject: [PATCH] feat(transmission): add wait and path reporting --- transmission.py | 79 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 68 insertions(+), 11 deletions(-) diff --git a/transmission.py b/transmission.py index 576bb4c..15665f4 100755 --- a/transmission.py +++ b/transmission.py @@ -9,7 +9,9 @@ import base64 import logging from os import getenv import os +import sys import subprocess +import time import httpx from pathlib import Path @@ -17,32 +19,34 @@ from pathlib import Path def move_to_trash(file_path: Path): if not file_path.is_file(): raise FileNotFoundError - if os.name != "darwin": + + if sys.platform == "darwin": args = [ "osascript", "-e", f'tell app "Finder" to move POSIX file "{file_path}" to trash', ] - subprocess.run( + return subprocess.run( args, check=True, + capture_output=True, ) - if os.name == "win32": - # move to recycle bin - # Add-Type -AssemblyName Microsoft.VisualBasic; [Microsoft.VisualBasic.FileIO.FileSystem]::DeleteFile("C:\path\to\your\file.txt", 'OnlyErrorDialogs', 'SendToRecycleBin') + if sys.platform == "win32": args = [ "powershell", "-NoProfile", "-Command", f'Add-Type -AssemblyName Microsoft.VisualBasic; [Microsoft.VisualBasic.FileIO.FileSystem]::DeleteFile("{file_path}", "OnlyErrorDialogs", "SendToRecycleBin")', ] - subprocess.run( + return subprocess.run( args, check=True, ) + raise NotImplementedError("Unsupported platform") -def add_torrent(client: httpx.Client, torrent_path: Path): + +def add_torrent(client: httpx.Client, torrent_path: Path) -> str | None: encoded_torrent = base64.b64encode(torrent_path.read_bytes()).decode() payload = { @@ -59,7 +63,48 @@ def add_torrent(client: httpx.Client, torrent_path: Path): raise Exception(f"HTTP {res.status_code}: {res.text}") result = res.json() - return result["result"] in ["duplicate-torrent", "success"] + if result["result"] not in ["duplicate-torrent", "success"]: + return None + added = result["arguments"].get("torrent-added") or result["arguments"].get("torrent-duplicate") + return added["hashString"] + + +def print_download_paths(client: httpx.Client, hashes: list[str]): + payload = { + "method": "torrent-get", + "arguments": { + "ids": hashes, + "fields": ["hashString", "name", "downloadDir"], + }, + } + res = client.post("/transmission/rpc", json=payload) + if res.is_error: + raise Exception(f"HTTP {res.status_code}: {res.text}") + for t in res.json()["arguments"]["torrents"]: + print(str(Path(t["downloadDir"]) / t["name"])) + + +def poll_until_done(client: httpx.Client, hashes: list[str], interval: int): + pending = set(hashes) + while pending: + payload = { + "method": "torrent-get", + "arguments": { + "ids": list(pending), + "fields": ["hashString", "name", "percentDone", "isFinished", "status"], + }, + } + res = client.post("/transmission/rpc", json=payload) + if res.is_error: + raise Exception(f"HTTP {res.status_code}: {res.text}") + torrents = res.json()["arguments"]["torrents"] + for t in torrents: + logging.info(f"{t['name']}: {t['percentDone'] * 100:.1f}%") + if t["isFinished"] or t["status"] == 6: + logging.info(f"Done: {t['name']}") + pending.discard(t["hashString"]) + if pending: + time.sleep(interval) def parse_args(): @@ -68,6 +113,9 @@ def parse_args(): parser.add_argument("--host", required=True, default=getenv("TRANSMISSION_HOST", "http://localhost:9091"), help="Transmission RPC host URL") parser.add_argument("--auth", default=getenv("TRANSMISSION_AUTH"), help="Transmission RPC username:password") parser.add_argument("--clean", action="store_true", help="Delete torrent file after successful upload") + parser.add_argument("--wait", action="store_true", help="Block until all added torrents finish downloading") + parser.add_argument("--interval", type=int, default=5, metavar="SECS", help="Polling interval in seconds when using --wait (default: 5)") + parser.add_argument("--print-paths", action="store_true", help="Print download paths after adding (one per line)") return parser.parse_args() @@ -79,19 +127,28 @@ def main(): client = httpx.Client( base_url=args.host, auth=httpx.BasicAuth(*args.auth.split(":")) if args.auth else None, - timeout=10, + timeout=20, ) csrf_token = client.post("/transmission/rpc", json={"method": "session-get"}).headers["x-transmission-session-id"] client.headers["X-Transmission-Session-Id"] = csrf_token + hashes = [] for torrent_path in args.torrent_paths: if not torrent_path.is_file(): - logging.error(f"Torrent file not found: {args.torrent_path}") + logging.error(f"Torrent file not found: {torrent_path}") raise SystemError(1) - add_torrent(client=client, torrent_path=torrent_path) + hash_string = add_torrent(client=client, torrent_path=torrent_path) + if hash_string: + hashes.append(hash_string) if args.clean: move_to_trash(torrent_path) + if args.wait and hashes: + poll_until_done(client=client, hashes=hashes, interval=args.interval) + + if args.print_paths and hashes: + print_download_paths(client=client, hashes=hashes) + if __name__ == "__main__": main()