feat(transmission): add wait and path reporting
This commit is contained in:
+68
-11
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user