import subprocess from typing import Any, Dict, List import httpx from fastapi import APIRouter, HTTPException import config router = APIRouter(prefix="/api/transmission", tags=["transmission"]) def get_session_id() -> str: """Get Transmission RPC session ID""" try: with httpx.Client(timeout=5.0) as client: response = client.get( config.TRANSMISSION_URL, auth=(config.TRANSMISSION_USER, config.TRANSMISSION_PASS) ) session_id = response.headers.get("X-Transmission-Session-Id") if not session_id: raise Exception("Failed to get session ID from headers") return session_id except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to get Transmission session ID: {str(e)}") def transmission_rpc(method: str, arguments: Dict[str, Any] = None) -> Dict[str, Any]: """Make Transmission RPC call""" try: session_id = get_session_id() payload = {"method": method} if arguments: payload["arguments"] = arguments # Use httpx to make the request with httpx.Client(timeout=10.0) as client: response = client.post( config.TRANSMISSION_URL, json=payload, auth=(config.TRANSMISSION_USER, config.TRANSMISSION_PASS), headers={"X-Transmission-Session-Id": session_id} ) if response.status_code == 200: data = response.json() if data.get("result") == "success": return data.get("arguments", {}) else: raise Exception(f"RPC error: {data.get('result')}") else: raise Exception(f"HTTP {response.status_code}: {response.text}") except Exception as e: raise HTTPException(status_code=500, detail=f"Transmission RPC failed: {str(e)}") @router.get("/status") async def get_status(): """Get Transmission daemon status""" try: # Check if process is running result = subprocess.run( ["ssh", "nas", "ps aux | grep transmission-daemon | grep sc-transmission | grep -v grep"], capture_output=True, text=True, shell=True, timeout=5 ) is_running = bool(result.stdout.strip()) if not is_running: return { "running": False, "message": "Transmission daemon is not running" } # Get session stats try: stats = transmission_rpc("session-stats") return { "running": True, "stats": stats } except: return { "running": True, "message": "Daemon running but RPC not accessible" } except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.get("/torrents") async def get_torrents(): """Get list of torrents with tracker status""" try: data = transmission_rpc("torrent-get", { "fields": [ "id", "name", "status", "percentDone", "rateDownload", "rateUpload", "uploadRatio", "trackerStats", "error", "errorString" ] }) torrents = data.get("torrents", []) # Process tracker stats for torrent in torrents: tracker_errors = [] for tracker in torrent.get("trackerStats", []): if tracker.get("lastAnnounceResult") and tracker.get("lastAnnounceResult") != "Success": tracker_errors.append({ "host": tracker.get("host"), "error": tracker.get("lastAnnounceResult") }) torrent["trackerErrors"] = tracker_errors torrent["hasTrackerErrors"] = len(tracker_errors) > 0 return {"torrents": torrents} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.get("/trackers") async def get_tracker_summary(): """Get summary of tracker connectivity""" try: data = transmission_rpc("torrent-get", { "fields": ["trackerStats"] }) torrents = data.get("torrents", []) tracker_summary = {} for torrent in torrents: for tracker in torrent.get("trackerStats", []): host = tracker.get("host") if host not in tracker_summary: tracker_summary[host] = { "host": host, "total": 0, "errors": 0, "lastError": None } tracker_summary[host]["total"] += 1 if tracker.get("lastAnnounceResult") and tracker.get("lastAnnounceResult") != "Success": tracker_summary[host]["errors"] += 1 tracker_summary[host]["lastError"] = tracker.get("lastAnnounceResult") return {"trackers": list(tracker_summary.values())} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.get("/stats") async def get_stats(): """Get Transmission statistics""" try: stats = transmission_rpc("session-stats") return stats except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.post("/start/{torrent_id}") async def start_torrent(torrent_id: int): """Start a torrent""" try: transmission_rpc("torrent-start", {"ids": [torrent_id]}) return {"success": True} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.post("/stop/{torrent_id}") async def stop_torrent(torrent_id: int): """Stop a torrent""" try: transmission_rpc("torrent-stop", {"ids": [torrent_id]}) return {"success": True} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.post("/verify/{torrent_id}") async def verify_torrent(torrent_id: int): """Verify a torrent""" try: transmission_rpc("torrent-verify", {"ids": [torrent_id]}) return {"success": True} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.post("/reannounce/{torrent_id}") async def reannounce_torrent(torrent_id: int): """Force reannounce to tracker""" try: transmission_rpc("torrent-reannounce", {"ids": [torrent_id]}) return {"success": True} except Exception as e: raise HTTPException(status_code=500, detail=str(e))