04-ago: [videoclub] comando 'serie <nombre>' (temporadas/capitulos con enlaces) + filtros ampliados (peliculas sin limite de anio, series con subs ES/EN) + lecciones 43-48

This commit is contained in:
juanjo
2026-08-04 01:04:45 +02:00
parent 46af244e56
commit 35264ac8e5
3 changed files with 226 additions and 18 deletions

View File

@@ -47,13 +47,43 @@ SERIE_PALABRAS = {"star trek", "stargate", "heidi", "euphoria", "from", "the pit
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"]
"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)", re.IGNORECASE)
# Idiomas que indican castellano/espanol/dual
CASTELLANO = re.compile(r"(Esp|Castellano|Span|dual|lat)", re.IGNORECASE)
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):
@@ -125,20 +155,15 @@ def fetch_catalogo():
return items
def seleccionar(items, ano_min=2024):
"""Novedades con buena calidad en castellano (excluye adulto y series sueltas)."""
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
try:
y = int(it["year"]) if it["year"] else 0
except ValueError:
y = 0
if y < ano_min:
continue
if it["tipo"] == "serie" and it.get("enlace") and "S01E0" in it["name"]:
continue
out.append(it)
@@ -146,6 +171,118 @@ def seleccionar(items, ano_min=2024):
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 = [], []
@@ -180,9 +317,8 @@ def trocear(lista, max_len=CHUNK_MAX):
return mensajes
def trocear_efimero(texto, items, max_len=CHUNK_MAX):
"""Trocea un texto con lineas 'titulo + descargar <url>' sin partir lineas."""
lineas = texto.split("\n")
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:
@@ -196,11 +332,16 @@ def trocear_efimero(texto, items, max_len=CHUNK_MAX):
return mensajes
def generar(fecha=None, ano_min=2024):
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, ano_min=ano_min)
sel = seleccionar(normales)
mensajes = trocear(sel)
if not mensajes:
mensajes = ["No hay novedades con buena calidad en castellano."]

View File

@@ -180,6 +180,25 @@ def procesar(texto, chat_id):
else "❌ No se pudo añadir al NAS", chat_id)
elif t in ("pelis", "/pelis"):
procesar_pelis(chat_id)
elif t in ("series", "/series"):
procesar_series(chat_id)
elif t.startswith("serie ") or t.startswith("/serie "):
nombre = texto.split(" ", 1)[1].strip()
try:
import pelis
except Exception as e:
log(f"serie import error: {e}")
enviar("❌ No se pudo cargar el modulo pelis", chat_id)
return
enviar("🔎 Buscando la serie en el catálogo...", chat_id)
try:
mensajes, encontrada = pelis.buscar_serie(nombre)
except Exception as e:
log(f"serie buscar error: {e}")
enviar(f"❌ Error consultando el catálogo: {e}", chat_id)
return
for m in mensajes:
enviar(m, chat_id)
elif t in ("ayuda", "/ayuda", "help", "/help"):
enviar("Comandos:\n"
"<b>estado</b> — estado de la Pi\n"
@@ -189,6 +208,8 @@ def procesar(texto, chat_id):
"<b>youtube &lt;URL&gt;</b> — reproduce YouTube y apaga al terminar\n"
"<b>descargar &lt;enlace&gt;</b> — añade a Download Station del NAS\n"
"<b>pelis</b> — novedades en castellano con comando listo\n"
"<b>series</b> — series en castellano con comando listo\n"
"<b>serie &lt;nombre&gt;</b> — temporadas/capitulos de una serie con enlaces\n"
"<b>panorama</b> — fotos de las cámaras\n"
"<b>limpieza</b> — limpia procesos atascados de la Pi\n"
"<b>reboot</b> — reinicia la Pi (necesita confirmar con /confirmar-reboot)\n"
@@ -212,6 +233,44 @@ def procesar(texto, chat_id):
sh("echo 'raspberry' | sudo -S reboot")
def procesar_series(chat_id):
"""Comando 'series': series con buena calidad en castellano del catalogo,
con el comando 'descargar <url>' listo. Adultos efimeros (5 min)."""
try:
import pelis
except Exception as e:
log(f"series import error: {e}")
enviar("❌ No se pudo cargar el modulo pelis", chat_id)
return
try:
enviar("📺 Consultando series del catálogo...", chat_id)
mensajes, adultos = pelis.generar_series()
except Exception as e:
log(f"series generar error: {e}")
enviar(f"❌ Error consultando el catálogo: {e}", chat_id)
return
for m in mensajes:
enviar(m, chat_id)
if adultos:
lineas = [f"🔞 Contenido adulto ({len(adultos)} novedades) — se borrará en 5 min:"]
for it in adultos[:30]:
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>"
lineas.append(linea)
if len(adultos) > 30:
lineas.append(f"... y {len(adultos) - 30} más")
texto = "\n".join(lineas)
for parte in pelis.trocear_efimero(texto, adultos):
mid = enviar(parte, chat_id)
if mid:
threading.Timer(300, borrar, args=(chat_id, mid)).start()
def procesar_pelis(chat_id):
"""Comando 'pelis': novedades con buena calidad en castellano, con el
comando 'descargar <url>' listo para copiar. Los adultos se envian en