Un Tracker de Torrents Privado, Dockerizado y Curtido en Batalla
███████████████████████████████████████████████████████████████
█ █
█ 🛡️ UNIT3D BÚNKER | Nuclear Order Bit Syndicate █
█ █
█ "From the Scene, For the Scene" █
█ 2000+ hours of stabilization, automation, and resilience █
█ █
███████████████████████████████████████████████████████████████
⚠️ AVISO PARA NAVEGANTES: Si vas a tocar el stack, deja el café un segundo y lee. Entrar aquí sin pasar por la wiki es como intentar desarmar una bomba con palillos chinos. Bajo tu propio riesgo.
Todo lo que necesitas para que el tracker no explote está en nuestra Wiki oficial:
¿Qué encontrarás ahí dentro?
- 🛠️ Configuración del Entorno: Cómo domar los contenedores sin morir en el intento.
- 💾 Backups Blindados: El sistema de snapshots que nos salva el culo a las 06:00 AM.
- 📑 Guía de Desarrollo: Para que el código nuevo no parezca escrito por el becario.
- 🏗️ Testing: Cómo montar un laboratorio de pruebas que no pese 22GB.
- ¿Qué es UNIT3D?
- ¿Por qué NOBS? Lo que Construimos
- Mejoras Clave (22)
- Rutas de Instalación
- Gestión: El Makefile
- Arquitectura
- Mapeo de Puertos
- Notas de Seguridad
- Solución de Problemas
- Filosofía: De la Scene, Para la Scene
- Contribuciones · Licencia · Agradecimientos
📂 Las 22 Mejoras Clave, una por una
- Meilisearch: búsqueda instantánea y resiliente
- Lista negra de correos resiliente
- Transparencia de IPs (redes de Docker)
- Protección contra fuerza bruta equilibrada
- Infraestructura autónoma (el "Búnker")
- Branding de NOBS (tema personalizado)
- Ajustes de configuración
- Tema Retro v2 (estética y fixes)
- Integración con Telegram (bot)
- Sincronización cifrada a Google Drive
- Metadata multi-proveedor con consenso
- Meta-Worker (cola dedicada de metadatos)
- Swarm Intelligence y mapa 3D
- RetroArch Web (26 cores libretro)
- Aislamiento COOP COEP y proxy TMDB
- Thanks Ratio y locale español
- Endurecimiento del edge (nginx announce)
- UI reactiva (trailers, flash cards, backdrops)
- Base de datos profundamente modificada
- Super-paneles de staff (en FOSS)
- Arcade ScummVM WebAssembly
- Banco forense y de recuperación (PITR)
UNIT3D es un software moderno y rico en funciones para Trackers de Torrents Privados, construido sobre Laravel 12, Livewire y AlpineJS. Creado por el equipo de HDInnovations, impulsa comunidades de trackers privados de alto rendimiento con soporte para:
- 🔐 Gestión Avanzada de Usuarios: Roles, permisos, invitaciones, logros
- 🔍 Integración con Meilisearch: Búsqueda en milisegundos a través de millones de torrents
- 📊 Analíticas Completas: Estadísticas de torrents, actividad de usuarios, ratios de seedeo
- 🎨 Sistema de Temas: UI personalizable con Sass/CSS
- 📧 Notificaciones por Correo: Integración SMTP, alertas de actividad
- 🔗 Integración IRC: Anuncios en vivo e integración de bots
- 🌍 Internacionalización: Soporte para múltiples idiomas
Este proyecto no existiría sin UNIT3D. Los desarrolladores originales crearon una plataforma increíble para comunidades de trackers privados. → Visita el GitHub de UNIT3D
UNIT3D es una plataforma brillante, pero llega como código fuente, no como una implementación empaquetada. Tomamos la Edición Comunitaria e hicimos tres cosas: arreglar lo que venía roto, dockerizar el stack completo y endurecerlo para que opere solo (la filosofía "Búnker").
La Edición Comunitaria tenía bugs sin corregir y funciones faltantes:
| Problema | Impacto | Nuestra Solución |
|---|---|---|
| Instalador no incluido | La edición comunitaria no trae instalador; la instalación oficial automatizada se ofrece como servicio de pago de HDInnovations | Cubrimos la instalación por dos vías propias y gratuitas: (1) el entrypoint.sh de Docker hace el setup completo en cada arranque (migraciones, listas negras, caché, permisos), y (2) un instalador baremetal reescrito desde cero para Debian/Ubuntu en Unit3d_9.2-Installer-fixed/Unit3d-installer-debian.sh |
| Meilisearch sin Configurar | El motor de búsqueda se incluía pero no se indexaba ni sincronizaba | Implementamos indexación en arranque en frío, sincronización con observadores en tiempo real y protección con Llave Maestra |
| Fuerza Bruta Demasiado Agresiva | La configuración bloqueaba a usuarios legítimos (5 intentos = bloqueo de 24h) | Ajustamos FortifyServiceProvider (5→15 intentos, 24h→1h, creamos propietario de respaldo) |
| Fragilidad de la Lista Negra de Correos | El sistema se rompía si el CDN externo no era accesible | Creamos una caché local persistente (storage/app/email-blacklist.json) con un sistema de respaldo híbrido |
El UNIT3D original no es nativo de Docker. Construimos la contenedorización completa:
| Desafío | Solución |
|---|---|
| Servicios en Segundo Plano Faltantes | Añadimos contenedores scheduler y worker con entrypoints dedicados |
| Enmascaramiento de Direcciones IP | Configuramos cabeceras de proxy inverso en Nginx + TrustProxies de Laravel (IPs reales en perfiles) |
| Caos de Permisos en Contenedores | Autoreparación en entrypoint.sh (chmod 775, chown www-data en el arranque) |
| Enlace de Almacenamiento en Docker | Configuramos montajes de volúmenes persistentes con los enlaces simbólicos correctos |
| Sin Persistencia de Dependencias | Incluimos vendor/ y node_modules/ en el repositorio (recuperación Plug & Play sin conexión) |
Más allá de arreglar y dockerizar, añadimos características autónomas y orientadas al funcionamiento sin conexión:
| Característica | Beneficio |
|---|---|
| Estrategia de Backup en Frío | Detener contenedores → copiar → reiniciar (cero corrupción, integridad de datos garantizada) |
| Sincronización Cifrada a Google Drive | Snapshots cifrados con rclone + contenedor efímero (recuperación off-site) |
| Automatización de Health Check | Monitoriza Nginx, Meilisearch, MySQL, Redis, scheduler, worker y endpoint announce |
| Entrypoints de Autoreparación | Apagar/encender → todo funciona (sin intervención manual) |
| Cola Dedicada de Metadatos | Worker aislado meta-refresh para que TMDB/MAL/IGDB no ahoguen la cola principal |
| Tracker en Rust (UNIT3D-Announce) | /announce/ desacoplado de PHP, con sync en caliente desde Laravel |
| Notificaciones a Telegram | Bot vinculado por deep-link, kick automático en baneos, póster + mediainfo |
| Control con Makefile | make up, make backup, make health, make meilisearch (operaciones simples) |
Resultado: Un sistema listo para producción, autónomo y diseñado para comunidades que gestionan su propia infraestructura.
🔍 Búsqueda en milisegundos, autoindexada y resiliente.
El Desafío: UNIT3D incluye Meilisearch como su motor de búsqueda, pero no proporciona documentación ni configuración. La instalación y configuración quedan a cargo del operador.
Nuestra Solución:
🏗️ INFRAESTRUCTURA:
• Contenedor dedicado (getmeili/meilisearch:latest) en docker-compose.yml
• Almacenamiento persistente de índices (volumen de Docker meilisearch-data)
• Protección con Llave Maestra (MEILISEARCH_KEY en .env, nunca registrada en logs)
🔄 INICIALIZACIÓN:
• Indexación en arranque en frío: entrypoint.sh ejecuta php artisan scout:import
• Si faltan los índices, el sistema los reconstruye al arrancar (autoreparación)
• Configuración: app/Http/Scout config mapea Torrent → Meilisearch
⚡ SINCRONIZACIÓN EN TIEMPO REAL:
• Observadores de Laravel escuchan por torrents nuevos o actualizados
• Indexación instantánea (milisegundos) a medida que los usuarios suben
• Enriquecimiento de metadatos de TMDB/IGDB (pósters, géneros, valoraciones)
🛡️ RESILIENCIA:
• Los índices sobreviven a los reinicios de los contenedores (persisten en el volumen)
• Consulta de respaldo a MySQL si Meilisearch no está disponible
• El Health Check monitoriza el endpoint /health
Por qué es importante: Buscar en más de 50,000 torrents toma milisegundos en lugar de segundos. La base de datos se mantiene ligera. Los usuarios obtienen resultados instantáneos y filtrados.
📧 Registro a prueba de caídas del CDN de dominios desechables.
El Problema: UNIT3D obtiene dominios de correo desechables de un CDN externo durante la validación del registro. Si el CDN está caído o inaccesible, los registros fallan por completo.
Nuestra Solución - Estrategia de Lista Negra Híbrida:
PRIMARIO (Online):
✅ Obtiene una lista actualizada del CDN (andreis/disposable-emails)
✅ Actualiza una vez al arrancar (php artisan auto:email-blacklist-update)
RESPALDO (Offline):
✅ Almacena una copia local: storage/app/email-blacklist.json
✅ Más de 7,160 dominios persistidos localmente
✅ Si el CDN no es accesible, usa la caché local (el registro sigue funcionando)
PERSISTENCIA:
✅ La caché sobrevive a los reinicios de los contenedores
✅ La caché sobrevive a `docker compose down/up`
✅ La caché se incluye en los backups completos
Detalles de Implementación:
- Se creó
app/Helpers/EmailBlacklistUpdater.php(lógica de actualización automática) - El entrypoint ejecuta
php artisan auto:email-blacklist-updateal arrancar - Un comando de Artisan personalizado vigila el CDN y escribe en el JSON local
- El registro usa la caché local como principal (más rápido, fiable)
Resultado: El registro funciona incluso si el CDN está caído. El sistema es autónomo y capaz de funcionar sin conexión.
🌐 La IP real del usuario tras el proxy de Docker.
El Problema: En Docker, Nginx y la aplicación Laravel se ejecutan en contenedores separados. Sin las cabeceras adecuadas, todas las peticiones parecen provenir de la puerta de enlace interna de Docker (172.21.0.1). Todos los usuarios muestran la misma IP en sus perfiles.
Nuestra Solución - Cabeceras de Proxy Inverso + TrustProxies:
CAPA DE NGINX (.docker/nginx/default.conf):
• proxy_set_header X-Real-IP $remote_addr;
• proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
• proxy_set_header X-Forwarded-Proto $scheme;
CAPA DE LARAVEL (app/Http/Middleware/TrustProxies.php):
• protected $proxies = '*'; [Confiar en Nginx como proxy inverso]
• Lee la cabecera X-Real-IP y la usa como la IP de origen del usuario
RESULTADO:
✅ IPs reales de los usuarios capturadas en la base de datos
✅ Cada usuario ve su IP pública real en su perfil
✅ El baneo y las estadísticas basadas en IP funcionan correctamente
Verificación: Inicia sesión, visita tu perfil → verás tu IP pública real, no la puerta de enlace de Docker.
🔒 Anti fuerza bruta sin bloquear a los legítimos.
El Problema: La configuración por defecto de Fortify en UNIT3D era demasiado agresiva:
- 5 inicios de sesión fallidos → bloqueado por 24 horas
- IP de puerta de enlace compartida única (172.21.0.1 en Docker) → usuarios legítimos bloqueados juntos
- Resultado: Los desarrolladores se bloqueaban a sí mismos durante las pruebas/recuperación
Nuestro Ajuste (app/Providers/FortifyServiceProvider.php):
// Antes (demasiado estricto):
RateLimiter::for('login', 5 intentos por minuto); // 5 fallos = bloqueo
$throttleKey = hashless unique attempt;
// Después (equilibrado):
RateLimiter::for('login', 15 intentos por minuto); // 15 fallos = bloqueo
RateLimiter::for('two-factor', 6 intentos por minuto); // 2FA más indulgente
Duración del bloqueo: 24h → 1h // Recuperación más rápida
Verificación de multi-cuenta: umbral de 1 → 3 // Permite cambiar de cuentaSeguridad Adicional:
- Se creó la cuenta
BackupOwnercon permisos completos (acceso de emergencia) - Se puede usar la cuenta de respaldo si la principal está bloqueada
- Los logs rastrean los intentos fallidos para investigar ataques reales
Resultado: El sistema protege contra la fuerza bruta mientras permite la recuperación y pruebas legítimas.
🛡️ Autoreparación, backups en frío y health checks: el "Búnker".
Cada arranque de contenedor desencadena una recuperación automática:
# .docker/entrypoint.sh hace lo siguiente:
✅ Copia .env.example → .env (si .env no existe)
✅ composer install (si vendor/ no existe)
✅ npm install + build (si public/build/ no existe)
✅ Crea carpetas de almacenamiento
✅ Arregla permisos (chmod 775, chown www-data)
✅ Espera a MySQL
✅ Genera APP_KEY (si no existe)
✅ Ejecuta migraciones (--force)
✅ Actualiza la lista negra de correos
✅ Inicia PHP-FPMResultado: Apaga el servidor, enciéndelo → todo funciona. Sin intervención manual.
Filosofía: Los backups deben ser a prueba de corrupción, completos y fáciles de restaurar.
Flujo de trabajo de ./backup.sh:
1. 💾 Volcado de MySQL (volcado en caliente, --no-tablespaces para MySQL 8)
└─ Captura el estado de la base de datos sin problemas de bloqueo
2. 🛑 Congelación de Contenedores (docker compose stop)
└─ Detiene todos los contenedores para un snapshot de archivos consistente
3. 📂 Archivo Completo (tar -czf)
└─ Comprime: código de la aplicación, vendor/, node_modules/, configuraciones, datos
└─ Incluye el árbol del proyecto y la receta exacta del despliegue
4. 🧳 Espejo Externo Opcional
└─ Copia el snapshot a `BACKUP_EXTERNAL_DIR` si `BACKUP_EXTERNAL_ENABLED=true`
└─ Mantiene su propia rotación para tener una segunda copia local fuera del árbol principal
5. ♻️ Rotación (local + externa)
└─ `BACKUP_LOCAL_RETENTION` controla la rotación local
└─ `BACKUP_EXTERNAL_RETENTION` controla la rotación de la copia en disco externo
6. 🚀 Resurrección (docker compose up)
└─ Verifica que el backup se haya realizado con éxito
└─ Reinicia el sistema inmediatamente (minimiza el tiempo de inactividad)¿Por qué "quirúrgico"?:
- ✅ Sin corrupción: Detener los contenedores asegura la consistencia de los archivos durante la copia
- ✅ Plug & Play: Se incluyen
vendor/ynode_modules/completos - ✅ Copia secundaria integrada: El snapshot puede reflejarse automáticamente a un disco externo con rotación propia
- ✅ Atómico: Snapshot completo en un único punto en el tiempo
make health # Ejecuta ./health_check.sh
Chequeos:
✅ La URL principal responde con HTTP 200/302
✅ Endpoint /health de Meilisearch
✅ Conectividad con MySQL
✅ Conectividad con Redis
✅ El worker de la cola está vivo
✅ El scheduler se está ejecutando
✅ Endpoint announce opcional si `ANNOUNCE_HEALTHCHECK_URL` está configurado
Si alguno falla: Alertas + puede reiniciar automáticamenteLa ruta /announce/ ya no depende del controlador PHP clásico. Ahora se sirve mediante un servicio dedicado announce en Docker que ejecuta UNIT3D-Announce en Rust.
Arquitectura actual:
Internet/Cloudflare
↓
Nginx (`web`)
↓ proxy /announce/
Rust tracker (`announce:6969`)
↓ API interna
Laravel (`app`) → Unit3dAnnounce::*Puntos importantes:
- ✅ Código vendorizado en el repo:
rust-announce/UNIT3D-Announce - ✅ Sin submódulos, sin symlinks, sin hardlinks en el árbol vendorizado de producción
- ✅ IP real del cliente reenviada al tracker por
CF-Connecting-IP/X-Real-IP - ✅ Admin API del tracker bloqueada públicamente en nginx (
/announce/{TRACKER_KEY}/...) - ✅ Healthcheck dedicado en
/announce/health/ping - ✅ Rollback simple: cambiar
TRACKER_ENABLED=falseen.envy reiniciarapp/scheduler/worker/web
Variables relevantes:
TRACKER_ENABLEDTRACKER_HOST=announceTRACKER_PORT=6969TRACKER_KEYANNOUNCE_HEALTHCHECK_URL
Implicaciones operativas:
backup.shya captura el código del tracker porque forma parte del árbol del proyectodisaster-recovery-script.shya reconstruye el servicio al hacerdocker compose up -d- el dashboard sigue pudiendo leer estadísticas del tracker externo mediante
Unit3dAnnounce::getStats()
🎨 Identidad visual NOBS, sin tocar el núcleo.
UNIT3D viene con un tema por defecto. Creamos una identidad personalizada de N.O.B.S:
-
Tema SCSS Personalizado:
resources/sass/themes/_refined-nobs.scss- Estética neón cian/rosa
- Paneles de glassmorphism con efectos de desenfoque
- Tipografía industrial y de bloques
-
Personalización de Activos:
- Favicon: Icono de medalla de NOBS personalizado de 64x64
- Logo: Marca de NOBS en las páginas de inicio de sesión/registro
- Imagen OG: Imagen para compartir en redes sociales
- Páginas de Autenticación: Fondos y estilos personalizados
-
Extensibilidad Fácil:
- Todos los estilos en Sass (variables temáticas)
- Compilado con Vite (
npm run build) - Cambia de tema a través del panel de administración o
config/other.php
Esto no es un cambio en el núcleo de UNIT3D — es una piel personalizada que respeta la plataforma original.
⚙️ Ajustes finos de invitaciones, ratios y seguridad.
Optimizaciones en config/other.php:
- Tiempo de espera para invitaciones: 24h → 1h (después de la activación de 2FA)
- Máximo de invitaciones sin usar por usuario: 1 → 10 (amigable para el staff)
- Subtítulo del sitio: Contextualizado para N.O.B.S
- Correo de respaldo: Valor por defecto seguro si falta en .env
Endurecimiento de la seguridad:
SESSION_SECURE_COOKIE=true(solo HTTPS)SESSION_DOMAIN=nobs.rawsmoke.net(dominio explícito)TRUSTED_PROXIES=*(para cadenas de proxy inverso)
🎨 Tema Retro v2: dark mode neón y fixes de UX.
El Desafío: El tema original refined_nobs presentaba problemas críticos de UX/UI: menús desplegables inaccesibles por conflictos de capas (z-index), botones que aparecían como bloques negros sin relieve, y una saturación de color rosa que dificultaba la lectura prolongada. Además, los ratios de torrents mostraban errores técnicos (INF o código HTML fugado).
Nuestra Solución (v2):
🚀 UI/UX REIMAGINADA:
• Base Dark Mode Profundo (#050507) con acentos neón (Púrpura a Fucsia) y Cyan.
• Corrección de Dropdowns: Ajuste de z-index y efectos hover para navegación fluida.
• Botones Profesionales: Bordes suavizados (6px), gradientes sutiles e iconos blancos.
• Legibilidad: Zebra-striping en todas las tablas y paneles de datos centrados.
🛠️ FIXES TÉCNICOS:
• Ratio "INF" corregido: Sustituido por el símbolo de infinito (∞) más elegante.
• HTML Leak: Eliminación del código literal en las tablas de historia de torrents.
• Optimización Blade: Uso de directivas @class para un renderizado robusto y limpio.
Resultado: Una interfaz moderna, elegante y funcional que respeta la estética Cyberpunk/Synthwave de N.O.B.S sin sacrificar la usabilidad ni la claridad de los datos.
📡 Notificaciones de torrents en Telegram con póster y mediainfo.
El Desafío: UNIT3D no incluye ningún canal de notificaciones externo en tiempo real. Los usuarios no reciben alertas cuando se aprueba un torrent, y el staff no puede actuar sobre los miembros del grupo de Telegram directamente desde la plataforma.
Nuestra Solución:
🤝 VINCULACIÓN DE CUENTA (Deep-Link Handshake):
• Cada usuario recibe un token único con formato TRK-XXXXXXXX al registrarse
• El usuario inicia /start TRK-xxx con el bot → se vincula su chat_id a su cuenta
• La transacción usa lockForUpdate() para evitar condiciones de carrera
• Ruta: POST /api/telegram/webhook (excluye throttle:api, auth:api, banned)
📢 NOTIFICACIONES DE TORRENTS:
• TorrentObserver dispara SendTelegramNotification cuando status → APPROVED
• Job con colas ($tries=3, $backoff=[10,60,300], $timeout=30)
• Mensaje enriquecido: póster, mediainfo completo (codec, resolución, audio,
bitrate, framerate, ratio de aspecto, duración), banderas de idioma (40+ idiomas)
• Botones inline: IMDb / TMDb / Tráiler / Descargar
🚫 BAN → EXPULSIÓN AUTOMÁTICA:
• Al banear un usuario en UNIT3D, BanController llama a TelegramService::kickUser()
• Implementación limpia: banChatMember + unbanChatMember inmediato (expulsión, no ban permanente)
• Se limpian telegram_chat_id y telegram_token del usuario baneado
🔗 INVITACIÓN AL GRUPO:
• Los usuarios vinculados reciben el enlace de invitación al grupo vía bot
• El enlace se preserva íntegro usando Http::asJson() (mantiene el + en las URLs)
🛠️ COMANDOS DEL BOT:
• /start TRK-xxx — vinculación de cuenta
• /status — muestra si la cuenta está vinculada
• /help — ayuda del bot
Variables de Entorno Requeridas:
| Variable | Descripción |
|---|---|
TELEGRAM_BOT_TOKEN |
Token del bot (obtenido de @BotFather) |
TELEGRAM_GROUP_ID |
ID del supergrupo (número negativo, ej: -1001234567890) |
TELEGRAM_TOPIC_NOVEDADES |
ID del hilo/topic para anuncios de torrents |
TELEGRAM_BOT_USERNAME |
@username del bot sin la @ |
TELEGRAM_GROUP_INVITE_LINK |
Enlace de invitación al grupo (t.me/+xxxxx) |
Resultado: Los usuarios reciben notificaciones instantáneas de nuevos torrents directamente en Telegram, con póster e información técnica completa. Ver docs/TELEGRAM_INTEGRATION_GUIDE.md para la guía completa de configuración.
☁️ Snapshots cifrados a Google Drive con rclone.
El Desafío: Los snapshots locales del backup en frío quedan en el mismo servidor. Un fallo de disco o pérdida del host implica pérdida total de los backups.
Nuestra Solución - Sincronización Cifrada con Contenedor Efímero:
🐳 PATRÓN EFÍMERO:
• Contenedor rclone/rclone:latest que se crea, sincroniza y destruye (--rm)
• Sin estado persistente: el contenedor no queda corriendo en segundo plano
• Orchestrado desde rclone_gdrive/docker-compose.yml
🔐 CIFRADO TRANSPARENTE:
• Remote gdrive_crypt: cifra los archivos antes de subirlos a Google Drive
• La clave de cifrado reside en rclone_gdrive/config/rclone.conf (git-ignored)
• Los archivos en Drive son ilegibles sin la clave — privacidad garantizada
⚙️ PARÁMETROS DE SINCRONIZACIÓN:
• --drive-chunk-size 1024M (evita timeouts en archivos grandes)
• --transfers 4 / --checkers 8 (paralelismo controlado)
• --delete-after (borra en destino solo si la subida fue exitosa)
♻️ RESTAURACIÓN:
• rclone_gdrive/scripts/restore_snapshot.sh — interactivo
• Lista los backups disponibles en la nube, solicita el nombre del objetivo
• Descarga y desencripta automáticamente a restauracion_emergencia/
📋 LOGS:
• rclone_gdrive/logs/sync_execution.log (salida detallada de rclone)
• rclone_gdrive/logs/cron_wrapper.log (registro de ejecuciones de cron)
Uso:
# Sincronización manual
bash rclone_gdrive/scripts/run_sync.sh
# Restaurar un snapshot desde la nube
bash rclone_gdrive/scripts/restore_snapshot.sh
# Automatizar con cron (ejemplo: diario a las 07:00)
0 7 * * * /home/rawserver/UNIT3D_Docker/rclone_gdrive/scripts/run_sync.shResultado: Los snapshots locales se sincronizan cifrados a Google Drive. La recuperación ante desastres funciona incluso si el servidor físico desaparece por completo.
🧬 Metadatos por consenso de seis proveedores.
El Desafío: La Edición Comunitaria solo habla con TMDB. Si el match es ambiguo (título genérico, año confuso, animes con varias versiones), el póster, sinopsis y géneros terminan equivocados — y el operador acaba editando torrents a mano.
Nuestra Solución — ConsensusResolver:
🔍 6 PROVEEDORES EN PARALELO:
• TmdbClient — películas + series (autoritativo para media occidental)
• ImdbClient — fallback de fichas y ratings cuando TMDB falla
• TvmazeClient — calendario y datos episódicos de TV
• MalClient — anime (MyAnimeList API + scraper como respaldo)
• AnilistClient — anime/manga GraphQL (votos cruzados con MAL)
• IgdbClient — videojuegos (categoría aparte, no se mezcla con cine)
🗳️ ALGORITMO DE CONSENSO:
• Cada proveedor devuelve un score normalizado (título + año + tipo)
• Hits con score por debajo del umbral NO votan (evita ruido)
• Voto mayoritario decide canonical_id y artwork
• Empates se rompen con prioridad por categoría (anime→MAL, cine→TMDB, juegos→IGDB)
🖼️ ARTWORK ROTATIVO:
• Cada torrent guarda N pósters en `metadata_artwork`
• `meta:rotate-covers` rota el póster activo para que el catálogo respire visualmente
• Tabla `metadata_resolutions` audita qué proveedor ganó y por qué
Componentes:
app/Services/Metadata/ConsensusResolver.php— orquestador del votoapp/Services/Metadata/*Client.php— un cliente por proveedor- Tablas:
metadata_resolutions,metadata_artwork,mal_anime - Comandos:
meta:sync,meta:refresh-dispatch,meta:rotate-covers,fetch:meta,sync:missing-trailers
Resultado: Match de metadatos mucho más robusto, especialmente en anime (donde TMDB es históricamente débil) y en títulos con colisiones de nombre.
🛰️ Cola aparte para que el refresco de metadatos no ahogue la principal.
El Desafío: Refrescar metadatos puede colgar la cola principal: rate-limits de TMDB, scrapes lentos de MAL, timeouts de IMDb. Si comparten queue, las notificaciones de Telegram se atascan detrás de un scrape de 30 segundos.
Nuestra Solución — Worker Aislado:
meta-worker:
entrypoint: /usr/local/bin/entrypoint-worker.sh
environment:
QUEUE_WORK_QUEUES: meta-refresh
QUEUE_WORK_TIMEOUT: 300🚦 SEPARACIÓN DE COLAS:
• Cola `default` → worker normal (Telegram, mails, jobs ligeros)
• Cola `meta-refresh` → meta-worker (TMDB/IGDB/MAL/Anilist/IMDb)
• Timeout extendido (300s) para tolerar APIs lentas
• Sin contención: un scrape colgado no afecta a notificaciones
⏰ DISPATCH AUTOMÁTICO:
• `php artisan meta:refresh-dispatch --limit=5 --stale-hours=720 --dispatch-ttl-minutes=10`
• Corre cada minuto desde el scheduler
• TTL de despacho evita re-encolar trabajos en vuelo
• Refresca metadatos de torrents con más de 30 días sin actualizar
Resultado: La UI sigue rápida mientras el catálogo se reindexa en segundo plano. La cola crítica nunca se ahoga.
🕸️ Inteligencia del swarm y mapa 3D del tracker.
El Desafío: El operador no tiene visibilidad de la topología real del swarm. ¿Quién comparte con quién? ¿Hay cliques? ¿Hay un nodo central que si se cae fragmenta el swarm?
Nuestra Solución — Dos Vistas Inéditas:
📊 SWARM INTEL (página de torrent):
• Componente Livewire plegable incrustado en cada ficha (`torrent/show.blade.php`)
• Se carga por torrent vía <livewire:swarm-intelligence :torrentId="$torrent->id" />
• Distribución geográfica de peers (banderas, ASN, ISP)
• Histograma de clientes BitTorrent
• Detección de patrones sospechosos (mismo ASN, ventanas idénticas)
• Panel foldable: el staff/usuario lo abre solo cuando necesita inteligencia, sin ensuciar la página principal
🌐 MAPA 3D INTERACTIVO (sección Community):
• Grafo de fuerza 3D renderizado en WebGL (three.js + 3d-force-graph)
• Nodos = usuarios, aristas = co-seedeo en torrents comunes
• Filtros por categoría, rol, actividad
• Visualización en tiempo casi real (refresco periódico)
Stack técnico:
app/Http/Controllers/SwarmGraphController.php— API del graforesources/views/livewire/swarm-intelligence.blade.php— vista por torrentresources/views/swarm/*— visor 3D- Vendor JS bajo
public/vendor/(force-graph, 3d-force-graph, three, d3) — gitignored install-swarm-assets.sh— descarga las libs en clonado fresco / rebuild
Resultado: Primer fork público de UNIT3D con visualización 3D del swarm. Útil para detectar abuso, medir salud comunitaria y, francamente, queda increíble.
🎮 RetroArch en el navegador: 26 cores libretro.
El Desafío: ScummVM cubre point-and-click clásico, pero el catálogo retro de verdad vive en NES, SNES, Mega Drive, Game Boy, PS1, arcade Capcom/Neo Geo. Hacía falta un emulador genérico en el navegador.
Nuestra Solución — RetroArch Compilado a WebAssembly:
🕹️ 26 CORES LIBRETRO EN public/retroarch/:
• fceumm (NES), snes9x (SNES), genesis_plus_gx (Mega Drive/SMS/GG)
• gambatte (Game Boy/GBC), mgba (GBA), mednafen_psx (PS1)
• fbneo (arcade), pcsx_rearmed, ecwolf (Wolfenstein 3D), …
• Lista completa en core_list.js
🔐 AUTH-WALL:
• /retroarch/* protegido tras sesión de Laravel (no scraping anónimo)
• Middleware `gaming.isolation` aplica COOP/COEP en la página show
• Páginas index/show separadas: catálogo abierto a miembros, reproductor aislado
📦 BIBLIOTECA Y CUBIERTAS:
• Cubiertas vía `retroarch:fetch-covers`
• Escaneo de ROMs con `retroarch:scan-roms`
• ROMs gitignoreadas — solo skeleton + README por sistema en git
• Modo `?debug` para diagnosticar arranques fallidos de core
Componentes:
app/Http/Controllers/RetroArchController.phppublic/retroarch/(gitignored salvo skeleton + core_list.js)- Comandos:
retroarch:fetch-covers,retroarch:scan-roms
Resultado: NES, SNES, Mega Drive, PS1 y más, jugables directamente en el navegador del miembro. Cero instalaciones, sesión Laravel como única puerta.
🛡️ WASM aislado y pósters TMDB conviviendo bajo CSP.
El Desafío: Para que ciertos cores libretro y módulos WASM funcionen, hace falta Cross-Origin-Opener-Policy: same-origin + Cross-Origin-Embedder-Policy: require-corp. Pero entonces el navegador bloquea pósters de image.tmdb.org por faltar Cross-Origin-Resource-Policy. Conflicto.
Nuestra Solución — Middleware Selectivo + Proxy Local:
🧱 MIDDLEWARE gaming.isolation:
• Aplicado SOLO a /gaming/{id} y /retroarch/{system}/show
• Inyecta COOP/COEP únicamente donde hace falta WASM aislado
• El resto del sitio sigue sin restricciones (rendimiento intacto)
🖼️ TMDB IMAGE PROXY:
• Ruta /tmdb-proxy/{size}/{file} → TmdbImageProxyController
• Sirve imágenes TMDB desde el mismo origen (cumple CSP img-src 'self')
• Cache HTTP largo en la capa nginx
• Transparente para las vistas: helpers reescriben las URLs
🔐 CSP LIMPIO:
• Sin nonces dinámicos por petición (KISS)
• Inline JS minimizado, vendor bajo /vendor/
• Cabeceras consolidadas en nginx + capa Laravel
Resultado: WASM aislado funcionando y pósters mostrándose, todo en el mismo dominio, sin abrir agujeros en la CSP global.
🤝 Thanks Ratio y español por defecto.
Thanks Ratio:
- Métrica de "agradecimientos recibidos / agradecimientos enviados" expuesta en perfil, top-nav, y en formularios de invitaciones/BON.
- Incentiva participación más allá del ratio de seedeo crudo.
- Integrada en
User,InviteController,TransactionController.
Localización:
- Locale por defecto migrado de
en→es(change_locale_default_to_es_in_user_settings). - Traducción completa de audit logs, downloads, reports, subtitles, tools, configuración de usuario y secciones de perfil.
- Componentes Blade nuevos para botones de comando del staff y condicionales de perfil.
show_posteractivo por defecto (mejor primera impresión en catálogo nuevo).
Perfil Autoexplicativo — "Condiciones que te aplican":
- Parcial nueva en
resources/views/user/profile/partials/my-conditions.blade.php, integrada enuser/profile/show.blade.php. - Expone al usuario, sin ambigüedades, qué reglas le afectan realmente:
- Freeleech (por sitio, por grupo o ambos)
- Double upload
- Ratio mínimo efectivo (global o override de grupo)
- Hit & Run: seed mínimo, gracia, avisos máximos, expiración
- Slots de descarga
- Resultado: menos tickets absurdos de "¿por qué a mí sí/no me cuenta esto?" y menos staff haciendo de calculadora humana.
🔒 Edge endurecido: nginx announce y verificación.
Nginx Announce Hardening (.docker/nginx/default.conf):
- Límites de conexión por IP en
/announce/ - Timeouts ajustados (lectura/escritura cortos, evita conexiones zombies)
- Admin API del tracker Rust (
/announce/{TRACKER_KEY}/...) bloqueada al exterior - Healthcheck público solo en
/announce/health/ping
Flujo de Verificación Endurecido:
- Rate-limit propio en
/email/verify-link/{id}/{hash}(GET y POST) - Tokens single-use, expiración corta
- Logs explícitos de intentos fallidos
- Registro resiliente: backoff en
RegisterController, validación reforzada contra dominios desechables (lista local),ApplicationControllery flujo de aplicación con throttling separado para que los bots no asfixien el formulario público
Comandos de Sincronización con Tracker Rust:
tracker:sync-users— empuja cambios de usuarios al tracker en calientetracker:sync-torrents— re-sincroniza catálogotracker:sync-groups— actualiza permisos por grupo- Útiles tras restore de backup o cambios masivos de permisos
🎞️ Trailers flotantes, flash cards y backdrops de freeleech.
El Desafío: La lista de torrents de UNIT3D es texto plano: título, tamaño, ratio. El usuario tiene que abrir cada ficha para ver de qué va el contenido. Los pósters quedan escondidos. La portada de freeleech es un banner liso.
Nuestra Solución — Metadata Pulverizada por Toda la UI:
🎬 TRAILER FLOTANTE EN HOVER (sobre el nombre del torrent):
• Componente Alpine.js en components/torrent/row.blade.php
• Detecta clave de YouTube vía $meta->trailer o regex sobre description
• Embed youtube-nocookie con autoplay+mute+loop+modestbranding
• Fallback automático al póster si el video reporta onError (150/101)
• Delays calculados (400ms enter / 150ms leave) — no se dispara con paseos rápidos
🪪 FLASH CARD CON MEDIAINFO + TRAILER EMBEBIDO (botón quick-view):
• Botoncito junto al torrent abre una tarjeta lado a lado:
Trailer YouTube (autoplay) │ MediaInfo resumido (codec, res, audio, bitrate)
• Layout grid 1fr 1fr si hay ambos, 1fr si solo hay uno
• Thumbnail HD del trailer con fallback a hqdefault
• Cero peticiones extra: la metadata viaja con la fila del torrent
🃏 FLASH CARD DE TMDB EN MINIATURAS:
• Hover sobre el póster mini → tarjeta con sinopsis, género, rating, año
• Tirando del ConsensusResolver: el mismo voto que decide el poster decide la card
• Sin scrapeo extra en cliente — todo desde el caché local
🌌 BACKDROPS DINÁMICOS EN BANNER DE FREELEECH (resources/views/partials/alerts.blade.php):
• AlertsComposer (app/View/Composers/AlertsComposer.php) selecciona 10 backdrops
• Top torrents por seeders + completados (cache::flexible 900/1800)
• Imágenes servidas vía /authenticated-images/tmdb-proxy/{size}/{file}
• Cumple CSP img-src 'self' incluso con COOP/COEP global activa
• Las URLs de image.tmdb.org se reescriben on-the-fly antes de renderizar
🔖 BOOKMARKS REFINADOS:
• Botón muestra contador en vivo de cuántos usuarios lo han marcado
• Estado filled/outlined según si es propio
• Tooltip "Bookmarked by N users" (señal social útil sin filtrar identidades)
Resultado: El catálogo deja de ser una tabla y pasa a ser un escaparate. El usuario navega con preview cinematográfico antes de decidir qué descargar. Todo apoyado en los 6 proveedores de metadatos — TMDB, IGDB, MAL, Anilist, IMDb, TVmaze cagando bits por todo el tracker.
🗄️ Base de datos profundamente extendida (ya no es Community).
El Estado: Tras 2000+ horas, la DB de este fork ya no es reconocible para alguien que clone UNIT3D Community Edition y mire database/migrations/.
Total de migraciones: 376 archivos en database/migrations/. De ellas, 10 son aportes propios de N.O.B.S construidos sobre las migraciones originales:
| Migración | Propósito |
|---|---|
2026_03_08_000000_create_settings_table.php |
Tabla de settings dinámicos del tracker (sustituye constantes hardcodeadas) |
2026_03_24_010501_add_telegram_fields_to_users_table.php |
telegram_chat_id, telegram_token, telegram_username por usuario |
2026_03_27_000001_create_disposable_email_domains_table.php |
Lista negra de dominios desechables persistida en DB (no en JSON volátil) |
2026_04_27_000001_create_game_saves_table.php |
Partidas guardadas por usuario para el arcade ScummVM |
2026_05_10_000600_add_telegram_group_joined_at_to_users_table.php |
Auditoría de cuándo un usuario entró al grupo de Telegram |
2026_05_11_000000_change_locale_default_to_es_in_user_settings.php |
Locale por defecto migrado a es |
2026_05_12_000000_default_show_poster_to_true_in_user_settings.php |
show_poster activo por defecto en nuevas cuentas |
2026_05_17_000001_create_mal_anime_table.php |
Caché local de anime de MyAnimeList (evita rate-limit de scrapeo) |
2026_05_22_000001_create_metadata_resolutions_table.php |
Auditoría de votos del ConsensusResolver por torrent |
2026_05_22_000002_create_metadata_artwork_table.php |
Almacén multi-poster por torrent (artwork rotativo) |
Implicaciones operativas:
⚠️ No intentes migrar este fork contra una DB de UNIT3D Community vanilla — las tablas extra son obligatorias para que arranquen Telegram, arcade y resolver multi-proveedor.⚠️ migrate:freshestá prohibido en producción — borra todo el catálogo, todos los logs, todas las partidas guardadas, toda la auditoría de metadatos.- ✅ Para restaurar usa Ruta B (Restaurar desde Backup) — los backups capturan el SQL completo, incluyendo todas las migraciones aplicadas.
- ✅ Los seeders no rellenan tablas N.O.B.S — la lista de dominios desechables se siembra desde
EmailBlacklistUpdater::sync(), la metadata se rellena víameta:syncymeta:refresh-dispatch.
Resultado: La DB es ahora una extensión coherente de UNIT3D, no un parche pegado encima. Cualquier feature N.O.B.S tiene su tabla, su migración y su lugar en el backup.
🎛️ Centro de operaciones de staff, en FOSS.
Contexto: UNIT3D Community Edition viene con un Staff Dashboard funcional pero básico. Los paneles avanzados de administración — los que de verdad usa el operador a diario — no formaban parte de la edición pública. Ningún fork público de UNIT3D los había construido. Lo único que vimos fue una foto borrosa de uno. Una semana de diseño en papel después, empezamos. Lo que sigue lleva 2000+ horas de iteración encima — y algún nuke por accidente del tracker que duele recordar.
| Métrica | Community Edition | NOBS Fork | Delta |
|---|---|---|---|
Métodos en CommandController |
9 | 34 | +25 acciones |
Carpeta app/Http/Livewire/Staff/ |
no existe | presente (ConfigManager) |
+∞ |
Paneles temáticos en /staff/commands |
1 lista plana | 9 paneles con iconos | +8 |
| Rutas Staff totales | 257 | 282 | +25 |
| Panel de configuración global del sitio | inexistente (editas .php a pelo) |
UI Livewire con 6 grupos, 25 ajustes hot-swap | nuevo |
Métodos en UserController (Staff) |
4 | 5 (telegramInfo) |
+1 |
La versión Community es una sola lista vertical de 8-9 botones (clear cache, maintenance, test email). La nuestra es un centro de operaciones segmentado en 9 paneles temáticos con iconos:
🛡️ Mantenimiento y Control del Sitio (fa-shield-alt)
• Activar/desactivar modo mantenimiento
• Toggle invite-only
• Crear storage:link
⚡ Caché y Rendimiento (fa-bolt)
• Clear cache / view / route / config
• Optimize:clear (Laravel + OPcache)
• Set all cache (precaching agresivo)
• Flush queue
☢️ Operaciones de Datos Críticas (fa-radiation, estilo danger)
• Acciones destructivas confinadas a UN panel rojo
• Confirmación explícita antes de cada disparo
• Logs persistentes de quién apretó qué y cuándo
🎬 TMDB (fa-film)
• Sync de trailers faltantes (normal y --force)
• Rotate covers (artwork rotativo)
📡 Rust Tracker — Sincronización (fa-broadcast-tower)
• Sync de users / torrents / groups con UNIT3D-Announce
• Útil tras restore o cambio masivo de permisos
🌱 Gestión de Peers y Torrents (fa-seedling)
• Flush old peers / reset user flushes
• Sync peers / sync torrents
• Limpieza quirúrgica del state del tracker
👥 Usuarios y Limpieza (fa-users)
• Ban masivo de cuentas con email desechable
• Desactivar warnings caducados
• Generar tokens de Telegram en lote
• Clean failed login attempts
🧪 Pruebas y Utilidades (fa-flask)
• Test email (con resultado en pantalla, no en logs)
• Set Telegram webhook
• Fix Meilisearch + reindex scout
• Meilisearch full repair (nuke + rebuild)
• Update email blacklist desde CDN
🔎 Metadata — Identificación (fa-fingerprint)
• meta:sync y meta:sync --force
• Rotar póster activo
• Re-resolver torrents huérfanos
Componente reutilizable: Staff/command/_btn.blade.php — botón con confirmación inline, spinner Livewire y feedback visual. Cada panel lo usa para mantener la UI coherente.
UNIT3D Community no tiene panel de configuración global. Si quieres cambiar other.ratio, other.freeleech, hitrun.seedtime o cualquier ajuste profundo del tracker, tienes que editar .php, recargar caché, rezar.
Nosotros lo tenemos en producción en una ruta real:
https://nobs.rawsmoke.net/dashboard/config- Ahí el staff puede abrir/cerrar registros, tocar flags de sitio, revisar el estado actual y cambiar la configuración PHP efectiva sin salir del navegador.
Construimos uno desde cero:
app/Http/Controllers/Staff/ConfigController.php— endpointapp/Http/Livewire/Staff/ConfigManager.php— componente Livewire (141 líneas, persistencia en tablasettings)resources/views/livewire/staff/config-manager.blade.php— UI (175 líneas)resources/views/Staff/config/index.blade.php— page shell
6 grupos temáticos, 25 ajustes hot-swap:
| Grupo | Icono | Ajustes |
|---|---|---|
| Sitio | 🌐 | Solo por invitación, tema por defecto, etiqueta Telegram |
| Freeleech & Double Upload | 🎁 | Freeleech global, hasta-cuándo, double upload, ratio reembolsable |
| Ratio & Descargas | ⚖️ | Ratio mínimo, upload/download inicial, página de verificación, magnet links |
| Invitaciones | ✉️ | Expiración (días), máx. invitaciones sin usar por usuario |
| Hit & Run | H&R activo, tiempo mínimo de seed (horas), máx. advertencias | |
| Sistema de Thanks | (custom) | Umbrales de Thanks Ratio, integración con ratio bonus |
Tipos de campo soportados: boolean, bool01, text, integer, decimal, bytes, theme. Cada uno con su hint contextual.
Resultado: El operador cambia el ratio mínimo del tracker desde la UI. Sin SSH, sin editar .php, sin php artisan config:cache. El cambio persiste en DB (tabla settings que también añadimos — ver §19) y se aplica en caliente.
Este panel es un par de cosas a la vez:
-
Reducción de riesgo operativo: cada acción crítica del tracker tiene un botón con confirmación y log. Antes había que recordar la incantación exacta de
php artisanen una terminal de root a las 03:00. Ahora hay un panel rojo confa-radiationque dice "vas a borrar el cache de Meilisearch, ¿seguro?". -
Documentación viva: la organización del panel ES la documentación. Un nuevo miembro del staff abre
/staff/commandsy entiende qué herramientas existen sin tener que leer 40 archivos enapp/Console/Commands/. -
Aporte a la escena FOSS: hasta donde sabemos, ningún fork público de UNIT3D ha tenido esto. Es trabajo que la comunidad puede portar — los archivos están versionados, sin obfuscar, sin licencia restrictiva más allá de la AGPLv3 del proyecto madre.
-
Wiki Gate + flujo guiado para uploaders:
- El tracker no suelta al usuario a ciegas:
config('other.upload-guide_url')apunta a/pages/4. - El
PageSeederdocumenta explícitamente el uso de Singularity / RaW_Suite como vía recomendada de subida profesional. - Resultado: el staff no responde 80 veces la misma pregunta; la wiki hace de puerta de entrada y Singularity hace de herramienta pesada.
- El tracker no suelta al usuario a ciegas:
El panel del tracker no vive aislado. La lógica de identificación y mantenimiento masivo se coordina con Singularity / RaW_Suite, cuyo repo local está en:
https://github.com/RawSmokeTerribilus/Singularityhttps://codeberg.org/RawSmoke/Singularity
Lo relevante para admins y owners:
https://rawsmoke.codeberg.page/Singularity/unit3d_mass_edition/— documenta la suite de Mass-Editionhttps://rawsmoke.codeberg.page/Singularity/unit3d_mass_edition/pipeline/— pipeline, workflows, setup y seguridadhttps://rawsmoke.codeberg.page/Singularity/unit3d_mass_edition/workflows/— menú interactivo del módulo UNIT3Dconfig/mass_config.py— config del tracker para edición masiva
Qué aporta:
- Procesado masivo
- Upload masivo
- Edición masiva de páginas de torrents en trackers UNIT3D
- Resurrección de imágenes
- Limpieza / enriquecimiento de descripciones
- Recoordinación de metadata a escala, no torrent a torrent
Esto no es una curiosidad lateral. Es otra innovación de admin/owner: un multi-tool externo que habla el idioma del tracker y permite operar cientos de torrents con disciplina de pipeline, no con clics manuales.
🕹️ Arcade ScummVM en WebAssembly (pioneros).
El Desafío: Ningún fork de UNIT3D había intentado jamás ejecutar juegos de aventura clásicos directamente dentro del tracker. Nosotros lo hicimos.
Lo que construimos:
Una sala de arcade completa integrada en el tracker, con ScummVM compilado a WebAssembly corriendo directamente en el navegador, sin plugins, sin instalaciones, sin salir de la web.
🎮 7 CLÁSICOS DE LUCASARTS (motor SCUMM):
• The Secret of Monkey Island (VGA CD)
• Monkey Island 2: LeChuck's Revenge (CD Talkie)
• Maniac Mansion
• Loom (CD Talkie)
• Zak McKracken and the Alien Mindbenders
• Indiana Jones and the Fate of Atlantis (CD Talkie)
• Sam & Max Hit the Road (CD Talkie)
💾 PARTIDAS GUARDADAS POR USUARIO:
• Cada usuario tiene su propio espacio de guardado en base de datos
• Carga y descarga transparente vía API REST
• Las partidas persisten entre sesiones y dispositivos
⚙️ STACK TÉCNICO:
• ScummVM compilado a WASM con Asyncify (builds sin pthreads)
• Sin SharedArrayBuffer — sin cabeceras COOP/COEP necesarias
• Un único plugin cargado: libscumm.so (~3MB) — solo el motor SCUMM
• scummvm.js (~9MB) + scummvm.wasm (~37MB) servidos como estáticos
• INI generado dinámicamente por GamingController (savepath, idioma, subtítulos)
• Pantalla completa nativa: requestFullscreen() desde botón dedicado
🏗️ ARQUITECTURA EN LARAVEL:
• GamingController: catálogo estático de 7 juegos con metadatos completos
• GameSaveController (API): CRUD de partidas con validación por user_id + game_id
• Migración game_saves: tabla relacional con unicidad (user_id, game_id, filename)
• Blade views: arcade.index (catálogo) + arcade.show (reproductor con launcher JS)
• scummvm-launcher.js: 7 secciones INI, gestión de guardados, eventos de fullscreen
Detalles de la implementación:
- Los archivos de juego (ROMs) son gitignoreados — copyright. La estructura de directorios SÍ está en git con un
README.mdpor juego que lista los archivos necesarios y su fuente exacta. - El motor WASM también es gitignoreado (~50MB). Ver
docs/GAMING_SETUP.mdpara instrucciones completas de instalación. - Los archivos de
public/son propiedad de uid=82 (www-data del contenedor) — cualquier copia requieresudo+chown.
Por qué es pionero: Buscamos en todos los forks públicos de UNIT3D. Ninguno tiene arcade. Ninguno ha embebido ScummVM WASM. Ninguno tiene sistema de guardado por usuario. Nosotros lo tenemos en producción.
"Primer tracker privado con sala de arcade integrada y ScummVM corriendo en el navegador."
🔬 Recuperación forense point-in-time, aislada de producción.
El Desafío: El 10 de mayo de 2026 sufrimos un desastre en la BD de producción y tuvimos que recuperarla con un laboratorio improvisado y desechable. Nunca más a ciegas: convertimos esa recuperación en algo repetible, aislado y rápido.
Lo que construimos:
Un laboratorio MySQL permanente, apagado por defecto, junto al stack de producción. Hace recuperación híbrida point-in-time (PITR): restaura el último dump en un laboratorio aislado, reproduce los binlogs hacia adelante para rellenar el hueco, compara laboratorio contra producción por clave primaria, y exporta solo las filas que faltan para una fusión manual deliberada. El banco nunca escribe en producción.
🔒 GARANTÍAS DE AISLAMIENTO:
• Apagado por defecto (profiles: [forensics]) — 0 RAM en reposo
• Red internal: true — sin egreso, sin ruta a la BD de prod
• Toda ruta de prod montada :ro (solo lectura)
• Datadir propio del laboratorio — jamás el datadir de prod
• Cero escalada de privilegios en prod (coordenada PITR sin grants)
🧬 RECUPERACIÓN HÍBRIDA PITR:
• Restaura el último dump de 4h + replay de binlog a HEAD (o --until)
• lab-diff: anti-join exacto por id → qué le falta a prod (autoritativo)
• lab-export: INSERT IGNORE solo de las filas faltantes
• torrent-validate: fila BD <-> .torrent <-> info_hash (bencode propio)
⚙️ STACK TÉCNICO:
• docker-compose.forensics.yml — lab-db (mysql 8.0) + forensics (Percona)
• Imagen Percona: trae mysqlbinlog versión-correcta (server pkg) + toolkit
• Coordenada PITR sin privilegios: sidecar <dump>.binlogpos (file+pos vía stat)
• Orquestación por host vía docker exec: bin/forensics/*.sh
• Imagen pública: rawsmoke/unit3d-forensics en Docker Hub
Detalles de la implementación:
- Toda la config es por entorno:
forensics/forensics.env.example(plantilla, en git) yforensics/forensics.envlocal gitignoreado (puede contener la contraseña del laboratorio). gtid_mode=OFFen prod → el replay es por file+pos (mysqlbinlog --start-position), no GTID. Ventana PITR de 30 días (binlog_expire_logs_seconds).- Probado el 2026-05-29: una fila ausente del snapshot, recuperada puramente por replay de binlog, verificada exacta — sin grant, sin escritura en prod, sin downtime.
- Tablas con clave compuesta (
peers,history) se omiten en diff/export; las tablas de contenido humano (users/topics/posts/comments/torrents) todas tienenid.
Por qué importa: La mayoría de los forks confían en backups y rezan. Nosotros tenemos un banco forense air-gapped que recupera pérdidas recientes fila a fila, sin tocar producción jamás. Recuperación como disciplina, no como pánico.
"El banco nunca escribe en producción. La fusión a prod siempre la confirma un humano."
Para un tracker completamente nuevo en una máquina limpia:
# 1. Clonar
git clone https://github.com/RawSmokeTerribilus/UNIT3D_Docker.git
cd UNIT3D_Docker
# 2. Configurar
cp .env.example .env
# Edita .env con tu configuración:
# - APP_URL, ANNOUNCE_URL
# - Credenciales de la BD
# - Ajustes de MAIL_*
# - MEILISEARCH_KEY
# - TMDB_API_KEY (opcional)
# 3. Instalar
make install
# 4. Sembrar datos iniciales (opcional)
docker compose exec app php artisan db:seed
docker compose exec app php artisan scout:import
# 5. Acceder
# Web: http://localhost:8008
# Login: UNIT3D / UNIT3D (del seeder)Qué hace make install:
- Crea directorios
storage/framework - Establece permisos (775 en
storage/,bootstrap/cache/) - Construye las imágenes de Docker
- Inicia todos los contenedores
- El entrypoint gestiona automáticamente
composer/npm/migraciones
Si tu tracker se cae o te estás mudando a un nuevo servidor:
# 1. Ten tu backup
ls -lh backups/snapshot_*/unit3d_full_snapshot_*.tar.gz
# 2. En el nuevo host, extrae
mkdir -p /home/rawserver/UNIT3D_Docker
tar -xzf backup.tar.gz -C /home/rawserver/UNIT3D_Docker
# 3. Inicia los contenedores
cd /home/rawserver/UNIT3D_Docker
make up
# 4. Espera a que MySQL arranque
sleep 10
# 5. Restaura la base de datos
docker exec -i unit3d-db mysql -u unit3d -punit3d unit3d < db_unit3d.sql
# 6. Reinicia la capa de la aplicación
make restart
# 7. Verifica
make healthPor qué funciona esto:
- El backup incluye todo: código fuente,
vendor/,node_modules/, configuraciones - El volcado de la base de datos está incluido
- El snapshot puede copiarse automáticamente a un disco externo configurado en
.env - La reconstrucción usa el código en disco y la receta del despliegue versionada
⚠️ No recomendada salvo que sepas exactamente lo que haces. El stack de referencia de N.O.B.S es el dockerizado (Rutas A/B): es el que probamos, respaldamos y al que apunta el resto de features de este README. La vía baremetal existe para resucitar la edición comunitaria en un host sin Docker, no para reproducir el fork completo. Si no te sientes cómodo administrando nginx, PHP-FPM, MariaDB/MySQL, Redis, Meilisearch y supervisor a mano, quédate en Docker.
Instalador: Unit3d_9.2-Installer-fixed/Unit3d-installer-debian.sh (Debian Trixie /
Ubuntu 22.04 / 24.04). Instala PHP 8.4-FPM, nginx, MariaDB, Redis, Meilisearch
(systemd), Node 20/Bun, Certbot/SSL, supervisor (cola) y el cron del scheduler;
~20-25 min en un host limpio. Lee también Unit3d_9.2-Installer-fixed/readme.md y
SECURITY_CHANGES.md.
Qué adaptar respecto al setup dockerizado (el instalador NO lo cubre):
- Origen del código: clona este fork, no la community vanilla — si no, no tendrás ninguna feature N.O.B.S (las 10 migraciones propias, Telegram, arcade, resolver multi-proveedor…).
.envcon hosts locales: sustituye los nombres de servicio de Docker por127.0.0.1+ puertos reales —DB_HOST,REDIS_HOST,MEILISEARCH_HOST,MAIL_*(sin contenedor Mailpit → SMTP real),TRUSTED_PROXIESsegún tu edge.- Tracker Rust (UNIT3D-Announce): el instalador no lo monta.
/announce/necesita compilar y correr el binario Rust como servicio aparte, su proxy en nginx y las varsTRACKER_*. Sin esto no hay tracker desacoplado de PHP. - Meta-worker: el supervisor del instalador solo levanta
queue:work(coladefault). Añade un segundo programa para--queue=meta-refresh(timeout 300s) o el refresco de metadatos ahogará la cola principal. - Backups y forense:
backup.shy el banco forense son solo-Docker (docker compose stop/up,docker exec). En baremetal usamysqldumppor cron; el PITR del banco forense no aplica (asume MySQL 8.0 con ROW binlogs, no MariaDB). - Assets pesados gitignoreados: ejecuta
install-swarm-assets.sh(mapa 3D) y coloca a mano los blobs de RetroArch/ScummVM WASM; ajusta elchownalwww-datanativo del host (no el uid 82 del contenedor). - Cabeceras COOP/COEP + proxy TMDB: porta a tu nginx nativo el middleware
gaming.isolationy las rutas/tmdb-proxy·/authenticated-images, o el arcade WASM y los pósters bajo CSP fallarán. - Telegram: integración no incluida en baremetal — config + webhook manuales.
- Auto-reparación: sin
entrypoint.sh, corre tú las migraciones, elauto:email-blacklist-update(cron) y los permisos775/www-datatras cada cambio.
En resumen: la ruta baremetal te deja un UNIT3D comunitario estabilizado y funcionando; reconstruir el fork N.O.B.S completo encima es trabajo manual considerable. Recomendamos Docker salvo que tengas una razón fuerte y el conocimiento para mantenerlo.
make help # Muestra todos los comandos
make install # Instalación fresca (carpetas, permisos, build, up)
make up # Inicia los contenedores (modo daemon)
make stop # Detiene los contenedores
make restart # Reinicia app + web (después de cambios en el código)
make status # Muestra el estado de los contenedores
make backup # Ejecuta el backup quirúrgico (sudo ./backup.sh)
make health # Ejecuta los chequeos de salud
make logs # Muestra los logs de la app en vivo
make clean # Limpia y recachea config/route/view de Laravel
make meilisearch # Aplica la configuración de índices duales (Torrents + People)
make meilisearch-fix # Reinicia Meilisearch desde cero (borra data y reindexa)┌──────────────────────────────────────────────────────────┐
│ NGINX (web · puerto 8008) │
│ Proxy Inverso + Estáticos + TMDB Image Proxy │
└─────┬────────────────────────────────────┬───────────────┘
│ /announce/* │ /* (Laravel)
▼ ▼
┌──────────────┐ ┌────────────────┐
│ announce │ │ PHP-FPM (app) │
│ Rust tracker │ │ Laravel 12 │
│ :6969 │ │ :9000 │
└──────┬───────┘ └────────┬───────┘
│ API interna │
└────────────────┬──────────────────┘
│
┌────────┬───────────┼──────────────┬──────────────┬─────────────┐
│ │ │ │ │ │
┌──▼──┐ ┌──▼──┐ ┌────▼─────┐ ┌─────▼──────┐ ┌────▼─────┐ ┌────▼──────┐
│MySQL│ │Redis│ │Meilisearch│ │ Mailpit │ │ Worker │ │meta-worker│
│ 8.0 │ │ │ │ │ │ (test box) │ │ default │ │meta-refresh│
└─────┘ └─────┘ └───────────┘ └────────────┘ └──────────┘ └───────────┘
Scheduler: php artisan schedule:work (cron en segundo plano)
Worker: php artisan queue:work --queue=default (Telegram, mails, jobs ligeros)
Meta-worker: php artisan queue:work --queue=meta-refresh (TMDB/IGDB/MAL/Anilist/IMDb)
Announce: binario Rust (UNIT3D-Announce) — código vendorizado en rust-announce/
| Servicio | Interno | Externo | Propósito |
|---|---|---|---|
Nginx (web) |
80 | 8008 | UI Web + proxy a /announce/ |
Rust Tracker (announce) |
6969 | — | Tracker BitTorrent (solo accesible vía nginx) |
PHP-FPM (app) |
9000 | — | Entorno de ejecución de la app |
MySQL (db) |
3306 | 3307 | Base de datos |
| Redis | 6379 | 6380 | Caché/Sesiones/Cola |
| Meilisearch | 7700 | 7701 | Motor de Búsqueda |
| Mailpit | 1025/8025 | 8026 | Pruebas de Correo |
| Scheduler | — | — | schedule:work (background) |
| Worker | — | — | Cola default |
| Meta-worker | — | — | Cola meta-refresh |
Mantén esto a salvo:
APP_KEY— Clave de encriptación de Laravel (generada en la instalación)MAIL_PASSWORD— Credenciales SMTPMEILISEARCH_KEY— Llave Maestra del motor de búsquedaTMDB_API_KEY— Acceso a API de tercerosTELEGRAM_BOT_TOKEN— Token del bot de Telegram (acceso total a la API del bot)
Nunca comitees .env al control de versiones. Usa .env.example como plantilla.
- Las sesiones son solo HTTPS (
SESSION_SECURE_COOKIE=true) - El dominio de la sesión es explícito (
SESSION_DOMAIN=tu-dominio) - La protección contra fuerza bruta está ajustada para evitar bloqueos
- Las direcciones IP se reenvían correctamente (sin exposición de la puerta de enlace de Docker)
# Se arregla automáticamente al reiniciar, pero para forzar:
docker compose restart app
docker exec unit3d-app chmod -R 775 storage bootstrap/cache
docker exec unit3d-app chown -R www-data:www-data storage bootstrap/cache# Reindexar Meilisearch
docker compose exec app php artisan scout:import
# Verificar salud
make health# Revisa el panel de Mailpit (si usas pruebas locales)
# Abre: http://localhost:8026
# Si usas SMTP:
docker compose logs app | grep -i mail
# Probar vía Tinker
docker compose exec app php artisan tinker
# >>> Mail::raw('Test', fn($m) => $m->to('test@example.com')->send());# Revisa los logs de MySQL
docker compose logs db
# Si está corrupta, restaura desde el backup
# Ver "Ruta B: Restaurar desde un Backup" arriba# Verificar que el worker está procesando jobs de Telegram
docker compose logs worker | tail -20
# Comprobar que la ruta del webhook está registrada correctamente
docker compose exec -T app php artisan route:list | grep telegram
# Verificar configuración del webhook con Telegram
curl https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getWebhookInfo# Confirmar que el meta-worker está vivo
docker compose ps meta-worker
docker compose logs meta-worker | tail -50
# Forzar refresco manual de un lote
docker compose exec app php artisan meta:refresh-dispatch --limit=20 --stale-hours=0
# Re-resolver un torrent puntual (multi-proveedor)
docker compose exec app php artisan meta:sync --force --limit=1
# Rotar póster activo (artwork rotativo)
docker compose exec app php artisan meta:rotate-covers# Los assets de Swarm son gitignored. Re-descargar:
./install-swarm-assets.sh
# o dentro del contenedor:
docker compose exec app ./install-swarm-assets.sh
# Verificar que los archivos existen y tienen tamaño razonable
ls -lh public/vendor/{force-graph,3d-force-graph,three,d3}/*.js# Activar modo debug en el reproductor
# Abrir: https://tu-tracker/retroarch/{system}/{game}?debug
# Recargar el listado de cores y cubiertas
docker compose exec app php artisan retroarch:scan-roms
docker compose exec app php artisan retroarch:fetch-covers
# Verificar headers COOP/COEP en la página show (requeridos por algunos cores)
curl -I https://tu-tracker/retroarch/snes/show | grep -iE 'cross-origin'# Sincronización en caliente con el binario Rust
docker compose exec app php artisan tracker:sync-users
docker compose exec app php artisan tracker:sync-torrents
docker compose exec app php artisan tracker:sync-groupsEste proyecto refleja más de 2000 horas de trabajo para reparar y ampliar UNIT3D partiendo de la edición comunitaria. Cada arreglo, cada automatización, cada redundancia existe porque creemos en la plataforma.
- Primero sin conexión: Funciona de forma completamente autónoma (sin dependencias en la nube)
- Resiliente: Se autorepara de fallos comunes (permisos, carpetas faltantes, tiempos de espera de red)
- Transparente: Los cambios están documentados y justificados (ver este README)
- Mantenible: Makefile simple + scripts que cualquiera puede entender
- Peer-to-peer: Diseñado para comunidades que gestionan su propia infraestructura
Este es un software de tracker para gente que gestiona trackers, no un producto SaaS con dependencia de un proveedor.
¿Encontraste un bug? ¿Tienes una mejora? ¡Los Issues y PRs son bienvenidos!
Este es un fork comunitario. Estamos mejorando UNIT3D en beneficio de los operadores de trackers privados de todo el mundo.
UNIT3D está licenciado bajo la GNU Affero General Public License v3.0. Ver LICENSE.txt.
Este fork mantiene la misma licencia y espíritu: abierto, transparente e impulsado por la comunidad.
- HDInnovations por crear UNIT3D
- La escena de trackers privados por décadas de innovación y construcción de comunidades
- El equipo de N.O.B.S por las 2000+ horas que tomó hacer que esto funcionara
Última Actualización: Mayo 2026 | Estado: 🟢 Listo para Producción
Hecho con resiliencia y cuidado.
De la scene. Para la scene.





















