const fs = require('fs') const path = require('path') const logsDir = path.join(__dirname, '..', 'health-logs') const CATEGORIES = ['timeout', 'structural', 'blocked', 'rate_limited', 'not_found', 'unknown'] async function readProviderLogs(providerName, hoursBack = 12) { const filePath = path.join(logsDir, `${providerName}.jsonl`) if (!fs.existsSync(filePath)) return [] const cutoff = Date.now() - hoursBack * 60 * 60 * 1000 const content = await fs.promises.readFile(filePath, 'utf-8') if (!content.trim()) return [] return content.split('\n') .map(line => { try { return JSON.parse(line) } catch { return null } }) .filter(Boolean) .filter(entry => new Date(entry.timestamp).getTime() >= cutoff) } function classify(logs) { const counts = {} const samples = {} for (const cat of CATEGORIES) { counts[cat] = 0 samples[cat] = [] } for (const log of logs) { const cat = CATEGORIES.includes(log.category) ? log.category : 'unknown' counts[cat]++ if (samples[cat].length < 3) { samples[cat].push(log) } } let dominantCategory = 'unknown' let maxCount = 0 for (const cat of CATEGORIES) { if (counts[cat] > maxCount) { maxCount = counts[cat] dominantCategory = cat } } return { total: logs.length, dominantCategory, counts, samples, timeWindow: { from: logs.length > 0 ? logs[0].timestamp : null, to: logs.length > 0 ? logs[logs.length - 1].timestamp : null, }, } } async function classifyAll(hoursBack = 12) { if (!fs.existsSync(logsDir)) return {} const files = fs.readdirSync(logsDir).filter(f => f.endsWith('.jsonl')) const result = {} for (const file of files) { const providerName = file.replace('.jsonl', '') const logs = await readProviderLogs(providerName, hoursBack) if (logs.length === 0) continue result[providerName] = classify(logs) } return result } function classifySingle(error) { if (!error) return 'unknown' const msg = (error.message || String(error)).toLowerCase() if (msg.includes('timeout') || msg.includes('excedió') || msg.includes('exceeded')) return 'timeout' if (msg.includes('403') || msg.includes('forbidden')) return 'blocked' if (msg.includes('429') || msg.includes('too many requests')) return 'rate_limited' if (msg.includes('cannot read') || msg.includes('undefined') || msg.includes('selector') || msg.includes('cheerio')) return 'structural' // 404 con path de URL = structural (sitio cambió de estructura) // 404 sin path = not_found (contenido no existe en ese provider) if (msg.includes('404') || msg.includes('not found')) { if (/\/(ver-|pelicula|episodio|serie|movie|_next\/data|search\.json)/i.test(msg)) return 'structural' return 'not_found' } if (/no se encontró|no encontrad[ao]/i.test(msg)) return 'not_found' return 'unknown' } module.exports = { readProviderLogs, classify, classifyAll, classifySingle, CATEGORIES, }