234 lines
8.5 KiB
Python
234 lines
8.5 KiB
Python
"""
|
||
videoclub.py — Listado diario de videos disponibles en el catalogo RealDebrid
|
||
de Torrentio (addon de Stremio).
|
||
|
||
Consultas:
|
||
https://torrentio.strem.fun/<config>/catalog/other/torrentio-realdebrid.json
|
||
|
||
Hace snapshot diario, detecta novedades respecto al dia anterior y genera
|
||
un listado markdown. Con --notify envia resumen por Telegram.
|
||
"""
|
||
import argparse
|
||
import json
|
||
import re
|
||
import sys
|
||
import urllib.request
|
||
from datetime import datetime
|
||
from pathlib import Path
|
||
|
||
BASE = Path(__file__).resolve().parent
|
||
sys.path.insert(0, str(BASE))
|
||
|
||
import config
|
||
|
||
CATALOGO = "https://torrentio.strem.fun/{config}/catalog/other/torrentio-realdebrid.json"
|
||
|
||
# Patrones para clasificar titulos
|
||
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)
|
||
EPISODE_RE = re.compile(r"\.S\d{1,2}E\d{1,3}\.", 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"]
|
||
|
||
|
||
def clean_filename(name):
|
||
"""Limpia un nombre de fichero y 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))
|
||
|
||
# Quitar etiquetas entre corchetes y otros ruidos
|
||
name = re.sub(r"\[[^\]]*\]", " ", name)
|
||
name = re.sub(r"[\(\)\[\]]", " ", name)
|
||
name = name.replace("_", " ").replace(".", " ")
|
||
# Colapsar espacios
|
||
name = re.sub(r"\s+", " ", name).strip()
|
||
# Quitar palabras sueltas de calidad/formato residuales
|
||
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 y devuelve lista de items [{id,name,tipo,clean,year}]."""
|
||
url = CATALOGO.format(config=config.TORRENTIO_CONFIG_WITH_TOKEN)
|
||
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"))
|
||
items = []
|
||
for m in data.get("metas", []):
|
||
clean, year, tipo = clean_filename(m.get("name", ""))
|
||
items.append({
|
||
"id": m.get("id"),
|
||
"name": m.get("name"),
|
||
"clean": clean,
|
||
"year": year,
|
||
"tipo": tipo,
|
||
})
|
||
return items
|
||
|
||
|
||
def cargar_snapshot(fecha):
|
||
p = BASE / "snapshots" / f"{fecha}.json"
|
||
if not p.exists():
|
||
return None
|
||
with open(p, encoding="utf-8") as f:
|
||
return json.load(f)
|
||
|
||
|
||
def guardar_snapshot(fecha, items):
|
||
p = BASE / "snapshots" / f"{fecha}.json"
|
||
with open(p, "w", encoding="utf-8") as f:
|
||
json.dump({"fecha": fecha, "items": items}, f, ensure_ascii=False, indent=2)
|
||
|
||
|
||
def diff_novedades(hoy, ayer):
|
||
"""Devuelve items de hoy que no estaban en el snapshot de ayer."""
|
||
if not ayer:
|
||
return hoy
|
||
ids_ayer = {i["id"] for i in ayer["items"]}
|
||
return [i for i in hoy if i["id"] not in ids_ayer]
|
||
|
||
|
||
def agrupar(items):
|
||
g = {"pelicula": [], "serie": [], "adulto": [], "otro": []}
|
||
for it in items:
|
||
tipo = it["tipo"]
|
||
if tipo not in g:
|
||
tipo = "otro"
|
||
g[tipo].append(it)
|
||
for k in g:
|
||
g[k].sort(key=lambda x: (x["clean"] or "").lower())
|
||
return g
|
||
|
||
|
||
def generar_markdown(fecha, items, novedades):
|
||
g = agrupar(items)
|
||
gn = agrupar(novedades)
|
||
lineas = [f"# Videoclub — {fecha}", ""]
|
||
lineas.append(f"**Total disponibles: {len(items)}** | "
|
||
f"Novedades: {len(novedades)}")
|
||
lineas.append("")
|
||
for seccion, clave in [("Novedades", None)]:
|
||
if novedades:
|
||
lineas.append(f"## Novedades ({len(novedades)})")
|
||
lineas.append("")
|
||
for it in novedades:
|
||
marca = {"pelicula": "🎬", "serie": "📺", "adulto": "🔞", "otro": "📁"}.get(it["tipo"], "📁")
|
||
y = f" ({it['year']})" if it["year"] else ""
|
||
lineas.append(f"- {marca} {it['clean']}{y} `{it['name'][:60]}`")
|
||
lineas.append("")
|
||
for nombre, clave in [("Películas", "pelicula"), ("Series", "serie"),
|
||
("Adulto", "adulto"), ("Otros", "otro")]:
|
||
if g[clave]:
|
||
lineas.append(f"## {nombre} ({len(g[clave])})")
|
||
lineas.append("")
|
||
for it in g[clave]:
|
||
y = f" ({it['year']})" if it["year"] else ""
|
||
lineas.append(f"- {it['clean']}{y}")
|
||
lineas.append("")
|
||
return "\n".join(lineas)
|
||
|
||
|
||
def enlace_realdebrid(rd_id):
|
||
"""Devuelve el enlace directo de descarga para un id de torrent RD."""
|
||
import urllib.parse
|
||
rd_id = rd_id.split(":", 1)[-1]
|
||
tok = config.TORRENTIO_RD_TOKEN
|
||
url = f"https://api.real-debrid.com/rest/1.0/torrents/info/{urllib.parse.quote(rd_id)}"
|
||
req = urllib.request.Request(url, headers={"Authorization": f"Bearer {tok}"})
|
||
with urllib.request.urlopen(req, timeout=30) as r:
|
||
info = json.loads(r.read().decode("utf-8"))
|
||
links = info.get("links") or []
|
||
if not links:
|
||
print(f"[RD] Sin enlace descargable para {rd_id}")
|
||
return None
|
||
return links[0]
|
||
|
||
|
||
def buscar_item(items, termino):
|
||
"""Busca un item del catalogo por titulo (case-insensitive) o por id."""
|
||
t = termino.strip().lower()
|
||
for it in items:
|
||
if it["id"].lower() == t or it["id"].lower().replace("realdebrid:", "") == t:
|
||
return it
|
||
if t in (it.get("clean") or "").lower() or t in (it.get("name") or "").lower():
|
||
return it
|
||
return None
|
||
|
||
|
||
def main():
|
||
ap = argparse.ArgumentParser(description="Videoclub: listado diario RealDebrid")
|
||
ap.add_argument("--notify", action="store_true",
|
||
help="Enviar resumen de novedades por Telegram")
|
||
ap.add_argument("--fecha", help="Fecha del snapshot (YYYY-MM-DD), por defecto hoy")
|
||
ap.add_argument("--descargar", metavar="ID|TITULO",
|
||
help="Anadir un titulo del catalogo a Download Station del NAS")
|
||
args = ap.parse_args()
|
||
|
||
if args.descargar:
|
||
items = fetch_catalogo()
|
||
it = buscar_item(items, args.descargar)
|
||
if not it:
|
||
print(f"[NAS] No se encontro '{args.descargar}' en el catalogo")
|
||
return 1
|
||
enlace = enlace_realdebrid(it["id"])
|
||
if not enlace:
|
||
return 1
|
||
print(f"[NAS] {it['clean']} -> {enlace[:80]}")
|
||
import nas_dl
|
||
ok = nas_dl.add_torrent(enlace)
|
||
return 0 if ok else 1
|
||
|
||
fecha = args.fecha or datetime.now().strftime("%Y-%m-%d")
|
||
items = fetch_catalogo()
|
||
# Snapshot de referencia: el mas reciente con fecha distinta de hoy
|
||
ref = None
|
||
snaps = sorted((BASE / "snapshots").glob("*.json"))
|
||
for s in reversed(snaps):
|
||
if s.stem != fecha:
|
||
with open(s, encoding="utf-8") as f:
|
||
ref = json.load(f)
|
||
break
|
||
guardar_snapshot(fecha, items)
|
||
novedades = diff_novedades(items, ref)
|
||
|
||
md = generar_markdown(fecha, items, novedades)
|
||
listado = BASE / "listados" / f"{fecha}.md"
|
||
listado.write_text(md, encoding="utf-8")
|
||
|
||
print(f"Videoclub {fecha}: {len(items)} disponibles, {len(novedades)} novedades")
|
||
print(f"Listado: {listado}")
|
||
|
||
if args.notify:
|
||
from tg_notify import enviar_novedades
|
||
enviar_novedades(fecha, items, novedades)
|
||
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|