feat: add Transmission monitoring to dashboard
- Add backend API for Transmission RPC integration - Create frontend page with torrent and tracker status - Add health monitoring script - Display daemon status, speeds, and tracker errors - Support reannounce, start/stop actions
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
import subprocess
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
router = APIRouter(prefix="/api/transmission", tags=["transmission"])
|
||||
|
||||
|
||||
def get_session_id() -> str:
|
||||
"""Get Transmission RPC session ID"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["ssh", "nas", "curl -si -u 'admin:admin' 'http://127.0.0.1:9091/transmission/rpc' 2>&1 | grep -i 'X-Transmission-Session-Id:' | cut -d' ' -f2 | tr -d '\\r'"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
shell=True,
|
||||
timeout=5
|
||||
)
|
||||
session_id = result.stdout.strip()
|
||||
if not session_id:
|
||||
raise Exception("Failed to get session ID")
|
||||
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(
|
||||
"http://100.78.131.124:9091/transmission/rpc",
|
||||
json=payload,
|
||||
auth=("admin", "admin"),
|
||||
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))
|
||||
Reference in New Issue
Block a user