diff --git a/videoclub/global_buscar.py b/videoclub/global_buscar.py
index 8ee9084..3507ebd 100644
--- a/videoclub/global_buscar.py
+++ b/videoclub/global_buscar.py
@@ -27,6 +27,15 @@ sys.path.insert(0, str(BASE))
CINEMETA = "https://v3-cinemeta.strem.io"
TORRENTIO = "https://torrentio.strem.fun/language=spanish"
+# Catalogo por genero (Cinemeta). "top" = mas descargadas, "imdbRating" = mejor valoradas.
+CINEMETA_CAT = "https://cinemeta-catalogs.strem.io"
+CATEGORIAS = ("top", "imdbRating")
+GENEROS = (
+ "Action", "Adventure", "Animation", "Biography", "Comedy", "Crime",
+ "Documentary", "Drama", "Family", "Fantasy", "History", "Horror",
+ "Mystery", "Romance", "Sci-Fi", "Sport", "Thriller", "War", "Western",
+)
+
HEADERS = {"User-Agent": "Mozilla/5.0"}
# Utilidades de clasificacion reutilizadas de videoclub_buscador
@@ -117,6 +126,65 @@ def torrents_de(imdb_id, tipo="movie"):
return torrents
+def generos_disponibles():
+ """Devuelve la lista de generos con su numero (1-based) para el usuario."""
+ return list(enumerate(GENEROS, 1))
+
+
+def catalogo_por_genero(tipo, genero, categoria="top", limite=30):
+ """'las N mas descargadas/valoradas' de un genero y tipo.
+ tipo: movie|series. genero: nombre en ingles (Action, Horror...).
+ categoria: top (mas descargadas) | imdbRating (mejor valoradas).
+ Devuelve lista de {name, type, imdb_id, year}."""
+ if categoria not in CATEGORIAS:
+ categoria = "top"
+ if genero not in GENEROS:
+ return []
+ url = (f"{CINEMETA_CAT}/{categoria}/catalog/{tipo}/{categoria}/"
+ f"genre={urllib.parse.quote(genero)}&skip=0.json")
+ try:
+ metas = _get_json(url).get("metas", [])
+ except Exception:
+ return []
+ lista = []
+ for m in metas:
+ imdb = m.get("id", "")
+ if not re.match(r"^tt\d+$", imdb):
+ continue
+ name = m.get("name", "")
+ year = m.get("year") or m.get("releaseInfo") or None
+ lista.append({
+ "name": name,
+ "type": tipo,
+ "imdb_id": imdb,
+ "year": str(year) if year else None,
+ })
+ return lista[:limite]
+
+
+def formatear_generos():
+ """Numeros la lista de generos para elegir por numero en Telegram."""
+ lineas = ["🎠Elige un genero por numero:", ""]
+ for n, g in generos_disponibles():
+ lineas.append(f"{n}. {g}")
+ return vcb.trocear_texto(lineas)
+
+
+def formatear_catalogo(resultados, tipo, genero, categoria):
+ """Numera los titulos de un catalogo por genero para elegir luego /torrents."""
+ if not resultados:
+ return [f"No hay resultados para {genero}."]
+ etiqueta = "🎬 Peliculas" if tipo == "movie" else "📺 Series"
+ orden = "mas descargadas" if categoria == "top" else "mejor valoradas"
+ lineas = [f"{etiqueta} · {genero} — {len(resultados)} {orden}:",
+ "Responde /torrents <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}")
+ return vcb.trocear_texto(lineas)
+
+
def _seeders(title):
m = re.search(r"👤\s*(\d+)", title)
return int(m.group(1)) if m else 0
@@ -214,6 +282,8 @@ if __name__ == "__main__":
print("Uso:")
print(" python global_buscar.py busca ")
print(" python global_buscar.py torrents [movie|series]")
+ print(" python global_buscar.py generos")
+ print(" python global_buscar.py catalogo [top|imdbRating] [limite]")
sys.exit(1)
cmd = sys.argv[1]
@@ -230,3 +300,16 @@ if __name__ == "__main__":
for m in formatear_torrents(res, imdb):
print(m)
print("---")
+ elif cmd == "generos":
+ for m in formatear_generos():
+ print(m)
+ print("---")
+ elif cmd == "catalogo":
+ tipo = sys.argv[2]
+ genero = sys.argv[3]
+ cat = sys.argv[4] if len(sys.argv) > 4 else "top"
+ lim = int(sys.argv[5]) if len(sys.argv) > 5 else 30
+ res = catalogo_por_genero(tipo, genero, cat, lim)
+ for m in formatear_catalogo(res, tipo, genero, cat):
+ print(m)
+ print("---")
diff --git a/vigilancia/pi_bot.py b/vigilancia/pi_bot.py
index c652644..ae2ed81 100644
--- a/vigilancia/pi_bot.py
+++ b/vigilancia/pi_bot.py
@@ -11,6 +11,8 @@ import requests
# Estado por chat para la busqueda global interactiva (numero -> descarga)
ultima_titulos = {} # chat_id -> {'query':..., 'results':[...]}
ultimos_torrents = {} # chat_id -> {'titulo':..., 'torrents':[...]}
+flujo_genero = {} # chat_id -> 'esperando_tipo' | 'esperando_genero' | None
+flujo_genero_tipo = {} # chat_id -> 'movie' | 'series' (tipo ya elegido)
BASE = os.path.dirname(os.path.abspath(__file__))
LOG = os.path.join(BASE, "bot.log")
@@ -127,7 +129,35 @@ def mandar_youtube(url):
def procesar(texto, chat_id):
t = texto.lower().strip()
log(f"RECIBIDO chat={chat_id} texto={texto!r}")
- if t in ("estado", "/estado", "status", "/status"):
+ # Flujo interactivo por genero (espera numeros en pasos)
+ if flujo_genero.get(chat_id) and t.isdigit():
+ paso = flujo_genero[chat_id]
+ if paso == "esperando_tipo":
+ if t == "1":
+ flujo_genero_tipo[chat_id] = "movie"
+ flujo_genero[chat_id] = "esperando_genero"
+ _pedir_genero(chat_id, "🎬")
+ elif t == "2":
+ flujo_genero_tipo[chat_id] = "series"
+ flujo_genero[chat_id] = "esperando_genero"
+ _pedir_genero(chat_id, "📺")
+ else:
+ enviar("OpciĂłn inválida. Responde 1 para PelĂculas o 2 para Series.", chat_id)
+ elif paso == "esperando_genero":
+ n = int(t)
+ gol = _import_global_buscar()
+ if 1 <= n <= len(gol.GENEROS):
+ genero = gol.GENEROS[n - 1]
+ tipo = flujo_genero_tipo.get(chat_id, "movie")
+ flujo_genero.pop(chat_id, None)
+ flujo_genero_tipo.pop(chat_id, None)
+ _mostrar_catalogo_genero(chat_id, tipo, genero, "top")
+ else:
+ enviar("Número de género inválido.", chat_id)
+ return
+ if t in ("genero", "/genero"):
+ procesar_genero(chat_id)
+ elif t in ("estado", "/estado", "status", "/status"):
msg, _ = estado()
enviar(msg, chat_id)
elif "temperatura" in t or "temp" in t:
@@ -216,6 +246,7 @@ def procesar(texto, chat_id):
"serie <nombre> — buscar serie con temporadas/capitulos\n"
"novedades — ultimas novedades del catalogo\n"
"busca <nombre> — buscar en TODO internet (miles de pelis)\n"
+ "genero — catálogo por género (30 más descargadas)\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"
@@ -425,6 +456,47 @@ def procesar_busca(nombre, chat_id):
log(f"BUSCA global '{nombre}': {len(resultados)} titulos")
+def procesar_genero(chat_id):
+ """Comando '/genero': inicia el flujo por genero. Pide el tipo."""
+ flujo_genero[chat_id] = "esperando_tipo"
+ flujo_genero_tipo.pop(chat_id, None)
+ enviar("🎠Catalogo por genero (30 mas descargadas). Primero elige el tipo:\n"
+ "1. 🎬 PelĂculas\n"
+ "2. 📺 Series\n\nResponde el 1 o el 2.", chat_id)
+
+
+def _pedir_genero(chat_id, icono):
+ 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
+ for m in gol.formatear_generos():
+ # anadimos el icono de tipo en la cabecera
+ if m.startswith("đźŽ"):
+ m = f"{icono} {m[1:]}"
+ enviar(m, chat_id)
+ enviar("Responde el numero del genero.", chat_id)
+
+
+def _mostrar_catalogo_genero(chat_id, tipo, genero, categoria):
+ enviar(f"🔎 Consultando {genero} ({'peliculas' if tipo=='movie' else 'series'})...", chat_id)
+ try:
+ gol = _import_global_buscar()
+ resultados = gol.catalogo_por_genero(tipo, genero, categoria, limite=30)
+ mensajes = gol.formatear_catalogo(resultados, tipo, genero, categoria)
+ except Exception as e:
+ log(f"catalogo genero error: {e}")
+ enviar(f"❌ Error consultando el catalogo: {e}", chat_id)
+ return
+ for m in mensajes:
+ enviar(m, chat_id)
+ if resultados:
+ ultima_titulos[chat_id] = {"query": f"{genero} ({tipo})", "results": resultados}
+ log(f"GENERO {tipo} '{genero}': {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)