diff --git a/documentacion/guia-busqueda-global-videoclub-2026-09-03.md b/documentacion/guia-busqueda-global-videoclub-2026-09-03.md new file mode 100644 index 0000000..7407af9 --- /dev/null +++ b/documentacion/guia-busqueda-global-videoclub-2026-09-03.md @@ -0,0 +1,67 @@ +# Guía — Búsqueda global del videoclub (miles de películas) + +**Fecha:** 3 Septiembre 2026 +**Autor:** asistente laboratorio + +## Qué resuelve + +El catálogo de Torrentio/RealDebrid (`/pelicula`, `/serie`) solo muestra lo que +**ya tienes** en tu cuenta de RealDebrid (tus 51 items). Para buscar películas y +series que **NO tienes todavía** —es decir, "miles de películas" de todo +internet— se añade la **búsqueda global** con los comandos `/busca`, `/torrents` +y `/bajar`. + +## Cómo funciona (2 pasos internos) + +``` +/busca + ↓ Cinemeta (Stremio, sin clave): texto → títulos + id IMDb + 1. Interstellar tt0816692 + 2. The Science of... tt4415360 + ↓ /torrents + 1. 4K | 7.5GB | 👤37 | Interstellar...2160p...x265-QTZ.mkv + 2. 1080p| 4.2GB | 👤511| Interstellar.2014.PROPER.IMAX.1080p... + ↓ /bajar + Magnet → Download Station del NAS (baja y deja sembrando) +``` + +1. **Texto → título:** `Cinemeta` (v3-cinemeta.strem.io) resuelve tu texto a los + títulos top con su id IMDb. Sin API key. +2. **Título → torrents:** `Torrentio` (/stream/movie|series/{imdb}.json) escanea + en tiempo real **1337x, PirateBay, TorrentGalaxy, Wolfmax4k, MejorTorrent...** + y devuelve decenas/cientos de torrents reales por título (de ahí las + "miles de pelis"). +3. **Torrent → NAS:** construimos el `magnet` desde el `infoHash` y lo enviamos a + Synology Download Station, que se descarga la película. + +## Cómo usarlo (desde Telegram) + +- `/busca `interstellar → lista de títulos numerados +- `/torrents `2` → torrents reales del título 2 (calidad, tamaño, seeders) +- `/bajar `1` → descarga al NAS el torrent 1 + +Cada número se elige en un comando nuevo. El bot recuerda tu última búsqueda por +chat, así que no hace falta repetir el nombre. + +## Archivos nuevos/modificados + +- `videoclub/global_buscar.py` — módulo nuevo de búsqueda global + - `buscar_titulos(texto)` → Cinemeta + - `torrents_de(imdb_id, tipo)` → Torrentio + - `filtrar_torrents()` → filtra calidad mala, ruso/tamil/otros idiomas + - `magnet_de(torrent)` → magnet a partir de infoHash + - `formatear_titulos()` / `formatear_torrents()` → mensajes numerados +- `vigilancia/pi_bot.py` — comandos `/busca`, `/torrents`, `/bajar` + + estado por chat +- Reutiliza `vigilancia/nas_dl.py` (Download Station) y + `videoclub/videoclub_buscador.py` (filtros de idioma/calidad) + +## Notas + +- Los filtros quitan CAMrips, y releases en ruso/tamil/hindi/etc. para quedarse + con contenido en **castellano o inglés**. +- Los resultados se ordenan por calidad (4K → 1080p → 720p) y luego por seeders. +- Torrentio consulta muchos proveedores en tiempo real: para títulos raros puede + tardar unos segundos o devolver pocos resultados. +- Para desplegar en la Pi: `git pull` + reiniciar el servicio `pi-bot` + (la búsqueda global necesita que `np`/`python` tenga salida a internet). diff --git a/videoclub/global_buscar.py b/videoclub/global_buscar.py new file mode 100644 index 0000000..8606be3 --- /dev/null +++ b/videoclub/global_buscar.py @@ -0,0 +1,223 @@ +""" +global_buscar.py — Busqueda GLOBAL de peliculas/series que NO estan en tu +biblioteca RealDebrid. Busca "miles de pelis" en internet. + +Flujo (2 pasos, sin estado persistente): +1) Texto -> titulo: Cinemeta (Stremio, sin clave) devuelve los titulos + mas relevantes con su id IMDb (ttXXXXXXX). +2) Titulo -> torrente: Torrentio (Stremio) devuelve los torrents reales + scaneados de 1337x, TPB, TorrentGalaxy, Wolfmax4k, ... + cada uno con infoHash para montar el magnet. + +Zona "miles de peliculas": Torrentio consulta decenas de proveedores en +tiempo real y devuelve cientos de stream por titulo, no solo tu biblioteca. + +Bajar a NAS: ./nas_dl.py "" (Download Station se encarga). +""" +import json +import re +import sys +import urllib.parse +import urllib.request +from pathlib import Path + +BASE = Path(__file__).resolve().parent +sys.path.insert(0, str(BASE)) + +CINEMETA = "https://v3-cinemeta.strem.io" +TORRENTIO = "https://torrentio.strem.fun/language=spanish" + +HEADERS = {"User-Agent": "Mozilla/5.0"} + +# Utilidades de clasificacion reutilizadas de videoclub_buscador +import videoclub_buscador as vcb + +# Idiomas que no interesan en el catalogo global (ruso, tamil, hindi, etc.) +OTRO_IDIOMA_GLOBAL = re.compile( + r"(Tamil|Hindi|Telugu|Malayalam|Turkish|Tur[cç]e|Zulu|Arabic|" + r"Cyrillic|Russian|Greek|Korean|Chinese|Japanese|Thai)", + re.IGNORECASE) +RUSO = re.compile(r"[а-яА-ЯёЁ]") + + +def _get_json(url, timeout=35): + req = urllib.request.Request(url, headers=HEADERS) + with urllib.request.urlopen(req, timeout=timeout) as r: + return json.loads(r.read().decode("utf-8")) + + +def buscar_titulos(termino, limite=10): + """Texto -> lista de {name, type, imdb_id, year}. Usa Cinemeta (sin clave).""" + term = (termino or "").strip() + if not term: + return [] + resultado, lista = [], [] + for _type in ("movie", "series"): + url = (f"{CINEMETA}/catalog/{_type}/top/" + f"search={urllib.parse.quote(term)}.json") + try: + metas = _get_json(url).get("metas", []) + except Exception: + metas = [] + for m in metas[:limite]: + imdb = m.get("id", "") + if not re.match(r"^tt\d+$", imdb): + continue + name = m.get("name", "") + year = m.get("year") or None + resultado.append({ + "name": name, + "type": _type, + "imdb_id": imdb, + "year": str(year) if year else None, + }) + # Intercalamos movie/series y deduplicamos por imdb_id + vistos = set() + for r in resultado: + if r["imdb_id"] in vistos: + continue + vistos.add(r["imdb_id"]) + lista.append(r) + return lista[:20] + + +def torrents_de(imdb_id, tipo="movie"): + """IMDb id -> lista de torrents reales de Torrentio. + Cada item: {name, title, info_hash, file_id, filename, seeders, size}.""" + if not re.match(r"^tt\d+$", imdb_id or ""): + return [] + url = f"{TORRENTIO}/stream/{tipo}/{imdb_id}.json" + try: + data = _get_json(url) + except Exception: + return [] + torrents = [] + for s in data.get("streams", []): + if not s.get("infoHash"): + continue + filename = (s.get("behaviorHints", {}) or {}).get("filename") or "" + title = (s.get("title") or "").replace("\n", " ") + torrents.append({ + "name": s.get("name", "").replace("\n", " "), + "title": title, + "info_hash": s["infoHash"], + "file_id": s.get("fileIdx", 0), + "filename": filename, + "seeders": _seeders(title), + "size": _size(title), + "calidad": _calidad(filename + " " + title), + }) + return torrents + + +def _seeders(title): + m = re.search(r"👤\s*(\d+)", title) + return int(m.group(1)) if m else 0 + + +def _size(title): + m = re.search(r"💾\s*([\d.]+)\s*(TB|GB|MB)", title) + if not m: + return None + val = float(m.group(1)) + if m.group(2) == "TB": + return val * 1024 + if m.group(2) == "MB": + return val / 1024 + return val + + +def _calidad(texto): + t = " " + texto.lower() + " " + if "2160p" in t or ("4k" in t and "1080p" not in t): + return "4K" + if "1080p" in t or "1080" in t: + return "1080p" + if "720p" in t: + return "720p" + if "4k" in t or "uhd" in t: + return "4K" + return "?" + + +def filtrar_torrents(torrents, limite=20): + """Descarta calidad mala y contenido claramente no en castellano/ingles.""" + utiles = [] + for t in torrents: + if vcb.CALIDAD_MALA.search(t["title"]): + continue + if vcb.OTRO_IDIOMA.search(t["title"]) or OTRO_IDIOMA_GLOBAL.search(t["title"]): + continue + if RUSO.search(t["title"]): + continue + if not (vcb.CASTELLANO.search(t["title"]) or vcb.INGLES.search(t["title"])): + continue + utiles.append(t) + # Ordenar: mejor calidad, mas seeders + orden = {"4K": 0, "1080p": 1, "720p": 2, "?": 3} + utiles.sort(key=lambda t: (orden.get(t["calidad"], 3), -t["seeders"])) + return utiles[:limite] + + +def magnet_de(t): + """Construye el magnet a partir del infoHash y nombre de fichero.""" + dn = urllib.parse.quote((t.get("filename") or t.get("title") or "video")) + return f"magnet:?xt=urn:btih:{t['info_hash']}&dn={dn}" + + +def formatear_titulos(resultados): + """Formatea resultados de Cinemeta numerados para Telegram.""" + if not resultados: + return ["No se encontraron resultados para ese termino."] + lineas = [f"🎬 Resultados globales ({len(resultados)}):", + "Responde /bajar <numero> para ver torrents.", ""] + for i, r in enumerate(resultados, 1): + icono = "📺" if r["type"] == "series" else "🎬" + y = f" ({r['year']})" if r.get("year") else "" + lineas.append(f"{i}. {icono} {r['name']}{y}") + lineas.append(f" {r['imdb_id']}") + return vcb.trocear_texto(lineas) + + +def formatear_torrents(torrents, titulo): + """Formatea los torrents de un titulo numerados con calidad/size/seeders.""" + if not torrents: + return [f"No se encontraron torrents utiles para {titulo}."] + lineas = [ + f"🧲 {titulo} — {len(torrents)} torrents.", + "Responde /bajar <numero> de estos para descargarlo al NAS.", + "", + ] + for i, t in enumerate(torrents, 1): + size = f"{t['size']:.1f}GB" if t.get("size") else "?" + lineas.append( + f"{i}. {t['calidad']} | {size} | 👤 {t['seeders']} | {t['filename'] or t['name']}") + return vcb.trocear_texto(lineas) + + +if __name__ == "__main__": + try: + sys.stdout.reconfigure(encoding="utf-8", errors="replace") + except Exception: + pass + + if len(sys.argv) < 2: + print("Uso:") + print(" python global_buscar.py busca ") + print(" python global_buscar.py torrents [movie|series]") + sys.exit(1) + + cmd = sys.argv[1] + if cmd == "busca": + q = " ".join(sys.argv[2:]) + for m in formatear_titulos(buscar_titulos(q)): + print(m) + print("---") + elif cmd == "torrents": + imdb = sys.argv[2] + tipo = sys.argv[3] if len(sys.argv) > 3 else "movie" + res = torrents_de(imdb, tipo) + res = filtrar_torrents(res) + for m in formatear_torrents(res, imdb): + print(m) + print("---") diff --git a/vigilancia/pi_bot.py b/vigilancia/pi_bot.py index 5795ff6..c652644 100644 --- a/vigilancia/pi_bot.py +++ b/vigilancia/pi_bot.py @@ -8,6 +8,10 @@ import urllib.parse import requests +# Estado por chat para la busqueda global interactiva (numero -> descarga) +ultima_titulos = {} # chat_id -> {'query':..., 'results':[...]} +ultimos_torrents = {} # chat_id -> {'titulo':..., 'torrents':[...]} + BASE = os.path.dirname(os.path.abspath(__file__)) LOG = os.path.join(BASE, "bot.log") OFFSET_FILE = os.path.join(BASE, "bot.offset") @@ -191,6 +195,15 @@ def procesar(texto, chat_id): procesar_buscador_serie(nombre, chat_id) elif t in ("novedades", "/novedades"): procesar_novedades(chat_id) + elif t.startswith("busca ") or t.startswith("/busca "): + nombre = texto.split(" ", 1)[1].strip() + procesar_busca(nombre, chat_id) + elif t.startswith("torrents ") or t.startswith("/torrents "): + n = _int_arg(texto) + procesar_torrents(n, chat_id) + elif t.startswith("bajar ") or t.startswith("/bajar "): + n = _int_arg(texto) + procesar_bajar(n, chat_id) elif t in ("ayuda", "/ayuda", "help", "/help"): enviar("Comandos:\n" "estado — estado de la Pi\n" @@ -202,6 +215,9 @@ def procesar(texto, chat_id): "pelicula <nombre> — buscar pelicula y descargar\n" "serie <nombre> — buscar serie con temporadas/capitulos\n" "novedades — ultimas novedades del catalogo\n" + "busca <nombre> — buscar en TODO internet (miles de pelis)\n" + "torrents <n> — ver torrents del resultado n de /busca\n" + "bajar <n> — descargar al NAS el torrent n de /torrents\n" "panorama — fotos de las cámaras\n" "limpieza — limpia procesos atascados de la Pi\n" "reboot — reinicia la Pi (necesita confirmar con /confirmar-reboot)\n" @@ -371,6 +387,105 @@ def procesar_novedades(chat_id): enviar(m, chat_id) +def _int_arg(texto): + """Extrae el numero de un comando tipo '/bajar 3'.""" + partes = texto.split() + for p in partes[1:]: + if p.isdigit(): + return int(p) + return None + + +def _import_global_buscar(): + sys.path.insert(0, os.path.join(BASE, "..", "videoclub")) + import global_buscar as gol + return gol + + +def procesar_busca(nombre, chat_id): + """Busqueda global (Cinemeta): texto -> titulos con id IMDb numerados.""" + try: + gol = _import_global_buscar() + except Exception as e: + log(f"global_buscar import error: {e}") + enviar("❌ No se pudo cargar el modulo de busqueda global", chat_id) + return + enviar(f"🔎 Buscando {nombre} en todo el catalogo online...", chat_id) + try: + resultados = gol.buscar_titulos(nombre) + mensajes = gol.formatear_titulos(resultados) + except Exception as e: + log(f"busca error: {e}") + enviar(f"❌ Error en la busqueda: {e}", chat_id) + return + for m in mensajes: + enviar(m, chat_id) + if resultados: + ultima_titulos[chat_id] = {"query": nombre, "results": resultados} + log(f"BUSCA global '{nombre}': {len(resultados)} titulos") + + +def procesar_torrents(n, chat_id): + """Muestra los torrents (Torrentio) del titulo numero n de la ultima busqueda.""" + estado = ultima_titulos.get(chat_id) + if not estado or n is None or n < 1 or n > len(estado["results"]): + enviar("Primero usa /busca <nombre> y luego " + "/torrents <numero>.", chat_id) + return + try: + gol = _import_global_buscar() + except Exception as e: + log(f"global_buscar import error: {e}") + enviar("❌ No se pudo cargar el modulo", chat_id) + return + r = estado["results"][n - 1] + enviar(f"🧲 Buscando torrents de {r['name']}...", chat_id) + try: + torrents = gol.torrents_de(r["imdb_id"], r["type"]) + torrents = gol.filtrar_torrents(torrents) + mensajes = gol.formatear_torrents(torrents, r["name"]) + except Exception as e: + log(f"torrents error: {e}") + enviar(f"❌ Error obteniendo torrents: {e}", chat_id) + return + for m in mensajes: + enviar(m, chat_id) + if torrents: + ultimos_torrents[chat_id] = {"titulo": r["name"], "torrents": torrents} + log(f"TORRENTS '{r['name']}': {len(torrents)} torrents") + + +def procesar_bajar(n, chat_id): + """Descarga el torrent numero n al NAS (Download Station via magnet).""" + estado = ultimos_torrents.get(chat_id) + if not estado or n is None or n < 1 or n > len(estado["torrents"]): + enviar("Primero usa /busca <nombre> y luego " + "/torrents <numero>.", chat_id) + return + try: + gol = _import_global_buscar() + except Exception as e: + log(f"global_buscar import error: {e}") + enviar("❌ No se pudo cargar el modulo", chat_id) + return + t = estado["torrents"][n - 1] + magnet = gol.magnet_de(t) + log(f"BAJAR '{estado['titulo']}' torrent {n}: {magnet[:60]}...") + try: + import nas_dl + ok = nas_dl.add_torrent(magnet) + except Exception as e: + log(f"bajar error: {e}") + enviar(f"❌ Error al anadir al NAS: {e}", chat_id) + return + if ok: + tamaño = f"{t['size']:.1f}GB" if t.get("size") else "?" + enviar(f"📥 Añadido al NAS: {t['filename'] or estado['titulo']} " + f"({t['calidad']}, {tamaño}).", chat_id) + else: + enviar("❌ No se pudo añadir al NAS.", chat_id) + + def monitor_temperatura(): while True: time.sleep(300)