Files
simva-home/backend/app/transmission.py
T
2026-10-23 12:05:26 +09:00

130 lines
5.1 KiB
Python

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()