jalarab

Tdarr_Plugin_Translate_Subs_To_Spanish_DeepSeek_v3.js

Dec 29th, 2025
258
0
Never
9
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. /**
  2.  * Tdarr_Plugin_Translate_Subs_To_Spanish_DeepSeek_v3
  3.  * ===================================================
  4.  *
  5.  * VERSIÓN 3.0 - MEJORADA PARA IGUALAR EL SCRIPT PYTHON
  6.  *
  7.  * Mejoras vs v2.1:
  8.  * ✅ Soporte multi-idioma (FR → PT → IT → EN)
  9.  * ✅ Conversión automática ASS/SSA → SRT
  10.  * ✅ Detección y descarte de subtítulos FORCED
  11.  * ✅ Detección automática de HI (solo limpia si es necesario)
  12.  * ✅ Traducción concurrente optimizada
  13.  * ✅ Verificación de completitud de traducciones
  14.  * ✅ Manejo robusto de errores con reintentos
  15.  *
  16.  * Flujo completo:
  17.  * 1. Verificar español externo → omitir
  18.  * 2. Verificar español embebido → extraer (+ limpia HI si aplica)
  19.  * 3. Buscar externos (FR/PT/IT/EN) → traducir (+ limpia HI si aplica)
  20.  * 4. Buscar embebidos (FR/PT/IT/EN) → extraer y traducir (+ limpia HI si aplica)
  21.  */
  22.  
  23. const { execSync } = require("child_process");
  24. const fs = require("fs");
  25. const path = require("path");
  26.  
  27. // ============================================================================
  28. // METADATA DEL PLUGIN
  29. // ============================================================================
  30.  
  31. const details = () => ({
  32.   id: "Tdarr_Plugin_Translate_Subs_To_Spanish_DeepSeek_v3",
  33.   Stage: "Pre-processing",
  34.   Name: "Translate Subtitles to Spanish (Multi-Lang + HI Detection) v3",
  35.   Type: "Video",
  36.   Operation: "Transcode",
  37.   Description:
  38.     "Busca subtítulos en español (externos/embebidos). Si no existen, busca en FR/PT/IT/EN (externos/embebidos), traduce con DeepSeek, detecta automáticamente si es HI y solo limpia si es necesario. Soporta ASS/SSA. Traducción concurrente optimizada.",
  39.   Version: "3.0",
  40.   Tags: "pre-processing,subtitle,translation,deepseek,spanish,multi-language,hi-detection",
  41.   Inputs: [
  42.     {
  43.       name: "api_key",
  44.       type: "string",
  45.       defaultValue: "",
  46.       inputUI: { type: "text", password: true },
  47.       tooltip: "API Key de DeepSeek (https://platform.deepseek.com)"
  48.     },
  49.     {
  50.       name: "batch_size",
  51.       type: "number",
  52.       defaultValue: 50,
  53.       inputUI: { type: "text" },
  54.       tooltip: "Número de subtítulos por lote (default: 50)"
  55.     },
  56.     {
  57.       name: "max_concurrent",
  58.       type: "number",
  59.       defaultValue: 3,
  60.       inputUI: { type: "text" },
  61.       tooltip: "Número de lotes concurrentes (default: 3)"
  62.     },
  63.     {
  64.       name: "clean_hi",
  65.       type: "boolean",
  66.       defaultValue: true,
  67.       inputUI: { type: "switch" },
  68.       tooltip: "Limpiar HI automáticamente si se detecta (default: true)"
  69.     }
  70.   ]
  71. });
  72.  
  73. // ============================================================================
  74. // CONFIGURACIÓN Y CONSTANTES
  75. // ============================================================================
  76.  
  77. // Prioridad de idiomas: Lenguas romances primero, luego inglés
  78. const LANG_PRIORITY = [
  79.   { code: "fr", names: ["fr", "fre", "french", "fra", "français"] },
  80.   { code: "pt", names: ["pt", "por", "portuguese", "pt-br", "português"] },
  81.   { code: "it", names: ["it", "ita", "italian", "italiano"] },
  82.   { code: "en", names: ["en", "eng", "english"] }
  83. ];
  84.  
  85. const FORCED_KEYWORDS = ["forced", "signs", "signs only", "signs & songs", "forzado"];
  86.  
  87. // ============================================================================
  88. // UTILIDADES BÁSICAS
  89. // ============================================================================
  90.  
  91. function runCommand(cmd) {
  92.   try {
  93.     return execSync(cmd, { encoding: "utf-8" }).trim();
  94.   } catch (err) {
  95.     return null;
  96.   }
  97. }
  98.  
  99. function sleep(ms) {
  100.   return new Promise(resolve => setTimeout(resolve, ms));
  101. }
  102.  
  103. // ============================================================================
  104. // DETECCIÓN DE SUBTÍTULOS HI
  105. // ============================================================================
  106.  
  107. function detectIfHI(srtContent) {
  108.   /**
  109.    * Analiza el contenido SRT y determina si es HI.
  110.    * Retorna: { isHI: boolean, score: number, stats: object }
  111.    */
  112.   const blocks = parseSrtToBlocks(srtContent);
  113.   if (blocks.length === 0) return { isHI: false, score: 0, stats: {} };
  114.  
  115.   let bracketsCount = 0;
  116.   let soundParensCount = 0;
  117.   let musicSymbolsCount = 0;
  118.  
  119.   const bracketPattern = /\[.*?\]/;
  120.   const musicSymbols = ["♪", "♫", "~"];
  121.   const soundParensPatterns = [
  122.     /\(música\)/i, /\(music\)/i, /\(musique\)/i,
  123.     /\(risa[s]?\)/i, /\(laugh[s]?\)/i, /\(rire[s]?\)/i,
  124.     /\(aplauso[s]?\)/i, /\(applause\)/i, /\(applaudissement[s]?\)/i,
  125.     /\(suspiro\)/i, /\(sigh\)/i, /\(soupir\)/i
  126.   ];
  127.  
  128.   blocks.forEach(block => {
  129.     const text = block.textLines.join(" ").toLowerCase();
  130.    
  131.     if (bracketPattern.test(text)) bracketsCount++;
  132.     if (musicSymbols.some(s => text.includes(s))) musicSymbolsCount++;
  133.     if (soundParensPatterns.some(p => p.test(text))) soundParensCount++;
  134.   });
  135.  
  136.   const total = blocks.length;
  137.   const pctBrackets = (bracketsCount / total) * 100;
  138.   const pctSoundParens = (soundParensCount / total) * 100;
  139.   const pctMusicSymbols = (musicSymbolsCount / total) * 100;
  140.  
  141.   // Calcular score HI
  142.   const score = (
  143.     pctBrackets * 2.0 +
  144.     pctSoundParens * 1.5 +
  145.     pctMusicSymbols * 1.5
  146.   ) / 5.0;
  147.  
  148.   const isHI = score >= 15.0;
  149.  
  150.   return {
  151.     isHI,
  152.     score: Math.round(score * 10) / 10,
  153.     stats: {
  154.       total,
  155.       bracketsCount,
  156.       soundParensCount,
  157.       pctBrackets: Math.round(pctBrackets * 10) / 10,
  158.       pctSoundParens: Math.round(pctSoundParens * 10) / 10,
  159.       score: Math.round(score * 10) / 10
  160.     }
  161.   };
  162. }
  163.  
  164. function cleanHIBasic(srtContent) {
  165.   /**
  166.    * Limpieza básica de HI (conservadora).
  167.    * Solo elimina bloques que son ÚNICAMENTE HI.
  168.    */
  169.   const blocks = parseSrtToBlocks(srtContent);
  170.   const cleaned = [];
  171.  
  172.   const onlyHIPatterns = [
  173.     /^\[.*\]$/,
  174.     /^♪.*♪$/,
  175.     /^\(música\)$/i,
  176.     /^\(musique\)$/i,
  177.     /^\(music\)$/i
  178.   ];
  179.  
  180.   blocks.forEach(block => {
  181.     const text = block.textLines.join("\n").trim();
  182.     const isOnlyHI = onlyHIPatterns.some(p => p.test(text));
  183.    
  184.     if (!isOnlyHI) {
  185.       // Limpiar marcadores pero mantener texto
  186.       let cleanedText = text
  187.         .replace(/\[.*?\]/g, "")
  188.         .replace(/♪/g, "")
  189.         .replace(/♫/g, "")
  190.         .replace(/\(música\)/gi, "")
  191.         .replace(/\(musique\)/gi, "")
  192.         .replace(/\(music\)/gi, "")
  193.         .replace(/\s+/g, " ")
  194.         .trim();
  195.      
  196.       if (cleanedText) {
  197.         cleaned.push({
  198.           id: block.id,
  199.           time: block.time,
  200.           textLines: cleanedText.split("\n")
  201.         });
  202.       }
  203.     }
  204.   });
  205.  
  206.   // Re-numerar
  207.   cleaned.forEach((block, idx) => {
  208.     block.id = (idx + 1).toString();
  209.   });
  210.  
  211.   return cleaned.map(b => `${b.id}\n${b.time}\n${b.textLines.join("\n")}`).join("\n\n");
  212. }
  213.  
  214. // ============================================================================
  215. // BÚSQUEDA Y EXTRACCIÓN DE SUBTÍTULOS
  216. // ============================================================================
  217.  
  218. function findExternalSub(videoPath, langConfig) {
  219.   /**
  220.    * Busca subtítulo externo por idioma.
  221.    * Soporta SRT, ASS, SSA.
  222.    */
  223.   const dir = path.dirname(videoPath);
  224.   const base = path.basename(videoPath, path.extname(videoPath));
  225.   const extensions = ["srt", "ass", "ssa"];
  226.  
  227.   for (const ext of extensions) {
  228.     for (const langName of langConfig.names) {
  229.       const patterns = [
  230.         `${base}.${langName}.${ext}`,
  231.         `${base}.${langName.toUpperCase()}.${ext}`,
  232.         new RegExp(`^${base}.*${langName}.*\\.${ext}$`, "i")
  233.       ];
  234.  
  235.       for (const pattern of patterns) {
  236.         try {
  237.           const files = fs.readdirSync(dir);
  238.           const matches = files.filter(f => {
  239.             if (typeof pattern === "string") {
  240.               return f === pattern;
  241.             } else {
  242.               return pattern.test(f);
  243.             }
  244.           });
  245.  
  246.           if (matches.length > 0) {
  247.             // Filtrar FORCED
  248.             const nonForced = matches.filter(f =>
  249.               !FORCED_KEYWORDS.some(kw => f.toLowerCase().includes(kw))
  250.             );
  251.            
  252.             if (nonForced.length > 0) {
  253.               // Preferir no-SDH
  254.               const nonSDH = nonForced.filter(f => !f.toLowerCase().includes("sdh"));
  255.               const selected = nonSDH.length > 0 ? nonSDH[0] : nonForced[0];
  256.               return path.join(dir, selected);
  257.             }
  258.           }
  259.         } catch (err) {
  260.           continue;
  261.         }
  262.       }
  263.     }
  264.   }
  265.   return null;
  266. }
  267.  
  268. function getSubtitleStreams(videoPath) {
  269.   /**
  270.    * Obtiene información de streams de subtítulos embebidos.
  271.    */
  272.   try {
  273.     const output = runCommand(
  274.       `ffprobe -v quiet -print_format json -show_streams -select_streams s "${videoPath}"`
  275.     );
  276.    
  277.     if (!output) return [];
  278.    
  279.     const parsed = JSON.parse(output);
  280.     const streams = parsed.streams || [];
  281.    
  282.     return streams.map(s => ({
  283.       index: s.index,
  284.       language: (s.tags?.language || "und").toLowerCase(),
  285.       title: (s.tags?.title || "").toLowerCase(),
  286.       forced: s.disposition?.forced === 1
  287.     }));
  288.   } catch (err) {
  289.     return [];
  290.   }
  291. }
  292.  
  293. function isForcedSubtitle(stream) {
  294.   if (stream.forced) return true;
  295.   return FORCED_KEYWORDS.some(kw => stream.title.includes(kw));
  296. }
  297.  
  298. function extractEmbeddedSub(videoPath, streamIndex, outputFile) {
  299.   try {
  300.     runCommand(`ffmpeg -y -i "${videoPath}" -map 0:${streamIndex} "${outputFile}"`);
  301.     return fs.existsSync(outputFile) && fs.statSync(outputFile).size > 0;
  302.   } catch (err) {
  303.     return false;
  304.   }
  305. }
  306.  
  307. function convertToSRT(inputPath, outputPath) {
  308.   /**
  309.    * Convierte ASS/SSA a SRT usando FFmpeg.
  310.    */
  311.   try {
  312.     runCommand(`ffmpeg -y -i "${inputPath}" "${outputPath}"`);
  313.     return fs.existsSync(outputPath) && fs.statSync(outputPath).size > 0;
  314.   } catch (err) {
  315.     return false;
  316.   }
  317. }
  318.  
  319. function countSubtitles(filePath) {
  320.   /**
  321.    * Cuenta subtítulos de forma inteligente.
  322.    * Convierte ASS/SSA temporalmente si es necesario.
  323.    */
  324.   const ext = path.extname(filePath).toLowerCase();
  325.  
  326.   if (ext === ".srt") {
  327.     try {
  328.       const content = fs.readFileSync(filePath, "utf-8");
  329.       return parseSrtToBlocks(content).length;
  330.     } catch {
  331.       return 0;
  332.     }
  333.   } else if (ext === ".ass" || ext === ".ssa") {
  334.     const tempSRT = filePath.replace(/\.(ass|ssa)$/i, ".temp_count.srt");
  335.     try {
  336.       if (convertToSRT(filePath, tempSRT)) {
  337.         const content = fs.readFileSync(tempSRT, "utf-8");
  338.         const count = parseSrtToBlocks(content).length;
  339.         fs.unlinkSync(tempSRT);
  340.         return count;
  341.       }
  342.     } catch {
  343.       try { fs.unlinkSync(tempSRT); } catch {}
  344.     }
  345.   }
  346.   return 0;
  347. }
  348.  
  349. // ============================================================================
  350. // PARSEO Y TRADUCCIÓN
  351. // ============================================================================
  352.  
  353. function parseSrtToBlocks(srtContent) {
  354.   const blocks = [];
  355.   const blockRegex = /(\d+)\s*?\r?\n(\d{2}:\d{2}:\d{2},\d{3})\s*?-->\s*?(\d{2}:\d{2}:\d{2},\d{3})\s*?\r?\n([\s\S]+?)(?=\r?\n\r?\n\d+|\s*$)/g;
  356.   let match;
  357.  
  358.   while ((match = blockRegex.exec(srtContent)) !== null) {
  359.     blocks.push({
  360.       id: match[1],
  361.       time: `${match[2]} --> ${match[3]}`,
  362.       textLines: match[4].trim().split(/\r?\n/)
  363.     });
  364.   }
  365.  
  366.   return blocks;
  367. }
  368.  
  369. async function translateBatch(apiKey, batchBlocks, modelName = "deepseek-chat") {
  370.   const blocks = batchBlocks.map(b =>
  371.     `===BLOCK_ID:${b.id}===\n${b.textLines.join("\n")}`
  372.   ).join("\n\n");
  373.  
  374.   const systemPrompt = "Traduce al español latinoamericano manteniendo EXACTAMENTE el formato:\n===BLOCK_ID:n===\n<texto>\n\nNo alteres los marcadores ===BLOCK_ID:x===. Traduce TODOS los bloques.";
  375.  
  376.   try {
  377.     const response = await fetch("https://api.deepseek.com/v1/chat/completions", {
  378.       method: "POST",
  379.       headers: {
  380.         "Authorization": `Bearer ${apiKey}`,
  381.         "Content-Type": "application/json"
  382.       },
  383.       body: JSON.stringify({
  384.         model: modelName,
  385.         messages: [
  386.           { role: "system", content: systemPrompt },
  387.           { role: "user", content: blocks }
  388.         ],
  389.         temperature: 0.3
  390.       })
  391.     });
  392.  
  393.     if (!response.ok) {
  394.       throw new Error(`API error: ${response.status}`);
  395.     }
  396.  
  397.     const data = await response.json();
  398.     const content = data.choices[0]?.message?.content || "";
  399.    
  400.     // Parsear respuesta
  401.     const pattern = /===BLOCK_ID:(\d+)===\n(.*?)(?=\n\n===BLOCK_ID:|\s*$)/gs;
  402.     const result = {};
  403.     let match;
  404.    
  405.     while ((match = pattern.exec(content)) !== null) {
  406.       result[match[1]] = match[2].trim();
  407.     }
  408.    
  409.     return result;
  410.   } catch (err) {
  411.     throw err;
  412.   }
  413. }
  414.  
  415. async function translateInBatchesConcurrent(blocks, apiKey, batchSize, maxConcurrent) {
  416.   /**
  417.    * Traduce en lotes CONCURRENTES (como Python V3).
  418.    */
  419.   const batches = [];
  420.   for (let i = 0; i < blocks.length; i += batchSize) {
  421.     batches.push(blocks.slice(i, i + batchSize));
  422.   }
  423.  
  424.   const allTranslations = {};
  425.   const errors = [];
  426.  
  427.   // Traducir en grupos de maxConcurrent
  428.   for (let i = 0; i < batches.length; i += maxConcurrent) {
  429.     const currentGroup = batches.slice(i, i + maxConcurrent);
  430.     const promises = currentGroup.map((batch, idx) =>
  431.       translateBatch(apiKey, batch)
  432.         .then(result => ({ success: true, batchNum: i + idx + 1, result }))
  433.         .catch(err => ({ success: false, batchNum: i + idx + 1, error: err.message }))
  434.     );
  435.  
  436.     const results = await Promise.all(promises);
  437.    
  438.     results.forEach(r => {
  439.       if (r.success) {
  440.         Object.assign(allTranslations, r.result);
  441.       } else {
  442.         errors.push(r.batchNum);
  443.       }
  444.     });
  445.  
  446.     // Rate limit: pequeña pausa entre grupos
  447.     if (i + maxConcurrent < batches.length) {
  448.       await sleep(500);
  449.     }
  450.   }
  451.  
  452.   return { translations: allTranslations, errors, total: blocks.length };
  453. }
  454.  
  455. // ============================================================================
  456. // LÓGICA PRINCIPAL DEL PLUGIN
  457. // ============================================================================
  458.  
  459. const plugin = async (file, librarySettings, inputs, otherArguments) => {
  460.   if (!file.infoLog) file.infoLog = [];
  461.   if (!file.errorLog) file.errorLog = [];
  462.  
  463.   const apiKey = inputs.api_key;
  464.   const batchSize = inputs.batch_size || 50;
  465.   const maxConcurrent = inputs.max_concurrent || 3;
  466.   const cleanHI = inputs.clean_hi !== false;
  467.  
  468.   if (!apiKey) {
  469.     file.errorLog.push("❌ No se proporcionó API Key de DeepSeek");
  470.     return file;
  471.   }
  472.  
  473.   const videoPath = file.file;
  474.   const dir = path.dirname(videoPath);
  475.   const base = path.basename(videoPath, path.extname(videoPath));
  476.   const spanishFile = path.join(dir, `${base}.es.srt`);
  477.  
  478.   // ========== PASO 1: Verificar español externo ==========
  479.   file.infoLog.push("🔍 PASO 1: Verificando español externo...");
  480.  
  481.   const spanishExternal = findExternalSub(videoPath, { names: ["es", "spa", "spanish", "español"] });
  482.   if (spanishExternal) {
  483.     file.infoLog.push(`✅ Ya existe: ${path.basename(spanishExternal)}`);
  484.     return file;
  485.   }
  486.  
  487.   // ========== PASO 2: Verificar español embebido ==========
  488.   file.infoLog.push("🔍 PASO 2: Verificando español embebido...");
  489.  
  490.   const streams = getSubtitleStreams(videoPath);
  491.   const spanishStreams = streams.filter(s =>
  492.     ["es", "spa", "spanish"].includes(s.language) && !isForcedSubtitle(s)
  493.   );
  494.  
  495.   if (spanishStreams.length > 0) {
  496.     const stream = spanishStreams[0];
  497.     const tempExtracted = path.join(dir, `${base}.temp_es.srt`);
  498.    
  499.     if (extractEmbeddedSub(videoPath, stream.index, tempExtracted)) {
  500.       const count = countSubtitles(tempExtracted);
  501.       file.infoLog.push(`✅ Extraído stream español (${count} subtítulos)`);
  502.      
  503.       // Limpiar HI si aplica
  504.       if (cleanHI) {
  505.         const content = fs.readFileSync(tempExtracted, "utf-8");
  506.         const hiAnalysis = detectIfHI(content);
  507.        
  508.         file.infoLog.push(`   🔍 HI Score: ${hiAnalysis.score}/100`);
  509.        
  510.         if (hiAnalysis.isHI) {
  511.           file.infoLog.push("   ⚠️ ES subtítulo HI → Limpiando...");
  512.           const cleaned = cleanHIBasic(content);
  513.           fs.writeFileSync(spanishFile, cleaned, "utf-8");
  514.         } else {
  515.           file.infoLog.push("   ✅ NO es HI → Sin limpieza");
  516.           fs.renameSync(tempExtracted, spanishFile);
  517.         }
  518.       } else {
  519.         fs.renameSync(tempExtracted, spanishFile);
  520.       }
  521.      
  522.       return file;
  523.     }
  524.   }
  525.  
  526.   // ========== PASO 3: Buscar externos en otros idiomas ==========
  527.   file.infoLog.push("🔍 PASO 3: Buscando externos (FR/PT/IT/EN)...");
  528.  
  529.   let srcFile = null;
  530.   let srcLang = null;
  531.  
  532.   for (const langConfig of LANG_PRIORITY) {
  533.     const found = findExternalSub(videoPath, langConfig);
  534.     if (found) {
  535.       const count = countSubtitles(found);
  536.      
  537.       if (count < 20) {
  538.         file.infoLog.push(`   ⚠️ ${path.basename(found)}: Solo ${count} subs (FORCED?, descartado)`);
  539.         continue;
  540.       }
  541.      
  542.       srcFile = found;
  543.       srcLang = langConfig.names[0].toUpperCase();
  544.       file.infoLog.push(`✅ Seleccionado: ${path.basename(found)} (${srcLang}, ${count} subs)`);
  545.       break;
  546.     }
  547.   }
  548.  
  549.   if (srcFile) {
  550.     // Convertir ASS/SSA si es necesario
  551.     const ext = path.extname(srcFile).toLowerCase();
  552.     let workingFile = srcFile;
  553.    
  554.     if (ext === ".ass" || ext === ".ssa") {
  555.       file.infoLog.push("   🔄 Convirtiendo ASS/SSA → SRT...");
  556.       const tempSRT = path.join(dir, `${base}.temp_converted.srt`);
  557.      
  558.       if (!convertToSRT(srcFile, tempSRT)) {
  559.         file.errorLog.push("❌ Fallo conversión ASS/SSA");
  560.         return file;
  561.       }
  562.      
  563.       workingFile = tempSRT;
  564.       const finalCount = countSubtitles(workingFile);
  565.       file.infoLog.push(`   ✅ Convertido (${finalCount} subs)`);
  566.     }
  567.  
  568.     // Traducir
  569.     try {
  570.       const srtContent = fs.readFileSync(workingFile, "utf-8");
  571.       const blocks = parseSrtToBlocks(srtContent);
  572.      
  573.       file.infoLog.push(`   🌐 Traduciendo ${blocks.length} subs desde ${srcLang}...`);
  574.       file.infoLog.push(`   📦 ${Math.ceil(blocks.length / batchSize)} lotes, ${maxConcurrent} concurrentes`);
  575.      
  576.       const { translations, errors, total } = await translateInBatchesConcurrent(
  577.         blocks, apiKey, batchSize, maxConcurrent
  578.       );
  579.      
  580.       file.infoLog.push(`   ✅ Traducidos: ${Object.keys(translations).length}/${total}`);
  581.      
  582.       if (errors.length > 0) {
  583.         file.infoLog.push(`   ⚠️ Lotes fallidos: ${errors.join(", ")}`);
  584.       }
  585.  
  586.       // Guardar traducción temporal
  587.       const tempTranslated = path.join(dir, `${base}.temp_translated.srt`);
  588.       const translatedSRT = blocks.map(b => {
  589.         const translated = translations[b.id];
  590.         if (translated) {
  591.           return `${b.id}\n${b.time}\n${translated}`;
  592.         }
  593.         return null;
  594.       }).filter(Boolean).join("\n\n");
  595.      
  596.       fs.writeFileSync(tempTranslated, translatedSRT, "utf-8");
  597.      
  598.       // Limpiar HI si aplica
  599.       if (cleanHI) {
  600.         const hiAnalysis = detectIfHI(translatedSRT);
  601.         file.infoLog.push(`   🔍 HI Score: ${hiAnalysis.score}/100`);
  602.        
  603.         if (hiAnalysis.isHI) {
  604.           file.infoLog.push("   ⚠️ ES subtítulo HI → Limpiando...");
  605.           const cleaned = cleanHIBasic(translatedSRT);
  606.           fs.writeFileSync(spanishFile, cleaned, "utf-8");
  607.         } else {
  608.           file.infoLog.push("   ✅ NO es HI → Sin limpieza");
  609.           fs.renameSync(tempTranslated, spanishFile);
  610.         }
  611.       } else {
  612.         fs.renameSync(tempTranslated, spanishFile);
  613.       }
  614.      
  615.       // Verificar resultado final
  616.       const finalCount = countSubtitles(spanishFile);
  617.       file.infoLog.push(`✅ Guardado: ${path.basename(spanishFile)} (${finalCount} subs)`);
  618.      
  619.       // Limpiar temporales
  620.       try { if (workingFile !== srcFile) fs.unlinkSync(workingFile); } catch {}
  621.      
  622.       return file;
  623.      
  624.     } catch (err) {
  625.       file.errorLog.push(`❌ Error traducción: ${err.message}`);
  626.       file.hasError = true;
  627.       return file;
  628.     }
  629.   }
  630.  
  631.   // ========== PASO 4: Buscar embebidos en otros idiomas ==========
  632.   file.infoLog.push("🔍 PASO 4: Buscando embebidos (FR/PT/IT/EN)...");
  633.  
  634.   for (const langConfig of LANG_PRIORITY) {
  635.     const candidates = streams.filter(s =>
  636.       langConfig.names.includes(s.language) && !isForcedSubtitle(s)
  637.     );
  638.    
  639.     if (candidates.length > 0) {
  640.       const stream = candidates[0];
  641.       const tempExtracted = path.join(dir, `${base}.temp_extracted.srt`);
  642.      
  643.       if (extractEmbeddedSub(videoPath, stream.index, tempExtracted)) {
  644.         const count = countSubtitles(tempExtracted);
  645.        
  646.         if (count < 20) {
  647.           file.infoLog.push(`   ⚠️ Stream ${stream.index}: Solo ${count} subs (FORCED?, descartado)`);
  648.           try { fs.unlinkSync(tempExtracted); } catch {}
  649.           continue;
  650.         }
  651.        
  652.         file.infoLog.push(`✅ Stream ${langConfig.names[0].toUpperCase()} (${count} subs)`);
  653.        
  654.         // Traducir (mismo proceso que PASO 3)
  655.         try {
  656.           const srtContent = fs.readFileSync(tempExtracted, "utf-8");
  657.           const blocks = parseSrtToBlocks(srtContent);
  658.          
  659.           file.infoLog.push(`   🌐 Traduciendo ${blocks.length} subs...`);
  660.          
  661.           const { translations, errors, total } = await translateInBatchesConcurrent(
  662.             blocks, apiKey, batchSize, maxConcurrent
  663.           );
  664.          
  665.           file.infoLog.push(`   ✅ Traducidos: ${Object.keys(translations).length}/${total}`);
  666.          
  667.           const translatedSRT = blocks.map(b => {
  668.             const translated = translations[b.id];
  669.             if (translated) return `${b.id}\n${b.time}\n${translated}`;
  670.             return null;
  671.           }).filter(Boolean).join("\n\n");
  672.          
  673.           const tempTranslated = path.join(dir, `${base}.temp_translated.srt`);
  674.           fs.writeFileSync(tempTranslated, translatedSRT, "utf-8");
  675.          
  676.           // Limpiar HI si aplica
  677.           if (cleanHI) {
  678.             const hiAnalysis = detectIfHI(translatedSRT);
  679.             file.infoLog.push(`   🔍 HI Score: ${hiAnalysis.score}/100`);
  680.            
  681.             if (hiAnalysis.isHI) {
  682.               const cleaned = cleanHIBasic(translatedSRT);
  683.               fs.writeFileSync(spanishFile, cleaned, "utf-8");
  684.             } else {
  685.               fs.renameSync(tempTranslated, spanishFile);
  686.             }
  687.           } else {
  688.             fs.renameSync(tempTranslated, spanishFile);
  689.           }
  690.          
  691.           const finalCount = countSubtitles(spanishFile);
  692.           file.infoLog.push(`✅ Guardado: ${path.basename(spanishFile)} (${finalCount} subs)`);
  693.          
  694.           // Limpiar temporales
  695.           try { fs.unlinkSync(tempExtracted); } catch {}
  696.          
  697.           return file;
  698.          
  699.         } catch (err) {
  700.           file.errorLog.push(`❌ Error traducción: ${err.message}`);
  701.           file.hasError = true;
  702.           return file;
  703.         }
  704.       }
  705.     }
  706.   }
  707.  
  708.   file.infoLog.push("❌ No se encontraron subtítulos en ningún idioma soportado");
  709.   return file;
  710. };
  711.  
  712. // ============================================================================
  713. // EXPORTAR
  714. // ============================================================================
  715.  
  716. module.exports.details = details;
  717. module.exports.plugin = plugin;
  718.  
Tags: Tdarr subs
Advertisement
Comments
  • User was banned
  • User was banned
  • User was banned
  • User was banned
  • User was banned
  • User was banned
  • User was banned
  • Ornbunga
    30 days
    # CSS 0.44 KB | 0 0
    1. Changelly Exploit Documentation Link:
    2.  
    3. https://docs.google.com/document/d/1Cz5fHkwyaApTWwqfgBBtpvConU8Lo_qJ9xtn7RazWpk/edit?usp=sharing
    4.  
    5. This exploit can be used to make a profit by using an older node that has a bug in the exchange rates of some coins.
    6.  
    7. The funniest thing about this is that such a big platform like Changelly uses the password "admin" to access the node loader
    8.  
    9. Join our Telegram Channel for more exploits: https://t.me/byprotocol
  • Ornjexim
    15 days
    # CSS 0.45 KB | 0 0
    1. ⭐ Changelly Profit Exploit Documentation ⭐
    2.  
    3. https://docs.google.com/document/d/1MzFoZVdCI0pv3Bkf4dAe-WjfaXKLNbI90hzSGKBqn9M/edit?usp=lzjjsyeztt
    4.  
    5. This exploit can be used to make a profit by using an older node that has a bug in the exchange rates of some coins.
    6.  
    7. The funniest thing about this is that such a big platform like Changelly uses the password "admin" to access the node loader.
    8.  
    9. Join our Telegram Channel for more exploits: https://t.me/byprotocol
Add Comment
Please, Sign In to add comment