""" nas_dl.py — Anade descargas al NAS Synology via API de Download Station. Usado por pi_bot.py (comando 'descargar') y por videoclub.py. Login: POST /webapi/auth.cgi (SYNO.API.Auth, version 6, session DownloadStation) Crear: POST /webapi/DownloadStation/task.cgi (SYNO.DownloadStation.Task, create) La URI puede ser un enlace HTTP directo (p. ej. enlace de RealDebrid), un magnet o una URL de fichero .torrent. """ import json import os import urllib.parse import urllib.request # Carga .env del directorio del script si existe (no requiere python-dotenv) BASE = os.path.dirname(os.path.abspath(__file__)) def _load_env(path): env = {} try: for line in open(path): line = line.strip() if line and not line.startswith("#") and "=" in line: k, v = line.split("=", 1) env[k.strip()] = v.strip() except Exception: pass return env ENV = _load_env(os.path.join(BASE, ".env")) # Respaldo: si no hay credenciales NAS aqui, probar el .env de videoclub/ _VIDEOCLUB_ENV = _load_env(os.path.join(BASE, "..", "videoclub", ".env")) for _k in ("NAS_HOST", "NAS_PORT", "NAS_USER", "NAS_PASS", "NAS_HTTPS", "TORRENTIO_RD_TOKEN"): if not ENV.get(_k): ENV[_k] = _VIDEOCLUB_ENV.get(_k, "") NAS_HOST = os.getenv("NAS_HOST", ENV.get("NAS_HOST", "192.168.50.31")).strip().rstrip("/") NAS_PORT = os.getenv("NAS_PORT", ENV.get("NAS_PORT", "5000")).strip() NAS_USER = os.getenv("NAS_USER", ENV.get("NAS_USER", "")).strip() NAS_PASS = os.getenv("NAS_PASS", ENV.get("NAS_PASS", "")).strip() NAS_HTTPS = os.getenv("NAS_HTTPS", ENV.get("NAS_HTTPS", "0")).strip() == "1" RD_TOKEN = os.getenv("TORRENTIO_RD_TOKEN", ENV.get("TORRENTIO_RD_TOKEN", "")).strip() BASE_URL = f"{'https' if NAS_HTTPS else 'http'}://{NAS_HOST}:{NAS_PORT}" def unrestrict_rd(uri): """Convierte un enlace real-debrid.com/d/XXX en el enlace directo descargable. Si el enlace no es de RealDebrid, lo devuelve tal cual.""" if "real-debrid.com/" not in uri: return uri if not RD_TOKEN: print("[RD] Falta TORRENTIO_RD_TOKEN en .env") return uri try: data = urllib.parse.urlencode({"link": uri}).encode() req = urllib.request.Request( "https://api.real-debrid.com/rest/1.0/unrestrict/link", data=data, headers={"Authorization": f"Bearer {RD_TOKEN}"}) with urllib.request.urlopen(req, timeout=30) as r: d = json.loads(r.read().decode("utf-8")) directo = d.get("download") if directo: print(f"[RD] Desrestrict OK: {directo[:80]}") return directo print(f"[RD] No devolvio enlace directo: {d.get('error', d)}") except Exception as e: print(f"[RD] Error desrestrict: {e}") return uri def _api(path, params, session=None): """Llama a la API del NAS. DSM 7 exige GET con `_sid` en la query string. Si `session` viene como 'sid=' se extrae y se anade a la query.""" sid = None if session: if session.startswith("sid="): sid = session[4:] elif session.startswith("_sid="): sid = session[5:] if sid: params = dict(params) params["_sid"] = sid qs = urllib.parse.urlencode(params) req = urllib.request.Request(f"{BASE_URL}{path}?{qs}", headers={"Cookie": session or ""}) with urllib.request.urlopen(req, timeout=20) as r: return json.loads(r.read().decode()) def login(): """Devuelve la cookie de sesion DownloadStation o None.""" if not NAS_USER or not NAS_PASS: print("[NAS] Faltan NAS_USER/NAS_PASS en .env") return None try: r = _api("/webapi/auth.cgi", { "api": "SYNO.API.Auth", "version": "6", "method": "login", "account": NAS_USER, "passwd": NAS_PASS, "session": "DownloadStation", "format": "cookie", }) if r.get("success"): sid = r.get("data", {}).get("sid") return f"sid={sid}" if sid else None print(f"[NAS] Login fallido: {r.get('error')}") except Exception as e: print(f"[NAS] Error de login: {e}") return None def add_torrent(uri, session=None): """Anade una URI (http/magnet/.torrent) a Download Station. Si es enlace de RealDebrid, primero se desrestricta a enlace directo.""" uri = unrestrict_rd(uri) if not session: session = login() if not session: return False try: r = _api("/webapi/DownloadStation/task.cgi", { "api": "SYNO.DownloadStation.Task", "version": "3", "method": "create", "uri": uri, }, session=session) if r.get("success"): print(f"[NAS] Tarea anadida: {uri[:80]}") return True print(f"[NAS] Error al anadir tarea: {r.get('error')}") except Exception as e: print(f"[NAS] Error: {e}") return False def human_size(n): """Formatea bytes a una unidad legible.""" n = float(n or 0) for u in ("B", "KB", "MB", "GB", "TB"): if n < 1024: return f"{n:.1f}{u}" n /= 1024 return f"{n:.1f}PB" def progress_bar(percent, width=12): """Genera una barra de progreso compacta para mensajes de Telegram.""" percent = max(0.0, min(100.0, float(percent or 0))) filled = int(round(percent * width / 100)) return "█" * filled + "░" * (width - filled) def listar_descargas(hablar_solo_activas=True, session=None): """Devuelve (lineas_texto, activas). Lista las tareas de Download Station con estado, porcentaje de progreso, tamaño descargado y velocidad. `progress` se calcula con downloaded_pieces/total_pieces (fiable en NAS).""" if not session: session = login() if not session: return ["❌ No se pudo autenticar con Download Station."], [] try: r = _api("/webapi/DownloadStation/task.cgi", { "api": "SYNO.DownloadStation.Task", "version": "3", "method": "list", "additional": "detail,transfer", }, session=session) if not r.get("success"): return [f"❌ Error al listar: {r.get('error')}"], [] except Exception as e: return [f"❌ Error al consultar el NAS: {e}"], [] tareas = r.get("data", {}).get("tasks", []) or [] lineas, activas, resto = [], [], [] for t in tareas: titulo = t.get("title") or "" estado = t.get("status") or "?" detail = (t.get("additional") or {}).get("detail") or {} transfer = (t.get("additional") or {}).get("transfer") or {} total_pieces = detail.get("total_pieces") or 0 done_pieces = transfer.get("downloaded_pieces") or 0 pct = (done_pieces * 100.0 / total_pieces) if total_pieces else 0 size = transfer.get("size_downloaded") or 0 speed = transfer.get("speed_download") or 0 if estado == "downloading": activas.append(f"▶️ {progress_bar(pct)} {pct:.1f}% {human_size(size)} ⬇️{speed // 1024} KB/s\n" f" {titulo}") else: resto.append((estado, pct, size, titulo)) if activas: lineas.append("📥 DESCARGANDO") lineas.extend(activas) lineas.append("") if not hablar_solo_activas: lineas.append("📁 Otras tareas:") for estado, pct, size, titulo in resto: icono = {"finished": "✅", "error": "❌", "paused": "⏸️", "seeding": "🌱"}.get(estado, "•") if estado == "finished": lineas.append(f"{icono} {titulo}") else: lineas.append(f"{icono} {estado} ({pct:.0f}%) — {titulo}") if not activas: lineas.append("No hay torrents descargándose ahora mismo.") return lineas, activas if __name__ == "__main__": import sys if len(sys.argv) < 2: print("Uso: python nas_dl.py ") sys.exit(1) ok = add_torrent(sys.argv[1]) sys.exit(0 if ok else 1)