import logging from typing import Dict, Any, List, Optional import httpx from .config import settings logger = logging.getLogger(__name__) STATUS_MAP = { 0: "일시정지됨", 1: "검사 대기", 2: "검사 중", 3: "다운로드 대기", 4: "다운로드 중", 5: "시딩 대기", 6: "시딩 중 (완료)", } class TransmissionClient: def __init__(self): self.session_id: str = "" self.base_url = settings.rpc_url self.auth = None if settings.TRANSMISSION_USER and settings.TRANSMISSION_PASSWORD: self.auth = (settings.TRANSMISSION_USER, settings.TRANSMISSION_PASSWORD) async def _request(self, method: str, arguments: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: """Transmission RPC 요청 전송 (세션 ID 자동 획득 및 409 처리)""" payload = {"method": method} if arguments: payload["arguments"] = arguments async with httpx.AsyncClient(timeout=10.0, auth=self.auth) as client: headers = {} if self.session_id: headers["X-Transmission-Session-Id"] = self.session_id # 첫 번째 요청 response = await client.post(self.base_url, json=payload, headers=headers) # 409 Conflict: 세션 ID 갱신 후 재시도 if response.status_code == 409: new_session_id = response.headers.get("X-Transmission-Session-Id") if not new_session_id: raise Exception("Transmission에서 X-Transmission-Session-Id 헤더를 받지 못했습니다.") self.session_id = new_session_id headers["X-Transmission-Session-Id"] = self.session_id response = await client.post(self.base_url, json=payload, headers=headers) if response.status_code != 200: raise Exception(f"Transmission RPC 오류: HTTP {response.status_code} - {response.text}") res_data = response.json() if res_data.get("result") != "success": raise Exception(f"Transmission 요청 실패: {res_data.get('result')}") return res_data async def add_torrent(self, magnet_or_url: str, download_dir: Optional[str] = None, paused: bool = False) -> Dict[str, Any]: """마그넷 또는 토렌트 URL 추가""" arguments: Dict[str, Any] = { "filename": magnet_or_url.strip(), "paused": paused } if download_dir and download_dir.strip(): arguments["download-dir"] = download_dir.strip() data = await self._request("torrent-add", arguments) args = data.get("arguments", {}) # 'torrent-added' or 'torrent-duplicate' if "torrent-added" in args: return {"status": "added", "torrent": args["torrent-added"]} elif "torrent-duplicate" in args: return {"status": "duplicate", "torrent": args["torrent-duplicate"]} else: return {"status": "unknown", "arguments": args} async def get_torrents(self) -> List[Dict[str, Any]]: """토렌트 목록 조회""" fields = [ "id", "name", "status", "percentDone", "rateDownload", "rateUpload", "totalSize", "eta", "downloadDir", "errorString", "isFinished", "addedDate" ] data = await self._request("torrent-get", {"fields": fields}) torrents = data.get("arguments", {}).get("torrents", []) # 가공 및 상태 레이블 추가 result = [] for t in torrents: status_code = t.get("status", 0) result.append({ "id": t.get("id"), "name": t.get("name", "알 수 없음"), "status": status_code, "status_label": STATUS_MAP.get(status_code, "알 수 없음"), "percent_done": round(t.get("percentDone", 0) * 100, 2), "rate_download": t.get("rateDownload", 0), "rate_upload": t.get("rateUpload", 0), "total_size": t.get("totalSize", 0), "eta": t.get("eta"), "download_dir": t.get("downloadDir"), "error_string": t.get("errorString"), "is_finished": t.get("isFinished", False), }) return result async def remove_torrent(self, torrent_id: int, delete_local_data: bool = False) -> None: """토렌트 삭제""" arguments = { "ids": [torrent_id], "delete-local-data": delete_local_data } await self._request("torrent-remove", arguments) async def start_torrent(self, torrent_id: int) -> None: """토렌트 시작/재개""" await self._request("torrent-start", {"ids": [torrent_id]}) async def stop_torrent(self, torrent_id: int) -> None: """토렌트 일시정지""" await self._request("torrent-stop", {"ids": [torrent_id]}) async def get_session_stats(self) -> Dict[str, Any]: """세션 정보 및 통계 확인""" session_data = await self._request("session-get") return session_data.get("arguments", {}) transmission_client = TransmissionClient()