04-ago: [videoclub] comando pelis (novedades castellano + comando listo + adultos efimeros 5min) + unidad NAS montada en sobremesa + lecciones
This commit is contained in:
@@ -106,5 +106,21 @@
|
||||
|
||||
37. **Cada cámara Tapo tiene credenciales RTSP propias**: No comparten usuario/pass necesariamente. El usuario RTSP puede ser el correo de la cuenta (`hermes211%40gmail.com`, con %40) o un usuario de cámara (jjminguez). Probarlas una a una con `ffmpeg`.
|
||||
|
||||
### Videoclub / NAS Synology (03-04 agosto)
|
||||
|
||||
38. **DSM 7 exige `_sid` como query parameter** en las APIs de Download Station (GET): `task.cgi?api=...&method=...&_sid=<sid>`. Enviar la cookie `sid=<x>` sola devuelve error **105** ("sin permiso") y FileStation da **403**. El login con `format=cookie` devuelve el sid correctamente, pero hay que pasarlo como `_sid` en la URL de cada llamada.
|
||||
|
||||
39. **El error 105 de Download Station es de sesión/permiso, no del usuario**: Con `_sid` en query funciona aunque el usuario `jjminguez` no sea admin. `list` de tareas devuelve 13 tareas (Lilo y Stitch, BLUELOCK, Heidi, etc.), todas del usuario `jjminguez`.
|
||||
|
||||
40. **Despliegue de archivos a la Pi sin git**: `scp` directo al túnel inverso falla (`scp: Connection closed`). Fiable: copiar al VPS (`scp` local→VPS funciona), luego base64 expandido en el VPS: `echo $(base64 -w0 /tmp/f.py) | base64 -d > /tmp/f.py` (los `$()` inline se expanden en el VPS antes de llegar a la Pi) y `sudo cp` al destino. Verificar con `md5sum`.
|
||||
|
||||
41. **Unidad NAS montada en sobremesa via túnel SSH encadenado**: La NAS (192.168.50.31) está en otra red que el sobremesa (192.168.1.239). Se montó `Z:` → `\\127.0.0.1\video` con: (a) la Pi reenvía SMB del NAS al VPS por autossh: `-R 1445:192.168.50.31:445`; (b) túnel local en el PC: `ssh -L 445:127.0.0.1:1445 root@185.187.169.109`; (c) `net use Z: \\127.0.0.1\video`. **Windows SMB usa SIEMPRE el puerto 445** — si el túnel escucha en otro puerto, `net use` da error 67 ("nombre de red no encontrado"). Script: `vigilancia/montar_nas.ps1` + acceso directo en Startup (la tarea programada exige admin).
|
||||
|
||||
42. **Reiniciar autossh de la Pi**: `systemctl restart autossh-tunel` puede fallar con `remote port forwarding failed for listen port 2222` porque queda un proceso autossh viejo con los puertos. Fix: `pkill -f autossh` + `pkill -f "ssh.*-R"` y luego restart. Al matar el túnel se corta la propia conexión SSH (hay que reconectar).
|
||||
|
||||
43. **Comando `pelis` en pi_bot**: Lista novedades con buena calidad en castellano del catálogo Torrentio/RealDebrid con el comando `descargar <url>` ya montado para copiar/pegar. Filtros: castellano = regex `Esp|Castellano|Span|dual|lat`; calidad mala excluida = `CAMRip|TS.|HDTS|Telesync|KinoRip|Screener|HDCAM`; año >= 2024. Se trocea en mensajes <=3500 chars (Telegram corta a ~4096). Adultos van en mensaje efímero que se borra a los 5 min (`threading.Timer(300, ...)` + `deleteMessage`) y NO se loguean. Módulo autónomo: `vigilancia/pelis.py` (lee `os.environ` primero, luego `.env`).
|
||||
|
||||
44. **El token RD y las credenciales NAS están en el `.env` de la Pi** (`/home/pi/minipc/vigilancia/.env`): `TORRENTIO_RD_TOKEN`, `NAS_HOST=192.168.50.31`, `NAS_PORT=5000`, `NAS_USER`, `NAS_PASS`. Hubo un bug previo donde `NAS_HTTPS=0TORRENTIO_RD_TOKEN=...` quedaron pegados en una línea — ojo al editar el `.env` por SSH.
|
||||
|
||||
|
||||
|
||||
|
||||
49
vigilancia/montar_nas.ps1
Normal file
49
vigilancia/montar_nas.ps1
Normal file
@@ -0,0 +1,49 @@
|
||||
# montar_nas.ps1 - Monta la unidad Z: (share \video de la NAS Synology 192.168.50.31)
|
||||
# via tunel SSH encadenado: PC -> VPS(185.187.169.109) -> Pi(tunel inverso 1445) -> NAS
|
||||
#
|
||||
# Requisitos:
|
||||
# - Clave SSH del VPS en C:\Users\juanm\Documents\GitHub\contabo
|
||||
# - Servicio autossh-tunel en la Pi con -R 1445:192.168.50.31:445
|
||||
# - Credencial SMB guardada con: cmdkey /add:127.0.0.1 /user:jjminguez /pass:"<pass>"
|
||||
#
|
||||
# Uso: powershell -File montar_nas.ps1
|
||||
|
||||
$ErrorActionPreference = "SilentlyContinue"
|
||||
|
||||
function Test-Tunel {
|
||||
$c = Get-NetTCPConnection -LocalPort 445 -State Listen -ErrorAction SilentlyContinue
|
||||
return ($null -ne $c)
|
||||
}
|
||||
|
||||
function Test-Unidad {
|
||||
$d = Get-PSDrive -Name Z -ErrorAction SilentlyContinue
|
||||
return ($null -ne $d)
|
||||
}
|
||||
|
||||
# 1) Levantar tunel local (445 -> VPS:1445 -> Pi -> NAS:445)
|
||||
if (-not (Test-Tunel)) {
|
||||
Write-Host "[NAS] Levantando tunel 445 -> VPS:1445..."
|
||||
Start-Process -FilePath "ssh" -ArgumentList `
|
||||
"-i", "C:\Users\juanm\Documents\GitHub\contabo", `
|
||||
"-N", "-L", "445:127.0.0.1:1445", `
|
||||
"root@185.187.169.109", `
|
||||
"-o", "ExitOnForwardFailure=yes", `
|
||||
"-o", "ServerAliveInterval=30", `
|
||||
"-o", "ServerAliveCountMax=3" `
|
||||
-WindowStyle Hidden | Out-Null
|
||||
Start-Sleep -Seconds 5
|
||||
}
|
||||
|
||||
# 2) Montar unidad Z: apuntando al share video (via tunel local)
|
||||
if (-not (Test-Unidad)) {
|
||||
Write-Host "[NAS] Montando Z: -> \\127.0.0.1\video ..."
|
||||
net use Z: \\127.0.0.1\video /persistent:no | Out-Null
|
||||
}
|
||||
|
||||
# 3) Verificar
|
||||
if (Test-Unidad) {
|
||||
$n = (Get-ChildItem "Z:\" -ErrorAction SilentlyContinue | Measure-Object).Count
|
||||
Write-Host "[NAS] Unidad Z: montada OK ($n elementos)"
|
||||
} else {
|
||||
Write-Host "[NAS] ERROR: no se pudo montar Z:"
|
||||
}
|
||||
216
vigilancia/pelis.py
Normal file
216
vigilancia/pelis.py
Normal file
@@ -0,0 +1,216 @@
|
||||
"""
|
||||
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"]
|
||||
|
||||
# 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)
|
||||
|
||||
|
||||
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, ano_min=2024):
|
||||
"""Novedades con buena calidad en castellano (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)
|
||||
out.sort(key=lambda x: (int(x["year"] or 0), x["clean"] or ""), reverse=True)
|
||||
return out
|
||||
|
||||
|
||||
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_efimero(texto, items, max_len=CHUNK_MAX):
|
||||
"""Trocea un texto con lineas 'titulo + descargar <url>' sin partir lineas."""
|
||||
lineas = texto.split("\n")
|
||||
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 generar(fecha=None, ano_min=2024):
|
||||
"""Devuelve (mensajes_normales, adultos_list). Uso desde pi_bot."""
|
||||
items = fetch_catalogo()
|
||||
normales, adultos = separar_adultos(items)
|
||||
sel = seleccionar(normales, ano_min=ano_min)
|
||||
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)")
|
||||
@@ -74,13 +74,28 @@ def estado():
|
||||
|
||||
def enviar(texto, chat_id):
|
||||
try:
|
||||
requests.post(
|
||||
r = requests.post(
|
||||
f"https://api.telegram.org/bot{TOKEN}/sendMessage",
|
||||
data={"chat_id": chat_id, "text": texto, "parse_mode": "HTML"},
|
||||
timeout=10,
|
||||
)
|
||||
d = r.json()
|
||||
if d.get("ok"):
|
||||
return d["result"].get("message_id")
|
||||
except Exception as e:
|
||||
print("Error enviando:", e)
|
||||
return None
|
||||
|
||||
|
||||
def borrar(chat_id, message_id):
|
||||
try:
|
||||
requests.post(
|
||||
f"https://api.telegram.org/bot{TOKEN}/deleteMessage",
|
||||
data={"chat_id": chat_id, "message_id": message_id},
|
||||
timeout=10,
|
||||
)
|
||||
except Exception as e:
|
||||
print("Error borrando:", e)
|
||||
|
||||
|
||||
def mandar_tv(mensaje, simple):
|
||||
@@ -163,6 +178,8 @@ def procesar(texto, chat_id):
|
||||
ok = nas_dl.add_torrent(uri)
|
||||
enviar("📥 Añadido a Download Station del NAS ✅" if ok
|
||||
else "❌ No se pudo añadir al NAS", chat_id)
|
||||
elif t in ("pelis", "/pelis"):
|
||||
procesar_pelis(chat_id)
|
||||
elif t in ("ayuda", "/ayuda", "help", "/help"):
|
||||
enviar("Comandos:\n"
|
||||
"<b>estado</b> — estado de la Pi\n"
|
||||
@@ -171,6 +188,7 @@ def procesar(texto, chat_id):
|
||||
"<b>alarma <mensaje></b> — secuencia completa (sirena x3)\n"
|
||||
"<b>youtube <URL></b> — reproduce YouTube y apaga al terminar\n"
|
||||
"<b>descargar <enlace></b> — añade a Download Station del NAS\n"
|
||||
"<b>pelis</b> — novedades en castellano con comando listo\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"
|
||||
@@ -194,6 +212,46 @@ def procesar(texto, chat_id):
|
||||
sh("echo 'raspberry' | sudo -S reboot")
|
||||
|
||||
|
||||
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
|
||||
un mensaje efimero que se borra a los 5 minutos y NO se loguea."""
|
||||
try:
|
||||
import pelis
|
||||
except Exception as e:
|
||||
log(f"pelis import error: {e}")
|
||||
enviar("❌ No se pudo cargar el modulo pelis", chat_id)
|
||||
return
|
||||
try:
|
||||
enviar("🎬 Consultando novedades del catálogo...", chat_id)
|
||||
mensajes, adultos = pelis.generar()
|
||||
except Exception as e:
|
||||
log(f"pelis generar error: {e}")
|
||||
enviar(f"❌ Error consultando el catálogo: {e}", chat_id)
|
||||
return
|
||||
|
||||
for m in mensajes:
|
||||
enviar(m, chat_id)
|
||||
|
||||
# Adultos: efimeros (5 min), sin loguear, nunca persisten
|
||||
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 monitor_temperatura():
|
||||
while True:
|
||||
time.sleep(300)
|
||||
|
||||
Reference in New Issue
Block a user