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."]