feat(transmission): add wait and path reporting

This commit is contained in:
2026-06-25 08:46:52 +03:00
parent c584265235
commit c199fe1c36
+68 -11
View File
@@ -9,7 +9,9 @@ import base64
import logging import logging
from os import getenv from os import getenv
import os import os
import sys
import subprocess import subprocess
import time
import httpx import httpx
from pathlib import Path from pathlib import Path
@@ -17,32 +19,34 @@ from pathlib import Path
def move_to_trash(file_path: Path): def move_to_trash(file_path: Path):
if not file_path.is_file(): if not file_path.is_file():
raise FileNotFoundError raise FileNotFoundError
if os.name != "darwin":
if sys.platform == "darwin":
args = [ args = [
"osascript", "osascript",
"-e", "-e",
f'tell app "Finder" to move POSIX file "{file_path}" to trash', f'tell app "Finder" to move POSIX file "{file_path}" to trash',
] ]
subprocess.run( return subprocess.run(
args, args,
check=True, check=True,
capture_output=True,
) )
if os.name == "win32": if sys.platform == "win32":
# move to recycle bin
# Add-Type -AssemblyName Microsoft.VisualBasic; [Microsoft.VisualBasic.FileIO.FileSystem]::DeleteFile("C:\path\to\your\file.txt", 'OnlyErrorDialogs', 'SendToRecycleBin')
args = [ args = [
"powershell", "powershell",
"-NoProfile", "-NoProfile",
"-Command", "-Command",
f'Add-Type -AssemblyName Microsoft.VisualBasic; [Microsoft.VisualBasic.FileIO.FileSystem]::DeleteFile("{file_path}", "OnlyErrorDialogs", "SendToRecycleBin")', f'Add-Type -AssemblyName Microsoft.VisualBasic; [Microsoft.VisualBasic.FileIO.FileSystem]::DeleteFile("{file_path}", "OnlyErrorDialogs", "SendToRecycleBin")',
] ]
subprocess.run( return subprocess.run(
args, args,
check=True, 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() encoded_torrent = base64.b64encode(torrent_path.read_bytes()).decode()
payload = { payload = {
@@ -59,7 +63,48 @@ def add_torrent(client: httpx.Client, torrent_path: Path):
raise Exception(f"HTTP {res.status_code}: {res.text}") raise Exception(f"HTTP {res.status_code}: {res.text}")
result = res.json() 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(): 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("--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("--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("--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() return parser.parse_args()
@@ -79,19 +127,28 @@ def main():
client = httpx.Client( client = httpx.Client(
base_url=args.host, base_url=args.host,
auth=httpx.BasicAuth(*args.auth.split(":")) if args.auth else None, 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"] csrf_token = client.post("/transmission/rpc", json={"method": "session-get"}).headers["x-transmission-session-id"]
client.headers["X-Transmission-Session-Id"] = csrf_token client.headers["X-Transmission-Session-Id"] = csrf_token
hashes = []
for torrent_path in args.torrent_paths: for torrent_path in args.torrent_paths:
if not torrent_path.is_file(): 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) 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: if args.clean:
move_to_trash(torrent_path) 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__": if __name__ == "__main__":
main() main()