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:
@@ -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 <nombre></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."]
|
||||
|
||||
Reference in New Issue
Block a user