Files

358 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
pelis.py — Listado de novedades con buena calidad en castellano del catalogo
RealDebrid/Torrentio, con el comando 'descargar <url>' ya montado para copiar
y pegar en Telegram. Lo usa el comando 'pelis' de pi_bot.py.
- Filtra: NO adulto, buena calidad, castellano.
- Trocea la salida en mensajes <= CHUNK_MAX para que Telegram no corte.
- Los items adultos se devuelven por separado (efimeros, nunca se guardan).
Autonomo (sin dependencias): usa solo urllib y la logica de clasificacion
duplicada de videoclub/videoclub.py para no depender del repo de negocio.
"""
import json
import os
import re
import urllib.request
from pathlib import Path
BASE = Path(__file__).resolve().parent
CHUNK_MAX = 3500 # limite de Telegram (~4096), con margen
# Reutiliza el .env del propio directorio (carga simple, sin python-dotenv)
ENV = {}
try:
for line in open(BASE / ".env"):
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
TORRENTIO_RD_TOKEN = os.getenv("TORRENTIO_RD_TOKEN") or ENV.get("TORRENTIO_RD_TOKEN", "")
TORRENTIO_CONFIG = os.getenv("TORRENTIO_CONFIG") or ENV.get("TORRENTIO_CONFIG", "language=spanish")
CATALOGO = ("https://torrentio.strem.fun/"
f"{TORRENTIO_CONFIG}|realdebrid={TORRENTIO_RD_TOKEN}/"
"catalog/other/torrentio-realdebrid.json")
YEAR_RE = re.compile(r"\((19\d\d|20\d\d)\)")
YEAR_BARE_RE = re.compile(r"\.(19\d\d|20\d\d)\.")
SERIE_RE = re.compile(r"(S\d{1,2}E\d{1,3}|Cap[\.\s]|cap[\.\s]|Temporada|temp\.|\bS0?1E\d|\dx\d\d)", re.IGNORECASE)
ADULTO_RE = re.compile(r"(\.XXX\.|XXX\.|private\.|brazzers|onlytarts|spankbang|xhamster|porntrex|missav|sexandsubmission|evilangel|monstersofcock|itsanal|thelifeerotic)", re.IGNORECASE)
SERIE_PALABRAS = {"star trek", "stargate", "heidi", "euphoria", "from", "the pitt",
"supergirl", "monarch", "paradise", "desaparecida", "vanished",
"strange new worlds", "star trek strange", "the studio",
"one punch man", "conan", "detective conan"}
EXTRA_SUCIO = ["wolfmax4k.com", "pctfenix", "pctfenix.com", "yts", "yts.mx",
"rarbg", "verpeliculasonline", "viruse", "proyecto", "kikorip",
"camrip", "telesync", "hdtv", "bluray", "remux", "webrip", "bdrip",
"repack", "proper", "xtrem", "webdl", "web-dl", "web", "dl", "x264", "x265",
"h264", "h265", "aac", "1080p", "720p", "4k", "2160p", "spanish",
"castellano", "dual", "lat", "es", "en", "audio", "sub", "subs"]
# Calidad mala -> se excluye de "buena calidad"
CALIDAD_MALA = re.compile(
r"(CAMRip|TS\.|HDTS|Telesync|KinoRip|Screener|HDCAM|HC\b|r5|telesync|MD-?AAC|360p|240p)", re.IGNORECASE)
# Idiomas que indican castellano/espanol/dual (audio o subs)
CASTELLANO = re.compile(r"(Esp|Castellano|Span|Spanish|dual|lat|\bES\b|audio)", re.IGNORECASE)
# Marcadores de ingles (audio o subs) que tambien cuentan como "bien"
INGLES = re.compile(r"(Eng|English|Ingl[eé]s|\bEN\b)", re.IGNORECASE)
# Marcadores claros de OTRO idioma sin espanol ni ingles (se excluyen)
OTRO_IDIOMA = re.compile(
r"(Ita\b|MIRCrew|RO\s?EN|South Wind|Silver Zero|Francais|Deutsch|Portugu[eê]s)", re.IGNORECASE)
# Capitulos/temporadas: "S01E05", "Cap 3", etc.
SERIE_EP_RE = re.compile(r"\bS(\d{1,2})E(\d{1,3})\b", re.IGNORECASE)
CAP_RE = re.compile(r"(?:Cap[\.\s]*(\d+))", re.IGNORECASE)
def parsear_episodio(name):
"""Extrae (temporada, capitulo) de un nombre de serie, o (0, 0)."""
m = SERIE_EP_RE.search(name)
if m:
return int(m.group(1)), int(m.group(2))
m = CAP_RE.search(name)
if m:
return 1, int(m.group(1))
return 0, 0
def serie_base(name):
"""Nombre base de la serie (lo que va antes del marcador SxxEyy o Cap.X)."""
m = SERIE_EP_RE.search(name) or CAP_RE.search(name)
base = name[:m.start()] if m else name
base = _limpiar_titulo_ep(base)
return base
def clean_filename(name):
"""Devuelve (titulo_limpio, year, tipo)."""
name = re.sub(r"\.(mkv|mp4|avi|mov|webm|m4v)$", "", name.strip(), flags=re.IGNORECASE)
year = None
m = YEAR_RE.search(name)
if m:
year = m.group(1)
name = name.replace(m.group(0), " ")
else:
m = YEAR_BARE_RE.search(name)
if m:
year = m.group(1)
name = name.replace(m.group(0), " ")
es_serie = bool(SERIE_RE.search(name)) or any(p in name.lower() for p in SERIE_PALABRAS)
es_adulto = bool(ADULTO_RE.search(name))
name = re.sub(r"\[[^\]]*\]", " ", name)
name = re.sub(r"[\(\)\[\]]", " ", name)
name = name.replace("_", " ").replace(".", " ")
name = re.sub(r"\s+", " ", name).strip()
tokens = [t for t in name.split(" ") if t.lower() not in EXTRA_SUCIO]
name = " ".join(tokens).strip(" -")
name = re.sub(r"\s+", " ", name).strip()
tipo = "adulto" if es_adulto else ("serie" if es_serie else "pelicula")
return name, year, tipo
def fetch_catalogo():
"""Descarga el catalogo + enlaces directos RD. Devuelve lista de items."""
if not TORRENTIO_RD_TOKEN:
return []
url = CATALOGO.format()
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
with urllib.request.urlopen(req, timeout=40) as r:
data = json.loads(r.read().decode("utf-8"))
enlaces = {}
try:
req2 = urllib.request.Request(
"https://api.real-debrid.com/rest/1.0/torrents",
headers={"Authorization": f"Bearer {TORRENTIO_RD_TOKEN}"})
with urllib.request.urlopen(req2, timeout=40) as r2:
for t in json.loads(r2.read().decode("utf-8")):
links = t.get("links") or []
if links:
enlaces[t["id"]] = links[0]
except Exception as e:
print(f"[pelis] No se pudieron obtener enlaces RD: {e}")
items = []
for m in data.get("metas", []):
name = m.get("name", "")
clean, year, tipo = clean_filename(name)
rd_id = m.get("id", "").split(":", 1)[-1]
items.append({
"id": m.get("id"),
"name": name,
"clean": clean,
"year": year,
"tipo": tipo,
"enlace": enlaces.get(rd_id),
"buena_calidad": not bool(CALIDAD_MALA.search(name)),
"castellano": bool(CASTELLANO.search(name)),
})
return items
def seleccionar(items):
"""Peliculas con buena calidad en castellano, sin limite de anio
(excluye adulto y series sueltas)."""
out = []
for it in items:
if it["tipo"] == "adulto":
continue
if not it["buena_calidad"] or not it["castellano"]:
continue
if it["tipo"] == "serie" and it.get("enlace") and "S01E0" in it["name"]:
continue
out.append(it)
out.sort(key=lambda x: (int(x["year"] or 0), x["clean"] or ""), reverse=True)
return out
def es_serie_idioma(it):
"""Una serie cuenta si tiene audio/subs en castellano o ingles
(marcador positivo o neutro, la config ya es language=spanish)
y NO tiene marcador claro de otro idioma."""
name = (it.get("name") or "").lower()
if OTRO_IDIOMA.search(name):
return False
return it.get("castellano") or bool(INGLES.search(name)) or True
def seleccionar_series(items):
"""Series con buena calidad en castellano/ingles (excluye adulto y peliculas)."""
out = []
for it in items:
if it["tipo"] != "serie":
continue
if not it["buena_calidad"]:
continue
if not es_serie_idioma(it):
continue
out.append(it)
out.sort(key=lambda x: (x["clean"] or "").lower())
return out
def agrupar_por_serie(items):
"""Agrupa los items de serie por nombre base y ordena por temporada/capitulo."""
grupos = {}
for it in items:
base = serie_base(it["name"]) or it["clean"] or "?"
grupos.setdefault(base, []).append(it)
for base in grupos:
grupos[base].sort(key=lambda x: parsear_episodio(x["name"]))
return grupos
def _limpiar_titulo_ep(texto):
"""Limpia un titulo de episodio: quita extension, calidad, idiomas, ruido."""
texto = re.sub(r"\.(mkv|mp4|avi|mov|webm|m4v)$", "", texto, flags=re.IGNORECASE)
texto = re.sub(r"\[[^\]]*\]", " ", texto)
texto = re.sub(r"\b\d+x\d+\b", " ", texto, flags=re.IGNORECASE)
texto = texto.replace("_", " ").replace(".", " ").replace("-", " ")
texto = re.sub(r"[\[\]\(\)]", " ", texto)
tokens = [t for t in texto.split(" ") if t.lower() not in EXTRA_SUCIO]
texto = " ".join(tokens).strip(" -")
return re.sub(r"\s+", " ", texto).strip()
def formatear_episodio(it):
"""Linea 'SxxExx titulo' con el comando descargar debajo."""
temp, cap = parsear_episodio(it["name"])
m = SERIE_EP_RE.search(it["name"])
if temp:
etiqueta = f"S{temp:02d}E{cap:02d}"
if m:
resto = _limpiar_titulo_ep(it["name"][m.end():])
else:
resto = _limpiar_titulo_ep(it["name"])
linea = f" {etiqueta}{resto}" if resto else f" {etiqueta}"
else:
linea = f" {it['clean']}"
enlace = it.get("enlace") or ""
if enlace:
linea += f"\n <code>descargar {enlace}</code>"
return linea
def generar_series():
"""Devuelve (mensajes_series, adultos_list). Series agrupadas por serie,
cada una con sus temporadas/capitulos y el comando 'descargar' listo."""
items = fetch_catalogo()
normales, adultos = separar_adultos(items)
sel = seleccionar_series(normales)
grupos = agrupar_por_serie(sel)
lineas = []
for base in sorted(grupos, key=lambda x: x.lower()):
lineas.append(f"📺 <b>{base}</b>")
for it in grupos[base]:
lineas.append(formatear_episodio(it))
mensajes = trocear_texto(lineas)
if not mensajes:
mensajes = ["No hay series con buena calidad (castellano/ingles)."]
return mensajes, adultos
def buscar_serie(nombre, items=None):
"""Devuelve (mensajes, encontrada). Busca una serie por nombre y lista
sus temporadas/capitulos con el comando 'descargar' listo."""
term = (nombre or "").strip().lower()
if not term:
return ["Uso: <b>serie &lt;nombre&gt;</b>"], False
if items is None:
items = fetch_catalogo()
normales, _ = separar_adultos(items)
coincidencias = {}
for it in normales:
if it["tipo"] != "serie":
continue
base = serie_base(it["name"]) or it["clean"] or ""
if term in base.lower() or term in (it["name"] or "").lower():
coincidencias.setdefault(base, []).append(it)
if not coincidencias:
return [f"No encuentro ninguna serie con <b>{nombre}</b>."], False
lineas = []
for base in sorted(coincidencias, key=lambda x: x.lower()):
lista = sorted(coincidencias[base], key=lambda x: parsear_episodio(x["name"]))
lineas.append(f"📺 <b>{base}</b>")
for it in lista:
lineas.append(formatear_episodio(it))
return trocear_texto(lineas), True
def separar_adultos(items):
"""Separa los items adultos del resto."""
normales, adultos = [], []
for it in items:
(adultos if it["tipo"] == "adulto" else normales).append(it)
return normales, adultos
def formatear_linea(it):
"""Línea lista para copiar/pegar: titulo + 'descargar <url>'."""
y = f" ({it['year']})" if it["year"] else ""
enlace = it.get("enlace") or ""
linea = f"{it['clean']}{y}"
if enlace:
linea += f"\n<code>descargar {enlace}</code>"
return linea
def trocear(lista, max_len=CHUNK_MAX):
"""Convierte items en mensajes <= max_len. Cada item no se parte."""
mensajes, actual = [], []
long = 0
for it in lista:
linea = formatear_linea(it)
if actual and long + len(linea) + 1 > max_len:
mensajes.append("\n".join(actual))
actual, long = [], 0
actual.append(linea)
long += len(linea) + 1
if actual:
mensajes.append("\n".join(actual))
return mensajes
def trocear_texto(lineas, max_len=CHUNK_MAX):
"""Convierte una lista de lineas en mensajes <= max_len sin partir lineas."""
mensajes, actual = [], []
long = 0
for linea in lineas:
if actual and long + len(linea) + 1 > max_len:
mensajes.append("\n".join(actual))
actual, long = [], 0
actual.append(linea)
long += len(linea) + 1
if actual:
mensajes.append("\n".join(actual))
return mensajes
def trocear_efimero(texto, items, max_len=CHUNK_MAX):
"""Trocea un texto con lineas 'titulo + descargar <url>' sin partir lineas."""
return trocear_texto(texto.split("\n"), max_len)
def generar(fecha=None):
"""Devuelve (mensajes_normales, adultos_list). Uso desde pi_bot."""
items = fetch_catalogo()
normales, adultos = separar_adultos(items)
sel = seleccionar(normales)
mensajes = trocear(sel)
if not mensajes:
mensajes = ["No hay novedades con buena calidad en castellano."]
return mensajes, adultos
if __name__ == "__main__":
msgs, adult = generar()
for i, m in enumerate(msgs, 1):
print(f"--- Mensaje {i}/{len(msgs)} ({len(m)} chars) ---")
print(m)
print()
print(f"ADULTOS: {len(adult)} (efimeros, no se listan aqui)")