luna-clasificador 1.2.0 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BD/luna_db.json +1 -0
- package/LUNA_clasificador.js +103 -53
- package/assets/icons.png +0 -0
- package/package.json +15 -2
- package/readme.md +208 -38
- package/tests/luna_dashboard.html +175 -0
- package/tests/luna_dashboard_server.js +292 -0
package/BD/luna_db.json
CHANGED
|
@@ -1238,6 +1238,7 @@
|
|
|
1238
1238
|
"contra el destino": { "nombreOficial": "Contra el Destino", "confianza": 1.0 },
|
|
1239
1239
|
"destino": { "nombreOficial": "Destino", "confianza": 0.8 },
|
|
1240
1240
|
"dueña de mi": { "nombreOficial": "Dueña de Mí", "confianza": 1.0 },
|
|
1241
|
+
"dueñ de mi": { "nombreOficial": "Dueña de Mí", "confianza": 1.0 },
|
|
1241
1242
|
"Dueñ De Mi": { "nombreOficial": "Dueña de Mí", "confianza": 1.0 },
|
|
1242
1243
|
"el arbol de los secretos": { "nombreOficial": "El Árbol de los Secretos", "confianza": 1.0 },
|
|
1243
1244
|
"el extraño en el espejo": { "nombreOficial": "El Extraño en el Espejo", "confianza": 1.0 },
|
package/LUNA_clasificador.js
CHANGED
|
@@ -14,7 +14,7 @@ const path = require('path');
|
|
|
14
14
|
const { setBinariosFromFolder, procesarDuracionMediainfo } = require('./Servicios/ProcesosCore.js');
|
|
15
15
|
const Clasificador = require('./Clasificacion.js');
|
|
16
16
|
const { guardarLogDebug } = require('./Debug/LUNA_Logger.js');
|
|
17
|
-
const VERSION_LUNA = '1.2.
|
|
17
|
+
const VERSION_LUNA = '1.2.1';
|
|
18
18
|
var seEjecuto = false;
|
|
19
19
|
var seEjecutoIPC = false;
|
|
20
20
|
|
|
@@ -665,6 +665,24 @@ function normalizarClaveDB(nombre) {
|
|
|
665
665
|
.toLowerCase();
|
|
666
666
|
}
|
|
667
667
|
|
|
668
|
+
function variantesClaveDB(nombre) {
|
|
669
|
+
const base = normalizarClaveDB(nombre);
|
|
670
|
+
if (!base) return [];
|
|
671
|
+
|
|
672
|
+
const variantes = new Set([base]);
|
|
673
|
+
|
|
674
|
+
// Variante útil para títulos con la 'a' faltante tras quitar tildes
|
|
675
|
+
// (ej. "Dueñ De Mi" -> "Duena De Mi")
|
|
676
|
+
const conA = base
|
|
677
|
+
.split(/\s+/)
|
|
678
|
+
.map(palabra => /^[a-z]+n$/i.test(palabra) ? palabra + 'a' : palabra)
|
|
679
|
+
.join(' ');
|
|
680
|
+
|
|
681
|
+
if (conA && conA !== base) variantes.add(conA);
|
|
682
|
+
|
|
683
|
+
return [...variantes];
|
|
684
|
+
}
|
|
685
|
+
|
|
668
686
|
// Busca un nombre en una categoria concreta.
|
|
669
687
|
// Estrategia:
|
|
670
688
|
// 1. aliases (global, fuera de categoria) -> nombre canonico
|
|
@@ -675,46 +693,48 @@ function buscarEnCategoriaDB(db, nombreLimpio, categoria) {
|
|
|
675
693
|
const bucket = db.categorias[categoria];
|
|
676
694
|
if (!bucket || !bucket.nombres) return null;
|
|
677
695
|
|
|
678
|
-
const
|
|
679
|
-
if (!
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
696
|
+
const claves = variantesClaveDB(nombreLimpio);
|
|
697
|
+
if (!claves.length) return null;
|
|
698
|
+
|
|
699
|
+
for (const claveOriginal of claves) {
|
|
700
|
+
// 1. Resolver aliases (pueden encadenarse, max 3 saltos)
|
|
701
|
+
let clave = claveOriginal;
|
|
702
|
+
if (db.aliases) {
|
|
703
|
+
for (let salto = 0; salto < 3; salto++) {
|
|
704
|
+
const destino = db.aliases[clave] || Object.entries(db.aliases).find(([k]) => normalizarClaveDB(k) === clave)?.[1];
|
|
705
|
+
if (!destino) break;
|
|
706
|
+
const destinoNorm = normalizarClaveDB(destino);
|
|
707
|
+
if (!destinoNorm || destinoNorm === clave) break;
|
|
708
|
+
clave = destinoNorm;
|
|
709
|
+
}
|
|
690
710
|
}
|
|
691
|
-
}
|
|
692
711
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
712
|
+
// 2. Match exacto por clave normalizada
|
|
713
|
+
for (const [keyCatalogo, entrada] of Object.entries(bucket.nombres)) {
|
|
714
|
+
const keyNorm = normalizarClaveDB(keyCatalogo);
|
|
715
|
+
if (keyNorm === clave) {
|
|
716
|
+
return {
|
|
717
|
+
categoria,
|
|
718
|
+
nombreOficial: entrada.nombreOficial || keyCatalogo,
|
|
719
|
+
confianza: entrada.confianza || 1.0,
|
|
720
|
+
viaAlias: clave !== claveOriginal
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
}
|
|
703
724
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
};
|
|
725
|
+
// 3. Match por prefijo comparando versiones normalizadas
|
|
726
|
+
for (const [keyCatalogo, entrada] of Object.entries(bucket.nombres)) {
|
|
727
|
+
const keyNorm = normalizarClaveDB(keyCatalogo);
|
|
728
|
+
if (keyNorm.length < 4) continue;
|
|
729
|
+
if (clave.startsWith(keyNorm + ' ')) {
|
|
730
|
+
return {
|
|
731
|
+
categoria,
|
|
732
|
+
nombreOficial: entrada.nombreOficial || keyCatalogo,
|
|
733
|
+
confianza: entrada.confianza || 1.0,
|
|
734
|
+
viaAlias: false,
|
|
735
|
+
viaPrefijo: true
|
|
736
|
+
};
|
|
737
|
+
}
|
|
718
738
|
}
|
|
719
739
|
}
|
|
720
740
|
|
|
@@ -784,22 +804,55 @@ function tomarDecision(paquete) {
|
|
|
784
804
|
const pPeli = (rep.pelicula && rep.pelicula.puntos) || 0;
|
|
785
805
|
const pSerie = (rep.serie && rep.serie.puntos) || 0;
|
|
786
806
|
let pVari = (rep.variado && rep.variado.puntos) || 0;
|
|
787
|
-
let pPeliFinal = pPeli;
|
|
788
807
|
let elegido = null;
|
|
789
808
|
|
|
790
|
-
|
|
809
|
+
// === DB override primero: si el nombre limpio coincide con una categoria
|
|
810
|
+
// de la DB, gana esa categoria antes del desempate por puntuacion.
|
|
811
|
+
// Esto corrige casos como 'La Novia' / 'Dueña de Mi' que son novelas.
|
|
812
|
+
try {
|
|
813
|
+
const db = cargarDB();
|
|
814
|
+
if (db && archivo.info && archivo.info.nombreLimpio) {
|
|
815
|
+
const prioridad = ['novela', 'programa', 'pelicula', 'variado'];
|
|
816
|
+
for (const categoria of prioridad) {
|
|
817
|
+
const matchDB = buscarEnCategoriaDB(db, archivo.info.nombreLimpio, categoria);
|
|
818
|
+
if (!matchDB) continue;
|
|
819
|
+
|
|
820
|
+
const mapa = {
|
|
821
|
+
novela: 'esNOVELA',
|
|
822
|
+
programa: 'esPROGRAM',
|
|
823
|
+
pelicula: 'esPELI',
|
|
824
|
+
variado: 'esVARIADO'
|
|
825
|
+
};
|
|
826
|
+
|
|
827
|
+
elegido = mapa[categoria];
|
|
828
|
+
archivo.info.DBmetadata = {
|
|
829
|
+
fuente: db.fuente || null,
|
|
830
|
+
version: db.version || null,
|
|
831
|
+
categoriaDB: categoria,
|
|
832
|
+
nombreOficial: matchDB.nombreOficial,
|
|
833
|
+
viaAlias: !!matchDB.viaAlias,
|
|
834
|
+
viaPrefijo: !!matchDB.viaPrefijo
|
|
835
|
+
};
|
|
836
|
+
break;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
} catch (e) {
|
|
840
|
+
paquete.errores.push({
|
|
841
|
+
etapa: 'tomarDecision.dbOverride',
|
|
842
|
+
ruta: archivo.ruta,
|
|
843
|
+
mensaje: e.message
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// === NUEVO: Chequeo de DB para VARIADO (350 puntos) ===
|
|
791
848
|
let esVariadoDB = false;
|
|
792
849
|
try {
|
|
793
850
|
const db = cargarDB();
|
|
794
851
|
if (db && archivo.info && archivo.info.nombreLimpio) {
|
|
795
|
-
// Usamos la misma función de búsqueda, pero en la categoría 'variado'
|
|
796
852
|
const matchDB = buscarEnCategoriaDB(db, archivo.info.nombreLimpio, 'variado');
|
|
797
853
|
if (matchDB) {
|
|
798
|
-
|
|
799
|
-
pVari += 350;
|
|
854
|
+
pVari += 350;
|
|
800
855
|
esVariadoDB = true;
|
|
801
|
-
|
|
802
|
-
// Guardamos la metadata para que el usuario sepa por qué
|
|
803
856
|
archivo.info.DBmetadata = {
|
|
804
857
|
fuente: db.fuente || null,
|
|
805
858
|
version: db.version || null,
|
|
@@ -811,7 +864,6 @@ function tomarDecision(paquete) {
|
|
|
811
864
|
}
|
|
812
865
|
}
|
|
813
866
|
} catch (e) {
|
|
814
|
-
// Si falla la DB, no rompemos el pipeline
|
|
815
867
|
paquete.errores.push({
|
|
816
868
|
etapa: 'tomarDecision.dbVariado',
|
|
817
869
|
ruta: archivo.ruta,
|
|
@@ -819,16 +871,15 @@ function tomarDecision(paquete) {
|
|
|
819
871
|
});
|
|
820
872
|
}
|
|
821
873
|
|
|
822
|
-
|
|
874
|
+
// === NUEVO: Chequeo de DB para PELICULA (350 puntos) ===
|
|
823
875
|
let esPeliDB = false;
|
|
824
876
|
try {
|
|
825
877
|
const db = cargarDB();
|
|
826
878
|
if (db && archivo.info && archivo.info.nombreLimpio) {
|
|
827
879
|
const matchDB = buscarEnCategoriaDB(db, archivo.info.nombreLimpio, 'pelicula');
|
|
828
880
|
if (matchDB) {
|
|
829
|
-
|
|
881
|
+
pPeli += 350;
|
|
830
882
|
esPeliDB = true;
|
|
831
|
-
|
|
832
883
|
archivo.info.DBmetadata = {
|
|
833
884
|
fuente: db.fuente || null,
|
|
834
885
|
version: db.version || null,
|
|
@@ -847,10 +898,9 @@ function tomarDecision(paquete) {
|
|
|
847
898
|
});
|
|
848
899
|
}
|
|
849
900
|
|
|
850
|
-
|
|
851
|
-
if (
|
|
852
|
-
else if (
|
|
853
|
-
else if (pVari > pPeli && pVari > pSerie) elegido = 'esVARIADO';
|
|
901
|
+
if (elegido === null && pPeli > pSerie && pPeli > pVari) elegido = 'esPELI';
|
|
902
|
+
else if (elegido === null && pSerie > pPeli && pSerie > pVari) elegido = 'esSERIE';
|
|
903
|
+
else if (elegido === null && pVari > pPeli && pVari > pSerie) elegido = 'esVARIADO';
|
|
854
904
|
|
|
855
905
|
// NUEVO: Si la DB lo marcó como variado, forzamos la decisión.
|
|
856
906
|
if (esVariadoDB) {
|
package/assets/icons.png
ADDED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "luna-clasificador",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
|
+
"icon": "assets/icons.png",
|
|
4
5
|
"description": "Clasificador local de archivos multimedia (película, serie, variado, programa, novela)",
|
|
5
6
|
"main": "LUNA.js",
|
|
6
7
|
"files": [
|
|
@@ -8,6 +9,9 @@
|
|
|
8
9
|
"Clasificacion.js",
|
|
9
10
|
"Debug/LUNA_Logger.js",
|
|
10
11
|
"tests/probe_LUNA.js",
|
|
12
|
+
"tests/luna_dashboard_server.js",
|
|
13
|
+
"tests/luna_dashboard.html",
|
|
14
|
+
"assets/icons.png",
|
|
11
15
|
"LUNA.js",
|
|
12
16
|
"Servicios/ProcesosCore.js",
|
|
13
17
|
"Servicios/ResolverPathffmpeg.js",
|
|
@@ -16,11 +20,20 @@
|
|
|
16
20
|
"optionalDependencies": {
|
|
17
21
|
"ffmpeg-static": "^5.3.0"
|
|
18
22
|
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"clasificador",
|
|
25
|
+
"multimedia",
|
|
26
|
+
"peliculas",
|
|
27
|
+
"series",
|
|
28
|
+
"ffmpeg",
|
|
29
|
+
"nodejs",
|
|
30
|
+
"file-organizer",
|
|
31
|
+
"media-classifier"
|
|
32
|
+
],
|
|
19
33
|
"engines": {
|
|
20
34
|
"node": ">=18"
|
|
21
35
|
},
|
|
22
36
|
"author": "Abraham de Jesús Piñirí Megret",
|
|
23
|
-
"keywords": ["multimedia", "clasificador", "video", "ffmpeg", "series", "peliculas"],
|
|
24
37
|
"license": "MIT",
|
|
25
38
|
"repository": {
|
|
26
39
|
"type": "git",
|
package/readme.md
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
|
|
2
|
+
<p align="center">
|
|
3
|
+
<img src="./assets/icons.png" width="120" alt="Logo del proyecto">
|
|
4
|
+
</p>
|
|
5
|
+
|
|
2
6
|
# 📘 README — Proyecto LUNA
|
|
3
7
|
|
|
8
|
+
<!-- Banner -->
|
|
9
|
+
<p align="center">
|
|
10
|
+
<img src="./assets/banner.png" width="100%">
|
|
11
|
+
</p>
|
|
4
12
|
> **Clasificador local de archivos multimedia** (película / serie / variado) con sistema de banderas puntuables, rescate por contexto de carpeta y reclasificación mediante base de datos de nombres conocidos.
|
|
5
13
|
|
|
6
14
|
---
|
|
@@ -25,12 +33,13 @@
|
|
|
25
33
|
8. [Sistema de puntajes y decisión](#-sistema-de-puntajes-y-decisión)
|
|
26
34
|
9. [Rescate y reclasificación](#-rescate-y-reclasificación)
|
|
27
35
|
10. [API pública](#-api-pública)
|
|
28
|
-
11. [Ejemplos de uso](#-ejemplos-de-uso)
|
|
36
|
+
11. [Ejemplos reales de uso](#-ejemplos-reales-de-uso)
|
|
29
37
|
12. [Instalación y dependencias](#-instalación-y-dependencias)
|
|
30
38
|
13. [Cómo ejecutar el probe](#-cómo-ejecutar-el-probe)
|
|
31
|
-
14. [
|
|
32
|
-
15. [
|
|
33
|
-
16. [
|
|
39
|
+
14. [Novedades v1.2.1](#-novedades-en-v121)
|
|
40
|
+
15. [Historial de fixes](#-historial-de-fixes)
|
|
41
|
+
16. [Limitaciones conocidas](#-limitaciones-conocidas)
|
|
42
|
+
17. [Autoría y licencia](#-autoría-y-licencia)
|
|
34
43
|
|
|
35
44
|
---
|
|
36
45
|
|
|
@@ -44,9 +53,28 @@
|
|
|
44
53
|
- `esNOVELA` → telenovela
|
|
45
54
|
- `esVARIADO` → video suelto, clip, personal, documental, deporte
|
|
46
55
|
|
|
47
|
-
Además devuelve metadata enriquecida: **nombre limpio**, **temporada**, **capítulo**, **año**, **duración**, y **puntaje de confianza**.
|
|
56
|
+
Además devuelve metadata enriquecida: **nombre limpio**, **temporada**, **capítulo**, **año**, **duración**, **fecha extra**, **tipo de archivo** y **puntaje de confianza**.
|
|
57
|
+
|
|
58
|
+
Fue diseñado para ordenar discos duros llenos de series, películas, novelas y videos personales, respetando nombres "sucios" típicos de la piratería, releases, grabaciones de TV y nombres manuales de series extranjeras.
|
|
59
|
+
|
|
60
|
+
> Versión actual del proyecto: **1.2.1**
|
|
61
|
+
> Base de datos actual: **1.0.0**
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 🆕 Novedades en v1.2.1
|
|
66
|
+
|
|
67
|
+
Esta versión deja el sistema mucho más estable y útil en escenarios reales:
|
|
48
68
|
|
|
49
|
-
|
|
69
|
+
- `infoDBMetadata()` cuenta correctamente el número total de registros de todas las categorías de `luna_db.json`.
|
|
70
|
+
- El banner muestra una línea con `DB registros: X`.
|
|
71
|
+
- El override de la DB ya se aplica antes del desempate, evitando que series/películas/novelas se queden en una categoría equivocada por heurísticas.
|
|
72
|
+
- La comparación normalizada contra la DB corrige nombres con tildes, puntos, espacios y variantes como `Dueñ De Mi`, `La Novia`, etc.
|
|
73
|
+
- La salida final expone `extension` y `tipoArchivo` de forma segura (`mkv`, `mp4`, `video`, `audio`).
|
|
74
|
+
- Se mantiene compatibilidad con `archivo.info.extension`, `archivo.info.tipoArchivo` y la salida final.
|
|
75
|
+
- La lógica de separación de título y nombre limpia siguió refinándose para evitar que capítulos y temporadas se queden pegados en `nombreLimpio`.
|
|
76
|
+
|
|
77
|
+
---
|
|
50
78
|
|
|
51
79
|
---
|
|
52
80
|
|
|
@@ -61,6 +89,9 @@ Fue diseñado para ordenar discos duros llenos de series, películas y videos pe
|
|
|
61
89
|
| **DB como último recurso** | `luna_db.json` no suma puntos: solo reclasifica `esSERIE → esPROGRAM / esNOVELA` si el nombre coincide. |
|
|
62
90
|
| **No romper con entradas inválidas** | `null`, `{}`, `123`, `[]` deben devolver error controlado, no lanzar excepción. |
|
|
63
91
|
|
|
92
|
+
<p align="center">
|
|
93
|
+
<img src="./assets/1.png" width="100%">
|
|
94
|
+
</p>
|
|
64
95
|
---
|
|
65
96
|
|
|
66
97
|
## 🏗 Arquitectura general
|
|
@@ -391,6 +422,10 @@ Usado por Cine, convertidor y el IPC del render para no cargar LUNA hasta que re
|
|
|
391
422
|
- Si el JSON está corrupto, se resetea a `[]`.
|
|
392
423
|
- Nunca lanza excepción hacia arriba.
|
|
393
424
|
|
|
425
|
+
<p align="center">
|
|
426
|
+
<img src="./assets/2.png" width="100%">
|
|
427
|
+
</p>
|
|
428
|
+
|
|
394
429
|
**Estructura de cada registro:**
|
|
395
430
|
|
|
396
431
|
```js
|
|
@@ -674,60 +709,189 @@ Igual que `infoDBMetadata` + `versionLUNA`, `Creador`, `Comentario`.
|
|
|
674
709
|
|
|
675
710
|
---
|
|
676
711
|
|
|
677
|
-
##
|
|
712
|
+
## 🧪 Ejemplos reales de uso
|
|
713
|
+
|
|
714
|
+
A continuación te dejo ejemplos reales de cómo yo lo uso en proyectos de Node y Electron. Están pensados para que veas exactamente qué entra y qué sale.
|
|
678
715
|
|
|
679
|
-
### Uso básico
|
|
716
|
+
### 1) Uso básico en Node.js
|
|
680
717
|
|
|
681
718
|
```js
|
|
682
|
-
const {
|
|
719
|
+
const { getLUNA } = require('luna-clasificador');
|
|
720
|
+
const LUNA = getLUNA();
|
|
683
721
|
|
|
684
722
|
(async () => {
|
|
685
|
-
const
|
|
686
|
-
|
|
687
|
-
console.log(
|
|
688
|
-
console.log(
|
|
689
|
-
console.log(
|
|
723
|
+
const resultado = await LUNA.LUNA('D:\\ANIME\\La Novia - 107 [15-10-2026].mp4');
|
|
724
|
+
|
|
725
|
+
console.log(resultado.exito);
|
|
726
|
+
console.log(resultado.salida.esCC); // esNOVELA
|
|
727
|
+
console.log(resultado.salida.nombreLimpio); // La Novia
|
|
728
|
+
console.log(resultado.salida.info.serie); // La Novia
|
|
729
|
+
console.log(resultado.salida.info.capituloNumero); // 107
|
|
730
|
+
console.log(resultado.salida.extension); // mp4
|
|
731
|
+
console.log(resultado.salida.tipoArchivo); // video
|
|
690
732
|
})();
|
|
691
733
|
```
|
|
692
734
|
|
|
693
|
-
### Uso
|
|
735
|
+
### 2) Uso con `getFFmpegPath()` y configurando la ruta del binario
|
|
736
|
+
|
|
737
|
+
Este es un patrón real que uso cuando el proyecto ya tiene ffmpeg en una ubicación custom.
|
|
694
738
|
|
|
695
739
|
```js
|
|
696
|
-
const
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
const
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
740
|
+
const path = require('path');
|
|
741
|
+
const { getLUNA } = require('luna-clasificador');
|
|
742
|
+
const { getFFmpegPath } = require('./ResolverPathFFmpeg.js');
|
|
743
|
+
|
|
744
|
+
const LUNA = getLUNA();
|
|
745
|
+
const ffmpegExe = getFFmpegPath();
|
|
746
|
+
const carpeta = path.dirname(ffmpegExe) + path.sep;
|
|
747
|
+
|
|
748
|
+
LUNA.setBinariosFromFolder(carpeta);
|
|
749
|
+
|
|
750
|
+
(async () => {
|
|
751
|
+
const res = await LUNA.LUNA('D:\\ANIME\\Peliculas\\John Wick 4.mkv');
|
|
752
|
+
console.log(JSON.stringify(res, null, 2));
|
|
753
|
+
})();
|
|
706
754
|
```
|
|
707
755
|
|
|
708
|
-
###
|
|
756
|
+
### 3) Uso clásico en una app Node / backend
|
|
709
757
|
|
|
710
758
|
```js
|
|
711
|
-
const { getLUNA } = require('
|
|
712
|
-
const
|
|
713
|
-
|
|
759
|
+
const { getLUNA, LUNA_PATH } = require('luna-clasificador');
|
|
760
|
+
const {
|
|
761
|
+
LUNA,
|
|
762
|
+
reclasificarDesdeDB,
|
|
763
|
+
cargarDB,
|
|
764
|
+
infoDBMetadata
|
|
765
|
+
} = getLUNA();
|
|
766
|
+
|
|
767
|
+
const ffmpegExe = getFFmpegPath();
|
|
768
|
+
const carpeta = path.dirname(ffmpegExe) + path.sep;
|
|
769
|
+
LUNA.setBinariosFromFolder(carpeta);
|
|
770
|
+
|
|
771
|
+
(async () => {
|
|
772
|
+
const meta = infoDBMetadata();
|
|
773
|
+
console.log('DB:', meta);
|
|
774
|
+
|
|
775
|
+
const salida = await LUNA('D:\\ANIME\\Thunder 3 [Temp 1] [2026] [Caps 12]\\Dueña de Mi - 139 [16-10-2026].mp4');
|
|
776
|
+
console.log(salida.salida.esCC); // esNOVELA
|
|
777
|
+
console.log(salida.salida.info.DBmetadata);
|
|
778
|
+
})();
|
|
714
779
|
```
|
|
715
780
|
|
|
716
|
-
###
|
|
781
|
+
### 4) Uso en Electron / proceso principal (`main`)
|
|
717
782
|
|
|
718
783
|
```js
|
|
719
|
-
const
|
|
784
|
+
const path = require('path');
|
|
785
|
+
const { ipcMain } = require('electron');
|
|
786
|
+
const { getLUNA } = require('luna-clasificador');
|
|
787
|
+
const { getFFmpegPath } = require('./ResolverPathFFmpeg.js');
|
|
788
|
+
|
|
789
|
+
const LUNA = getLUNA();
|
|
790
|
+
let lunaInicializado = false;
|
|
791
|
+
|
|
792
|
+
ipcMain.handle('luna:clasificar', async (event, entrada) => {
|
|
793
|
+
try {
|
|
794
|
+
if (!lunaInicializado) {
|
|
795
|
+
const ffmpegExe = getFFmpegPath();
|
|
796
|
+
const carpeta = path.dirname(ffmpegExe) + path.sep;
|
|
797
|
+
LUNA.setBinariosFromFolder(carpeta);
|
|
798
|
+
lunaInicializado = true;
|
|
799
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
800
|
+
}
|
|
720
801
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
802
|
+
if (entrada === 'info') {
|
|
803
|
+
return await LUNA.LUNAVersion();
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
return await LUNA.LUNA(entrada);
|
|
807
|
+
} catch (error) {
|
|
808
|
+
console.error('[LUNA:IPC] Error:', error);
|
|
809
|
+
return {
|
|
810
|
+
exito: false,
|
|
811
|
+
salida: null,
|
|
812
|
+
errores: [{ etapa: 'ipc', mensaje: error.message }]
|
|
813
|
+
};
|
|
814
|
+
}
|
|
725
815
|
});
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
### 5) Uso real de ejemplo de mi app con cache de versión
|
|
819
|
+
|
|
820
|
+
```js
|
|
821
|
+
const { getLUNA, LUNA_PATH } = require('luna-clasificador');
|
|
822
|
+
|
|
823
|
+
const LUNA = getLUNA();
|
|
824
|
+
|
|
825
|
+
const ffmpegExe = getFFmpegPath();
|
|
826
|
+
const carpeta = path.dirname(ffmpegExe) + path.sep;
|
|
827
|
+
LUNA.setBinariosFromFolder(carpeta);
|
|
828
|
+
|
|
829
|
+
let _lunaVersion = null;
|
|
830
|
+
function obtenerVersionLUNA() {
|
|
831
|
+
if (_lunaVersion !== null) return _lunaVersion;
|
|
832
|
+
|
|
833
|
+
try {
|
|
834
|
+
if (typeof LUNA.LUNAVersion === 'function') {
|
|
835
|
+
const v = LUNA.LUNAVersion();
|
|
836
|
+
_lunaVersion = (v && typeof v === 'object') ? (v.versionLUNA || null) : (typeof v === 'string' ? v : null);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
if (!_lunaVersion) {
|
|
840
|
+
const meta = typeof LUNA.infoDBMetadata === 'function' ? LUNA.infoDBMetadata() : null;
|
|
841
|
+
if (meta && meta.versionLUNA) _lunaVersion = meta.versionLUNA;
|
|
842
|
+
}
|
|
843
|
+
} catch (e) {
|
|
844
|
+
console.warn('[CINE] No se pudo obtener la versión de LUNA:', e.message);
|
|
845
|
+
}
|
|
726
846
|
|
|
727
|
-
|
|
728
|
-
|
|
847
|
+
if (!_lunaVersion) _lunaVersion = 'unknown';
|
|
848
|
+
return _lunaVersion;
|
|
849
|
+
}
|
|
729
850
|
```
|
|
730
851
|
|
|
852
|
+
### 6) Qué devuelve LUNA
|
|
853
|
+
|
|
854
|
+
La salida típica es este formato:
|
|
855
|
+
|
|
856
|
+
```js
|
|
857
|
+
{
|
|
858
|
+
exito: true,
|
|
859
|
+
salida: {
|
|
860
|
+
nombreOriginal: 'La Novia - 107 [15-10-2026].mp4',
|
|
861
|
+
nombreLimpio: 'La Novia',
|
|
862
|
+
tituloLimpio: null,
|
|
863
|
+
fechaExtra: '15-10-2026',
|
|
864
|
+
ruta: 'D:\\ANIME\\Thunder 3 [Temp 1] [2026] [Caps 12]\\La Novia - 107 [15-10-2026].mp4',
|
|
865
|
+
info: {
|
|
866
|
+
nombreLimpio: 'La Novia',
|
|
867
|
+
serie: 'La Novia',
|
|
868
|
+
temporada: 1,
|
|
869
|
+
capituloNumero: 107,
|
|
870
|
+
anio: null,
|
|
871
|
+
extension: 'mp4',
|
|
872
|
+
tipoArchivo: 'video',
|
|
873
|
+
categoriaGanadora: 'serie',
|
|
874
|
+
DBmetadata: {
|
|
875
|
+
categoriaDB: 'novela',
|
|
876
|
+
nombreOficial: 'La Novia'
|
|
877
|
+
}
|
|
878
|
+
},
|
|
879
|
+
extension: 'mp4',
|
|
880
|
+
tipoArchivo: 'video',
|
|
881
|
+
duracion: 1447.26,
|
|
882
|
+
esCC: 'esNOVELA'
|
|
883
|
+
},
|
|
884
|
+
errores: [],
|
|
885
|
+
meta: {
|
|
886
|
+
inicio: 172...,
|
|
887
|
+
fin: 172...,
|
|
888
|
+
duracionMs: 120
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
```
|
|
892
|
+
|
|
893
|
+
> LUNA acepta `string`, `objeto de configuración`, `array de strings` y `array de configuraciones`. Siempre devuelve un objeto con `exito`, `salida` y `errores`.
|
|
894
|
+
|
|
731
895
|
---
|
|
732
896
|
|
|
733
897
|
## 📦 Instalación y dependencias
|
|
@@ -821,6 +985,10 @@ Exatlon Mexico [T10] - [TX] [037].mp4 | esPROGRAM| 2530 | tr
|
|
|
821
985
|
|
|
822
986
|
---
|
|
823
987
|
|
|
988
|
+
<p align="center">
|
|
989
|
+
<img src="./assets/3.png" width="100%">
|
|
990
|
+
</p>
|
|
991
|
+
|
|
824
992
|
## ⚠️ Limitaciones conocidas
|
|
825
993
|
|
|
826
994
|
1. **`ProcesosCore.js` tiene un `setTimeout` de prueba** que ejecuta ffmpeg sobre un archivo hardcodeado. **Eliminar en producción.**
|
|
@@ -841,7 +1009,7 @@ Exatlon Mexico [T10] - [TX] [037].mp4 | esPROGRAM| 2530 | tr
|
|
|
841
1009
|
- **Autor:** Abraham de Jesús Piñirí Megret
|
|
842
1010
|
- **Agradecimientos:** DeepSeek
|
|
843
1011
|
- **Licencia:** MIT
|
|
844
|
-
- **Versión LUNA:** `1.
|
|
1012
|
+
- **Versión LUNA:** `1.2.1`
|
|
845
1013
|
- **Versión DB:** `1.0.0`
|
|
846
1014
|
- **Fecha DB:** `2026-10-19`
|
|
847
1015
|
|
|
@@ -868,6 +1036,8 @@ Exatlon Mexico [T10] - [TX] [037].mp4 | esPROGRAM| 2530 | tr
|
|
|
868
1036
|
|
|
869
1037
|
Esta versión trae mejoras importantes en cómo LUNA limpia y estructura los nombres de los archivos, separando la información de la obra de la información del capítulo.
|
|
870
1038
|
|
|
1039
|
+
> Si estás leyendo esto desde el README actual, ten en cuenta que la versión activa del proyecto es 1.2.1. La sección de v1.1.0 queda como referencia histórica del proyecto, no como versión actual.
|
|
1040
|
+
|
|
871
1041
|
### 1. Nuevo campo `tituloLimpio`
|
|
872
1042
|
Ahora LUNA es capaz de separar el nombre base (serie o película) de su título de capítulo/saga, usando el guion espaciado (` - `) como separador universal.
|
|
873
1043
|
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="es">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>LUNA · Análisis</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #f8fafc;
|
|
10
|
+
--surface: #ffffff;
|
|
11
|
+
--text: #0f172a;
|
|
12
|
+
--muted: #64748b;
|
|
13
|
+
--accent: #3b82f6;
|
|
14
|
+
--border: #e2e8f0;
|
|
15
|
+
--success: #10b981;
|
|
16
|
+
--warning: #f59e0b;
|
|
17
|
+
--danger: #ef4444;
|
|
18
|
+
}
|
|
19
|
+
* { box-sizing: border-box; margin: 0; }
|
|
20
|
+
body { font-family: 'Inter', system-ui, sans-serif; background: var(--bg); color: var(--text); padding: 40px 20px; }
|
|
21
|
+
.app { max-width: 1200px; margin: 0 auto; }
|
|
22
|
+
|
|
23
|
+
/* Header & Inputs */
|
|
24
|
+
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 32px; }
|
|
25
|
+
.brand h1 { font-size: 1.8rem; font-weight: 800; }
|
|
26
|
+
.brand p { color: var(--muted); font-size: 0.9rem; }
|
|
27
|
+
|
|
28
|
+
.controls { background: var(--surface); padding: 24px; border-radius: 16px; border: 1px solid var(--border); margin-bottom: 32px; }
|
|
29
|
+
.input-row { display: flex; gap: 12px; margin-bottom: 12px; }
|
|
30
|
+
.input-row input { flex: 1; padding: 12px; border: 1px solid var(--border); border-radius: 8px; }
|
|
31
|
+
.btn { padding: 10px 20px; border: none; border-radius: 8px; font-weight: 600; cursor: pointer; }
|
|
32
|
+
.btn-primary { background: var(--accent); color: white; }
|
|
33
|
+
.btn-secondary { background: #f1f5f9; color: var(--text); }
|
|
34
|
+
|
|
35
|
+
/* Grid & Cards */
|
|
36
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; }
|
|
37
|
+
.card {
|
|
38
|
+
background: var(--surface); border-radius: 16px; border: 1px solid var(--border);
|
|
39
|
+
padding: 20px; display: flex; flex-direction: column; gap: 16px;
|
|
40
|
+
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.05);
|
|
41
|
+
}
|
|
42
|
+
.card-header { display: flex; flex-direction: column; gap: 4px; }
|
|
43
|
+
.card-cat { font-size: 0.75rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--accent); }
|
|
44
|
+
.card-title { font-size: 1.2rem; font-weight: 700; line-height: 1.3; }
|
|
45
|
+
.card-subtitle { font-size: 0.9rem; color: var(--muted); font-weight: 500; }
|
|
46
|
+
|
|
47
|
+
.card-body { display: flex; flex-wrap: wrap; gap: 12px; border-top: 1px solid var(--border); padding-top: 16px; }
|
|
48
|
+
.data-item { display: flex; flex-direction: column; gap: 2px; }
|
|
49
|
+
.data-label { font-size: 0.7rem; font-weight: 600; color: var(--muted); text-transform: uppercase; }
|
|
50
|
+
.data-value { font-size: 0.9rem; font-weight: 500; }
|
|
51
|
+
|
|
52
|
+
.card-footer { font-size: 0.75rem; color: var(--muted); background: #f8fafc; padding: 8px; border-radius: 6px; word-break: break-all; margin-top: auto; }
|
|
53
|
+
|
|
54
|
+
.badge { padding: 4px 8px; border-radius: 6px; font-size: 0.7rem; font-weight: 700; }
|
|
55
|
+
.badge.ok { background: #d1fae5; color: #065f46; }
|
|
56
|
+
.badge.warn { background: #fef3c7; color: #92400e; }
|
|
57
|
+
.badge.err { background: #fee2e2; color: #991b1b; }
|
|
58
|
+
</style>
|
|
59
|
+
</head>
|
|
60
|
+
<body>
|
|
61
|
+
<div class="app">
|
|
62
|
+
<div class="header">
|
|
63
|
+
<div class="brand">
|
|
64
|
+
<h1>LUNA</h1>
|
|
65
|
+
<p>Clasificador inteligente de archivos</p>
|
|
66
|
+
</div>
|
|
67
|
+
<button class="btn btn-primary" id="runBtn">Ejecutar 20 casos</button>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div class="controls">
|
|
71
|
+
<div id="customInputs"></div>
|
|
72
|
+
<div style="display:flex; gap:12px; margin-top:12px;">
|
|
73
|
+
<button class="btn btn-secondary" id="addRowBtn">+ Añadir ruta</button>
|
|
74
|
+
<button class="btn btn-primary" id="runCustomBtn">Analizar rutas</button>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div class="grid" id="resultsGrid">
|
|
79
|
+
<div style="grid-column: 1/-1; text-align:center; color:var(--muted);">Esperando análisis...</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<script>
|
|
84
|
+
const resultsGrid = document.getElementById('resultsGrid');
|
|
85
|
+
const customInputs = document.getElementById('customInputs');
|
|
86
|
+
|
|
87
|
+
// --- Renderizado de Tarjeta ---
|
|
88
|
+
function renderCard(item) {
|
|
89
|
+
const badge = item.estado === 'ok' ? 'ok' : item.estado === 'warning' ? 'warn' : 'err';
|
|
90
|
+
const badgeText = item.estado === 'ok' ? 'OK' : item.estado === 'warning' ? 'Aviso' : 'Error';
|
|
91
|
+
|
|
92
|
+
// Construir HTML de detalles (solo si hay datos)
|
|
93
|
+
let detallesHtml = '';
|
|
94
|
+
if (item.detalles && item.detalles.length > 0) {
|
|
95
|
+
detallesHtml = item.detalles.map(d => `
|
|
96
|
+
<div class="data-item">
|
|
97
|
+
<span class="data-label">${d.etiqueta}</span>
|
|
98
|
+
<span class="data-value">${d.valor}</span>
|
|
99
|
+
</div>
|
|
100
|
+
`).join('');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Subtítulo opcional
|
|
104
|
+
const subtituloHtml = item.subtitulo ? `<div class="card-subtitle">${item.subtitulo}</div>` : '';
|
|
105
|
+
|
|
106
|
+
// Título (si es null, usamos el nombre del archivo)
|
|
107
|
+
const titulo = item.titulo || 'Archivo sin nombre claro';
|
|
108
|
+
|
|
109
|
+
return `
|
|
110
|
+
<div class="card">
|
|
111
|
+
<div class="card-header">
|
|
112
|
+
<div style="display:flex; justify-content:space-between;">
|
|
113
|
+
<span class="card-cat">${item.categoriaHumana || 'Desconocido'}</span>
|
|
114
|
+
<span class="badge ${badge}">${badgeText}</span>
|
|
115
|
+
</div>
|
|
116
|
+
<div class="card-title">${titulo}</div>
|
|
117
|
+
${subtituloHtml}
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
${detallesHtml ? `<div class="card-body">${detallesHtml}</div>` : ''}
|
|
121
|
+
|
|
122
|
+
<div class="card-footer">${item.ruta}</div>
|
|
123
|
+
${item.error ? `<div style="color:var(--danger); font-size:0.8rem; margin-top:8px;">${item.error}</div>` : ''}
|
|
124
|
+
</div>
|
|
125
|
+
`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// --- Lógica de la UI ---
|
|
129
|
+
async function runSuite() {
|
|
130
|
+
resultsGrid.innerHTML = '<div style="grid-column:1/-1; text-align:center;">Analizando...</div>';
|
|
131
|
+
try {
|
|
132
|
+
const res = await fetch('/api/run');
|
|
133
|
+
const data = await res.json();
|
|
134
|
+
if (!data.resultados) throw new Error('Sin datos');
|
|
135
|
+
resultsGrid.innerHTML = data.resultados.map(renderCard).join('');
|
|
136
|
+
} catch (e) {
|
|
137
|
+
resultsGrid.innerHTML = `<div style="color:var(--danger);">Error: ${e.message}</div>`;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function runCustom() {
|
|
142
|
+
const inputs = customInputs.querySelectorAll('input');
|
|
143
|
+
const casos = Array.from(inputs).map(i => ({ ruta: i.value.trim() })).filter(c => c.ruta);
|
|
144
|
+
if (casos.length === 0) return alert('Añade alguna ruta');
|
|
145
|
+
|
|
146
|
+
resultsGrid.innerHTML = '<div style="grid-column:1/-1; text-align:center;">Analizando...</div>';
|
|
147
|
+
try {
|
|
148
|
+
const res = await fetch('/api/run-custom', {
|
|
149
|
+
method: 'POST',
|
|
150
|
+
headers: { 'Content-Type': 'application/json' },
|
|
151
|
+
body: JSON.stringify({ casos })
|
|
152
|
+
});
|
|
153
|
+
const data = await res.json();
|
|
154
|
+
if (!data.resultados) throw new Error('Sin datos');
|
|
155
|
+
resultsGrid.innerHTML = data.resultados.map(renderCard).join('');
|
|
156
|
+
} catch (e) {
|
|
157
|
+
resultsGrid.innerHTML = `<div style="color:var(--danger);">Error: ${e.message}</div>`;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// --- Inputs Dinámicos ---
|
|
162
|
+
function addInput() {
|
|
163
|
+
const div = document.createElement('div');
|
|
164
|
+
div.className = 'input-row';
|
|
165
|
+
div.innerHTML = `<input type="text" placeholder="Ej: D:\\SERIES\\Silo\\Silo - 204.mkv"><button class="btn btn-secondary" onclick="this.parentElement.remove()">×</button>`;
|
|
166
|
+
customInputs.appendChild(div);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
addInput(); addInput(); addInput();
|
|
170
|
+
document.getElementById('addRowBtn').onclick = addInput;
|
|
171
|
+
document.getElementById('runBtn').onclick = runSuite;
|
|
172
|
+
document.getElementById('runCustomBtn').onclick = runCustom;
|
|
173
|
+
</script>
|
|
174
|
+
</body>
|
|
175
|
+
</html>
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
const http = require('http');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { getFFmpegPath } = require('../Servicios/ResolverPathffmpeg.js');
|
|
5
|
+
const LUNA = require('../LUNA_clasificador.js');
|
|
6
|
+
|
|
7
|
+
const PORT = 3466;
|
|
8
|
+
const HTML_PATH = path.join(__dirname, 'luna_dashboard.html');
|
|
9
|
+
|
|
10
|
+
//http://localhost:3466/
|
|
11
|
+
|
|
12
|
+
// Solo id + ruta. El nombre se deriva del nombre de archivo.
|
|
13
|
+
function getRandomizedCases() {
|
|
14
|
+
const rutas = [
|
|
15
|
+
'D:\\ANIME\\Thunder 3 [Temp 1] [2026] [Caps 12]\\La Novia - 107 [15-10-2026] [Corregido].mp4',
|
|
16
|
+
'D:\\ANIME\\Thunder 3 [Temp 1] [2026] [Caps 12]\\Dueña de Mi - 139 [16-10-2026].mp4',
|
|
17
|
+
'D:\\ANIME\\Peliculas\\John Wick 4.mkv',
|
|
18
|
+
'D:\\ANIME\\Peliculas\\Harry Potter La Piedra Filosofal.mkv',
|
|
19
|
+
'D:\\ANIME\\Code Geass 2\\[D-SnF] Code Geass - Lelouch of the Rebellion R2 - 11 [BD 1080p].mkv',
|
|
20
|
+
'D:\\TV\\The Bear\\The Bear - S02E07.mkv',
|
|
21
|
+
'D:\\SERIES\\Silo\\Season 2\\Silo - 204.mkv',
|
|
22
|
+
'D:\\PROGRAMAS\\MasterChef\\MasterChef - 008 [HD].mp4',
|
|
23
|
+
'D:\\PERSONAL\\VID_20240101_214500.mp4',
|
|
24
|
+
'D:\\VIDEOS\\clip_demo.mp4',
|
|
25
|
+
'D:\\SERIES\\One Piece\\One Piece - 1034.mp4',
|
|
26
|
+
'D:\\ANIME\\Shingeki no Kyojin\\Shingeki no Kyojin - 012 [1080p].mkv',
|
|
27
|
+
'D:\\SHOWS\\La Voz\\La Voz - 010 [TV].mp4',
|
|
28
|
+
'D:\\PELICULAS\\Dune Part Two (2024).mkv',
|
|
29
|
+
'D:\\NOVELAS\\La Promesa 18.mp4',
|
|
30
|
+
'D:\\MISC\\sample_01.mp4',
|
|
31
|
+
'D:\\SECCION CUBA\\Novelas\\La Vida Que Me Robaste 21.mp4',
|
|
32
|
+
'D:\\FOTOS\\WhatsApp Video 2024-10-10 at 20.00.00.mp4',
|
|
33
|
+
'D:\\Anime\\Blue Lock\\Blue Lock - S01E03 [Sub].mkv',
|
|
34
|
+
'D:\\Cine\\The Batman [4K UHD] [2022].mkv'
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
return rutas
|
|
38
|
+
.map((ruta, i) => ({ id: `case-${String(i + 1).padStart(2, '0')}`, ruta }))
|
|
39
|
+
.sort(() => Math.random() - 0.5)
|
|
40
|
+
.slice(0, 20);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function nombreDesdeRuta(ruta) {
|
|
44
|
+
if (!ruta) return '—';
|
|
45
|
+
const base = ruta.split(/[\\/]/).pop() || ruta;
|
|
46
|
+
return base.replace(/\.[^.]+$/, '');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Normaliza la salida de LUNA a un objeto plano y consistente.
|
|
50
|
+
// No inventa datos: solo propaga lo que LUNA devuelve.
|
|
51
|
+
function serializeResult(item, data) {
|
|
52
|
+
const base = {
|
|
53
|
+
id: item.id,
|
|
54
|
+
ruta: item.ruta,
|
|
55
|
+
estado: 'ok',
|
|
56
|
+
titulo: null,
|
|
57
|
+
subtitulo: null,
|
|
58
|
+
categoriaHumana: null,
|
|
59
|
+
detalles: [], // Solo datos reales
|
|
60
|
+
error: null
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
if (!data || !data.salida) {
|
|
64
|
+
base.estado = 'error';
|
|
65
|
+
base.error = 'No hubo salida de LUNA';
|
|
66
|
+
return base;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const s = data.salida || {};
|
|
70
|
+
const info = s.info || {};
|
|
71
|
+
|
|
72
|
+
// Traducir la categoría técnica a lenguaje humano
|
|
73
|
+
const catMap = {
|
|
74
|
+
'esPELI': 'Película',
|
|
75
|
+
'esSERIE': 'Serie',
|
|
76
|
+
'esPROGRAM': 'Programa',
|
|
77
|
+
'esNOVELA': 'Novela',
|
|
78
|
+
'esVARIADO': 'Variado'
|
|
79
|
+
};
|
|
80
|
+
base.categoriaHumana = catMap[s.esCC] || null;
|
|
81
|
+
|
|
82
|
+
// El título principal: nombreLimpio (ej: "La Vida Que Me Robaste")
|
|
83
|
+
base.titulo = s.nombreLimpio || info.nombreLimpio || null;
|
|
84
|
+
|
|
85
|
+
// El subtítulo: Puede ser el título del episodio O la temporada/capítulo
|
|
86
|
+
let subParts = [];
|
|
87
|
+
if (info.tituloLimpio) subParts.push(info.tituloLimpio); // Ej: "Lelouch of the Rebellion"
|
|
88
|
+
|
|
89
|
+
if (info.temporada) subParts.push(`Temporada ${info.temporada}`);
|
|
90
|
+
if (info.capituloNumero) subParts.push(`Capítulo ${info.capituloNumero}`);
|
|
91
|
+
|
|
92
|
+
if (subParts.length > 0) {
|
|
93
|
+
base.subtitulo = subParts.join(' · ');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Detalles: Solo lo que existe y es útil
|
|
97
|
+
if (info.serie) base.detalles.push({ etiqueta: 'Serie', valor: info.serie });
|
|
98
|
+
if (info.anio) base.detalles.push({ etiqueta: 'Año', valor: info.anio });
|
|
99
|
+
if (s.fechaExtra) base.detalles.push({ etiqueta: 'Fecha', valor: s.fechaExtra });
|
|
100
|
+
|
|
101
|
+
if (s.duracion && s.duracion > 0) {
|
|
102
|
+
const mins = Math.floor(s.duracion / 60);
|
|
103
|
+
base.detalles.push({ etiqueta: 'Duración', valor: `${mins} min` });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (s.extension) base.detalles.push({ etiqueta: 'Formato', valor: s.extension.toUpperCase() });
|
|
107
|
+
if (s.esCC === 'esSERIE' || s.esCC === 'esNOVELA') {
|
|
108
|
+
if (info.DBmetadata && info.DBmetadata.nombreOficial) {
|
|
109
|
+
base.detalles.push({ etiqueta: 'Nombre Oficial', valor: info.DBmetadata.nombreOficial });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (data.errores && data.errores.length) {
|
|
114
|
+
base.estado = 'warning';
|
|
115
|
+
base.error = data.errores[0].mensaje || 'Hay errores';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return base;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function ejecutarCasos(casos) {
|
|
122
|
+
const resultados = [];
|
|
123
|
+
|
|
124
|
+
for (const caso of casos) {
|
|
125
|
+
try {
|
|
126
|
+
const resultado = await LUNA.LUNA(caso.ruta);
|
|
127
|
+
resultados.push(serializeResult(caso, resultado));
|
|
128
|
+
} catch (error) {
|
|
129
|
+
resultados.push({
|
|
130
|
+
id: caso.id,
|
|
131
|
+
ruta: caso.ruta,
|
|
132
|
+
estado: 'error',
|
|
133
|
+
categoria: null,
|
|
134
|
+
esCC: null,
|
|
135
|
+
nombreLimpio: null,
|
|
136
|
+
extension: null,
|
|
137
|
+
tipoArchivo: null,
|
|
138
|
+
duracion: null,
|
|
139
|
+
fechaExtra: null,
|
|
140
|
+
serie: null,
|
|
141
|
+
temporada: null,
|
|
142
|
+
capitulo: null,
|
|
143
|
+
pelicula: null,
|
|
144
|
+
programa: null,
|
|
145
|
+
novela: null,
|
|
146
|
+
error: error.message || 'Error desconocido'
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
ejecutado: new Date().toISOString(),
|
|
153
|
+
total: resultados.length,
|
|
154
|
+
ok: resultados.filter(r => r.estado === 'ok').length,
|
|
155
|
+
warning: resultados.filter(r => r.estado === 'warning').length,
|
|
156
|
+
error: resultados.filter(r => r.estado === 'error').length,
|
|
157
|
+
resultados
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function ejecutarSuite() {
|
|
162
|
+
return ejecutarCasos(getRandomizedCases());
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------
|
|
166
|
+
// FFmpeg: se intenta cargar, pero si falla LUNA sigue funcionando
|
|
167
|
+
// por nombre (sin duración). No se lanza excepción hacia arriba.
|
|
168
|
+
// ---------------------------------------------------------------
|
|
169
|
+
function configurarFFmpeg() {
|
|
170
|
+
try {
|
|
171
|
+
//const ffmpegExe = getFFmpegPath();
|
|
172
|
+
//const carpeta = path.dirname(ffmpegExe) + path.sep;
|
|
173
|
+
LUNA.setBinariosFromFolder('D:\\NAtifevierddd\\ffmpeg-static');
|
|
174
|
+
console.log('[dashboard] FFmpeg configurado correctamente.');
|
|
175
|
+
return true;
|
|
176
|
+
} catch (e) {
|
|
177
|
+
console.warn('[dashboard] FFmpeg no disponible, LUNA seguirá funcionando por nombre:', e.message);
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
configurarFFmpeg();
|
|
183
|
+
|
|
184
|
+
function leerBodyJSON(req) {
|
|
185
|
+
return new Promise((resolve, reject) => {
|
|
186
|
+
let body = '';
|
|
187
|
+
req.on('data', chunk => {
|
|
188
|
+
body += chunk;
|
|
189
|
+
if (body.length > 1e6) {
|
|
190
|
+
reject(new Error('Cuerpo demasiado grande'));
|
|
191
|
+
req.destroy();
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
req.on('end', () => {
|
|
195
|
+
if (!body) return resolve({});
|
|
196
|
+
try { resolve(JSON.parse(body)); }
|
|
197
|
+
catch (e) { reject(new Error('JSON inválido')); }
|
|
198
|
+
});
|
|
199
|
+
req.on('error', reject);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const server = http.createServer(async (req, res) => {
|
|
204
|
+
const url = new URL(req.url, 'http://localhost');
|
|
205
|
+
|
|
206
|
+
// ---- Suite de 20 casos fijos ----
|
|
207
|
+
if (req.method === 'GET' && url.pathname === '/api/run') {
|
|
208
|
+
try {
|
|
209
|
+
const payload = await ejecutarSuite();
|
|
210
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
211
|
+
res.end(JSON.stringify(payload));
|
|
212
|
+
} catch (error) {
|
|
213
|
+
res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
214
|
+
res.end(JSON.stringify({ error: error.message }));
|
|
215
|
+
}
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ---- Casos personalizados desde el HTML (POST) ----
|
|
220
|
+
if (req.method === 'POST' && url.pathname === '/api/run-custom') {
|
|
221
|
+
try {
|
|
222
|
+
const body = await leerBodyJSON(req);
|
|
223
|
+
const casos = Array.isArray(body.casos) ? body.casos : [];
|
|
224
|
+
|
|
225
|
+
const limpios = casos
|
|
226
|
+
.map((c, i) => {
|
|
227
|
+
const ruta = typeof c?.ruta === 'string' ? c.ruta.trim() : '';
|
|
228
|
+
if (!ruta) return null;
|
|
229
|
+
return { id: `custom-${i + 1}`, ruta };
|
|
230
|
+
})
|
|
231
|
+
.filter(Boolean);
|
|
232
|
+
|
|
233
|
+
if (limpios.length === 0) {
|
|
234
|
+
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
235
|
+
res.end(JSON.stringify({ error: 'No se recibieron rutas válidas.' }));
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const payload = await ejecutarCasos(limpios);
|
|
240
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
241
|
+
res.end(JSON.stringify(payload));
|
|
242
|
+
} catch (error) {
|
|
243
|
+
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
244
|
+
res.end(JSON.stringify({ error: error.message }));
|
|
245
|
+
}
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ---- Metadata de la DB ----
|
|
250
|
+
if (req.method === 'GET' && url.pathname === '/api/meta') {
|
|
251
|
+
try {
|
|
252
|
+
const meta = typeof LUNA.infoDBMetadata === 'function' ? LUNA.infoDBMetadata() : null;
|
|
253
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
254
|
+
res.end(JSON.stringify({ ok: true, meta }));
|
|
255
|
+
} catch (error) {
|
|
256
|
+
res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
257
|
+
res.end(JSON.stringify({ ok: false, error: error.message }));
|
|
258
|
+
}
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ---- Health ----
|
|
263
|
+
if (req.method === 'GET' && url.pathname === '/health') {
|
|
264
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
265
|
+
res.end(JSON.stringify({ ok: true, service: 'luna-dashboard', port: PORT }));
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ---- HTML ----
|
|
270
|
+
if (req.method === 'GET') {
|
|
271
|
+
fs.readFile(HTML_PATH, 'utf8', (err, html) => {
|
|
272
|
+
if (err) {
|
|
273
|
+
res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
274
|
+
res.end('No se pudo cargar la vista HTML.');
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
278
|
+
res.end(html);
|
|
279
|
+
});
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
284
|
+
res.end('404');
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
server.listen(PORT, () => {
|
|
288
|
+
console.log(`\n====================================================`);
|
|
289
|
+
console.log(`LUNA dashboard activo en: http://localhost:${PORT}`);
|
|
290
|
+
console.log(`Prueba visual en: http://localhost:${PORT}`);
|
|
291
|
+
console.log(`====================================================\n`);
|
|
292
|
+
});
|