function qualityToPixels(q) { if (!q) return null; const s = String(q).toLowerCase(); if (/2160|4k|uhd/.test(s)) return 2160; const m = s.match(/(\d{3,4})\s*p/); if (m) { const n = parseInt(m[1], 10); return n >= 1440 ? 2160 : n; } if (/1080|full\s?hd|fhd/.test(s)) return 1080; if (/720/.test(s)) return 720; if (/480/.test(s)) return 480; return null; } function detectAudio(text) { const t = ` ${String(text || '').toLowerCase()} `; if (/\bdts\b|dts-?hd/.test(t)) return 'dts'; if (/e-?ac3|dd\+|ac-?3|dolby digital/.test(t)) return 'ac3'; if (/\baac\b/.test(t)) return 'aac'; if (/\bopus\b/.test(t)) return 'opus'; if (/\bflac\b|lossless/.test(t)) return 'flac'; if (/\bmp3\b/.test(t)) return 'mp3'; if (/dual-?audio|multi-?audio|original audio|multi audio/.test(t)) return 'multi'; return null; } function extOf(text) { const m = String(text || '').toLowerCase().split(/[?#]/)[0].match(/\.([a-z0-9]{2,5})$/); return m ? m[1] : ''; } function streamType(url, name) { const u = String(url || ''); const n = String(name || '').toLowerCase(); if (/^magnet:/i.test(u)) return 'magnet'; if (/m3u8/i.test(u)) return 'm3u8'; const ext = extOf(u) || extOf(n); if (ext === 'mkv' || /\.mkv\b/.test(n)) return 'mkv'; if (ext === 'mp4' || ext === 'm4v' || ext === 'mov' || /\.mp4\b/.test(n)) return 'mp4'; if (ext === 'avi' || /\.avi\b/.test(n)) return 'avi'; if (ext === 'webm') return 'webm'; if (ext === 'ts' || ext === 'm4a') return 'other'; return ext || 'other'; } function normalizeSubtitles(subs) { if (!Array.isArray(subs)) return []; return subs .filter((s) => s && typeof s.url === 'string' && s.url.length > 0) .map((s) => ({ url: s.url, language: s.language || s.id || s.lang || '', name: s.name || '', })); } function normalize(raw, providerName) { const text = [raw.name, raw.title, raw.description].filter(Boolean).join(' '); const pixels = qualityToPixels(raw.quality) ?? qualityToPixels(`${raw.name || ''} ${raw.title || ''}`); return { provider: providerName, serverName: raw._serverName || '', name: raw.name || raw.title || 'stream', title: raw.title || '', description: raw.description || '', url: raw.url, quality: raw.quality || (pixels ? `${pixels}p` : ''), pixels, audio: detectAudio(text), type: streamType(raw.url, raw.name || raw.title), subtitles: normalizeSubtitles(raw.subtitles), size: typeof raw.size === 'number' ? raw.size : 0, headers: raw.headers && typeof raw.headers === 'object' ? raw.headers : {}, is4k: !!raw._is4k || pixels === 2160, }; } function formatBytes(n) { if (!n || n <= 0) return ''; const units = ['B', 'KB', 'MB', 'GB', 'TB']; let i = 0; let v = n; while (v >= 1024 && i < units.length - 1) { v /= 1024; i++; } return `${v.toFixed(v >= 100 || i === 0 ? 0 : 1)}${units[i]}`; } module.exports = { normalize, qualityToPixels, detectAudio, formatBytes };