// === GLOBAL VARIABLES === let psshs = chrome.extension.getBackgroundPage().psshs; let requests = chrome.extension.getBackgroundPage().requests; let pageURL = chrome.extension.getBackgroundPage().pageURL; let targetIds = chrome.extension.getBackgroundPage().targetIds; let clearkey = chrome.extension.getBackgroundPage().clearkey; let userInputs = {}; // IMPORTANT: Cette variable était manquante! // === WIDEVINE KEY EXTRACTION === class WidevineExtractor { static async extractKeys() { const guessButton = document.getElementById("guess"); const resultTextarea = document.getElementById('result'); try { // Vérifier que userInputs.license est défini if (userInputs.license === undefined || !requests[userInputs.license]) { throw new Error('License not selected. Please wait for auto-selection or select manually.'); } // UI feedback UIHelpers.setLoadingState(guessButton, true); document.body.style.cursor = "wait"; // Initialize Pyodide const pyodide = await loadPyodide(); await pyodide.loadPackage([ "certifi-2024.2.2-py3-none-any.whl", "charset_normalizer-3.3.2-py3-none-any.whl", "construct-2.8.8-py2.py3-none-any.whl", "idna-3.6-py3-none-any.whl", "packaging-23.2-py3-none-any.whl", "protobuf-4.24.4-cp312-cp312-emscripten_3_1_52_wasm32.whl", "pycryptodome-3.20.0-cp35-abi3-emscripten_3_1_52_wasm32.whl", "pymp4-1.4.0-py3-none-any.whl", "pyodide_http-0.2.1-py3-none-any.whl", "pywidevine-1.8.0-py3-none-any.whl", "requests-2.31.0-py3-none-any.whl", "urllib3-2.2.1-py3-none-any.whl" ].map(e => "/libs/wheels/" + e)); // Configure Guesser pyodide.globals.set("pssh", document.getElementById('pssh').value); pyodide.globals.set("licUrl", requests[userInputs['license']]['url']); pyodide.globals.set("licHeaders", requests[userInputs['license']]['headers']); pyodide.globals.set("licBody", requests[userInputs['license']]['body']); // Load Python scripts const [pre, after, scheme] = await Promise.all([ fetch('/python/pre.py').then(res => res.text()), fetch('/python/after.py').then(res => res.text()), Promise.resolve(document.getElementById("schemeCode").value) ]); // Execute Python script const result = await pyodide.runPythonAsync([pre, scheme, after].join("\n")); resultTextarea.value = result; // Save to history this.saveToHistory(result); // Auto-update CrawlFlix integration CrawlFlixIntegration.updateAfterKeyExtraction(); StatusManager.show('Keys extracted successfully!', 'success'); } catch (error) { console.error('Key extraction failed:', error); StatusManager.show(`Key extraction failed: ${error.message}`, 'error'); } finally { // Reset UI UIHelpers.setLoadingState(guessButton, false); document.body.style.cursor = "auto"; } } static saveToHistory(result) { const historyData = { PSSH: document.getElementById('pssh').value, KEYS: result.split("\n").slice(0, -1) }; chrome.storage.local.set({[pageURL]: historyData}, null); } static async autoSelect() { userInputs["license"] = 0; document.getElementById("license").value = requests[0]['url']; document.getElementById('pssh').value = psshs[0]; try { const selectRules = await fetch("/selectRules.conf").then(r => r.text()); const rules = selectRules .replace(/\n^\s*$|\s*\/\/.*|\s*$/gm, "") .split("\n") .map(row => row.split("$$")); for (const item of rules) { const search = requests.map(r => r['url']).findIndex(e => e.includes(item[0])); if (search >= 0) { if (item[1]) document.getElementById("schemeSelect").value = item[1]; userInputs["license"] = search; document.getElementById("license").value = requests[search]['url']; break; } } document.getElementById("schemeSelect").dispatchEvent(new Event("input")); } catch (error) { console.error('Auto-select failed:', error); } } } class MPDCache { constructor() { this.cachedMPDs = new Map(); this.validationPromises = new Map(); } hasChanged(newMPDs) { const currentUrls = Array.from(this.cachedMPDs.keys()).sort(); const newUrls = newMPDs.sort(); return JSON.stringify(currentUrls) !== JSON.stringify(newUrls); } setCachedMPD(url, data) { this.cachedMPDs.set(url, { ...data, lastValidated: Date.now() }); } // Récupère un MPD du cache getCachedMPD(url) { return this.cachedMPDs.get(url); } // Supprime un MPD invalide du cache removeMPD(url) { this.cachedMPDs.delete(url); this.validationPromises.delete(url); } // Récupère tous les MPDs valides triés par score/durée getValidMPDs() { return Array.from(this.cachedMPDs.entries()) .map(([url, data]) => ({ url, ...data })) .sort((a, b) => { // Prioriser d'abord par durée si disponible if (a.duration && b.duration) { return b.duration - a.duration; } // Sinon par score return b.score - a.score; }); } cleanup(maxAge = 5 * 60 * 1000) { const now = Date.now(); for (const [url, data] of this.cachedMPDs.entries()) { if (now - data.lastValidated > maxAge) { this.removeMPD(url); } } } } class MPDValidator { static async validateMPD(url) { const API_URL = 'http://192.168.1.230:6080/api/processMpd'; try { const response = await fetch(API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ mp4Filename: "validation_test", mpdUrl: url }) }); if (!response.ok) { throw new Error(`API returned ${response.status}: ${response.statusText}`); } const result = await response.json(); // Valider la structure de la réponse if (!result.videoTracks || !Array.isArray(result.videoTracks)) { throw new Error('Invalid response structure: missing videoTracks array'); } // Calculer la durée maximale parmi les tracks const maxDuration = result.videoTracks.reduce((max, track) => { const duration = track.duration || 0; return Math.max(max, duration); }, 0); return { valid: true, duration: maxDuration, videoTracks: result.videoTracks, raw: result }; } catch (error) { console.error(`MPD validation failed for ${url}:`, error); return { valid: false, error: error.message, duration: 0 }; } } static async validateMPDWithCache(url, cache) { // Vérifier si on a déjà une promesse en cours pour cette URL if (cache.validationPromises.has(url)) { return await cache.validationPromises.get(url); } // Créer une nouvelle promesse de validation const validationPromise = this.validateMPD(url); cache.validationPromises.set(url, validationPromise); try { const result = await validationPromise; return result; } finally { // Nettoyer la promesse une fois terminée cache.validationPromises.delete(url); } } } class SmartMPDSelector { static scoreAndRankMPDs(mpdUrls) { const scoredMPDs = mpdUrls.map(url => ({ url, score: this.calculateMPDScore(url), reason: this.getSelectionReason(url) })); // Trier par score décroissant return scoredMPDs.sort((a, b) => b.score - a.score); } static calculateMPDScore(url) { let score = 0; const urlLower = url.toLowerCase(); // === CRITÈRES NÉGATIFS (à éviter) === // Éliminer les URLs non-MPD if (urlLower.includes('github.com') || urlLower.includes('manifest.json')) { return -1000; // Score très négatif } // Éviter les URLs avec trop de redirections/proxies if (urlLower.includes('routemeup') || urlLower.includes('route=')) { score -= 200; } // Éviter les URLs avec double-encoding ou trop complexes if (url.includes('__token__') && url.includes('%')) { score -= 100; } // === CRITÈRES POSITIFS (à privilégier) === // Privilégier les URLs directes CDN if (urlLower.includes('cdn.net') || urlLower.includes('vod-')) { score += 300; } // Privilégier les URLs avec .mpd explicite if (url.endsWith('.mpd') || urlLower.includes('.mpd?')) { score += 200; } // Privilégier les URLs courtes (moins de proxies) if (url.length < 300) { score += 100; } else if (url.length > 500) { score -= 50; } // Privilégier les tokens JWT simples vs double tokens const tokenCount = (url.match(/token=/g) || []).length; if (tokenCount === 1) { score += 50; } else if (tokenCount > 1) { score -= 100; } // Privilégier les URLs sans encoding excessif const encodingScore = url.length - decodeURIComponent(url).length; if (encodingScore < 50) { score += 50; } else { score -= encodingScore; } // === CRITÈRES SPÉCIFIQUES CANAL+ === // Détecter le pattern Canal+/Paramount+ if (urlLower.includes('paramountplus') || urlLower.includes('viacom')) { score += 100; // Privilégier les edge servers directs if (urlLower.includes('p-cdnvod-edge') && !urlLower.includes('routemeup')) { score += 150; } } return score; } static getSelectionReason(url) { const urlLower = url.toLowerCase(); if (urlLower.includes('github.com')) { return 'Excluded: Not a media manifest'; } if (urlLower.includes('routemeup')) { return 'Router/proxy URL (lower priority)'; } if (urlLower.includes('p-cdnvod-edge') && !urlLower.includes('routemeup')) { return 'Direct CDN edge server (optimal)'; } if (url.includes('__token__')) { return 'Tokenized CDN URL'; } return 'Standard MPD URL'; } static getBestMPD(mpdUrls) { if (!mpdUrls || mpdUrls.length === 0) return null; if (mpdUrls.length === 1) return mpdUrls[0]; const rankedMPDs = this.scoreAndRankMPDs(mpdUrls); const best = rankedMPDs[0]; console.log('MPD Selection Results:', rankedMPDs); // Ne retourner que si le score est positif return best.score > 0 ? best.url : null; } static analyzeAndDisplayMPDs(mpdUrls) { const rankedMPDs = this.scoreAndRankMPDs(mpdUrls); // Afficher l'analyse dans la console pour debug console.table(rankedMPDs.map(mpd => ({ URL: this.truncateUrl(mpd.url), Score: mpd.score, Reason: mpd.reason }))); return rankedMPDs; } static truncateUrl(url, maxLength = 60) { if (url.length <= maxLength) return url; try { const urlObj = new URL(url); const domain = urlObj.hostname; const filename = urlObj.pathname.split('/').pop(); return `${domain}/.../${filename}`; } catch { return url.substring(0, maxLength) + '...'; } } // Fonction pour mettre à jour l'UI avec le meilleur MPD static updateUIWithBestMPD(mpdUrls) { const bestMPD = this.getBestMPD(mpdUrls); if (bestMPD) { // Auto-sélectionner le meilleur MPD const selects = ['mpdSelect', 'mpdSelectCK']; const inputs = ['mpdUrl', 'mpdUrlCK']; selects.forEach((selectId, index) => { const select = document.getElementById(selectId); const input = document.getElementById(inputs[index]); if (select && input) { select.value = bestMPD; input.value = bestMPD; // Ajouter une classe pour indiquer la sélection auto select.classList.add('border-success'); setTimeout(() => select.classList.remove('border-success'), 3000); } }); const analysis = this.analyzeAndDisplayMPDs(mpdUrls); const bestAnalysis = analysis[0]; StatusManager.show( `Auto-selected best MPD: ${bestAnalysis.reason}`, 'success' ); } } } class MPDDetector { constructor() { this.cache = new MPDCache(); this.isUpdating = false; } static detectMPDUrls() { const backgroundPage = chrome.extension.getBackgroundPage(); const detectedMPDs = backgroundPage.detectedMPDs || []; console.log('Raw detected MPDs:', detectedMPDs); return detectedMPDs .filter((url, index, arr) => arr.indexOf(url) === index) // Remove duplicates .filter(url => url && url.length > 0) // Remove empty URLs .filter(url => !url.includes('/chunk-') && !url.includes('/segment-')); // Filter segments } async updateDropdowns() { // Éviter les mises à jour simultanées if (this.isUpdating) { console.log('MPD update already in progress, skipping...'); return; } this.isUpdating = true; try { const rawMPDs = MPDDetector.detectMPDUrls(); // Vérifier si la liste a changé if (!this.cache.hasChanged(rawMPDs)) { console.log('MPD list unchanged, using cache'); this.renderDropdowns(); return; } console.log('MPD list changed, updating cache...'); // Nettoyer le cache des URLs qui ne sont plus détectées const currentCachedUrls = Array.from(this.cache.cachedMPDs.keys()); currentCachedUrls.forEach(url => { if (!rawMPDs.includes(url)) { this.cache.removeMPD(url); } }); // Traiter les nouvelles URLs const newUrls = rawMPDs.filter(url => !this.cache.getCachedMPD(url)); if (newUrls.length > 0) { StatusManager.show( `Validating ${newUrls.length} new MPD(s)...`, 'info' ); await this.processNewMPDs(newUrls); } // Rendre les dropdowns this.renderDropdowns(); // Nettoyer le cache périodiquement this.cache.cleanup(); } catch (error) { console.error('MPD update failed:', error); StatusManager.show(`MPD update failed: ${error.message}`, 'error'); } finally { this.isUpdating = false; } } async processNewMPDs(urls) { const validationPromises = urls.map(async (url) => { try { // Calculer le score initial (logique existante) const initialScore = SmartMPDSelector.calculateMPDScore(url); const reason = SmartMPDSelector.getSelectionReason(url); // Valider via API const validationResult = await MPDValidator.validateMPDWithCache(url, this.cache); if (validationResult.valid) { // Ajuster le score basé sur la durée let finalScore = initialScore; if (validationResult.duration > 0) { // Bonus pour les durées plus longues finalScore += Math.min(validationResult.duration / 10, 500); // Max +500 points } this.cache.setCachedMPD(url, { score: finalScore, reason: `${reason} | Duration: ${validationResult.duration}s`, duration: validationResult.duration, valid: true, videoTracks: validationResult.videoTracks }); console.log(`✅ MPD validated: ${url} (duration: ${validationResult.duration}s)`); } else { // MPD invalide, ne pas l'ajouter au cache console.log(`❌ MPD invalid: ${url} (${validationResult.error})`); } } catch (error) { console.error(`Error processing MPD ${url}:`, error); } }); await Promise.allSettled(validationPromises); } renderDropdowns() { const validMPDs = this.cache.getValidMPDs(); const selects = ['mpdSelect', 'mpdSelectCK']; selects.forEach(selectId => { const select = document.getElementById(selectId); if (!select) return; select.innerHTML = ''; validMPDs.forEach((mpdInfo, index) => { const option = document.createElement('option'); option.value = mpdInfo.url; // Indicateurs visuels améliorés let prefix = ''; if (index === 0) { prefix = `🏆 [${mpdInfo.duration}s] `; } else if (mpdInfo.duration > 0) { prefix = `⏱️ [${mpdInfo.duration}s] `; } else if (mpdInfo.score > 0) { prefix = '✅ '; } else { prefix = '⚠️ '; } option.textContent = prefix + this.formatUrlForDisplay(mpdInfo.url); option.title = `Score: ${mpdInfo.score} | ${mpdInfo.reason}`; // Styling if (index === 0) { option.style.fontWeight = 'bold'; option.style.color = '#28a745'; } else if (mpdInfo.duration > 1000) { // Plus de 16 minutes option.style.color = '#17a2b8'; } else if (mpdInfo.score <= 0) { option.style.color = '#dc3545'; } select.appendChild(option); }); }); // Auto-sélection du meilleur if (validMPDs.length > 0) { this.selectBestMPD(validMPDs[0]); } // Mettre à jour le status const totalDetected = MPDDetector.detectMPDUrls().length; const validCount = validMPDs.length; const invalidCount = totalDetected - validCount; let statusMessage = `Found ${validCount} valid MPD(s)`; if (invalidCount > 0) { statusMessage += ` (${invalidCount} filtered out)`; } StatusManager.show(statusMessage, validCount > 0 ? 'success' : 'warning'); } selectBestMPD(bestMPD) { const selects = ['mpdSelect', 'mpdSelectCK']; const inputs = ['mpdUrl', 'mpdUrlCK']; selects.forEach((selectId, index) => { const select = document.getElementById(selectId); const input = document.getElementById(inputs[index]); if (select && input) { select.value = bestMPD.url; input.value = bestMPD.url; // Effet visuel select.classList.add('border-success'); setTimeout(() => select.classList.remove('border-success'), 3000); } }); } formatUrlForDisplay(url) { return SmartMPDSelector.truncateUrl(url, 80); } // Méthode publique pour forcer la mise à jour async forceUpdate() { this.cache = new MPDCache(); // Reset du cache await this.updateDropdowns(); } } const mpdDetectorInstance = new MPDDetector(); MPDDetector.updateDropdowns = () => mpdDetectorInstance.updateDropdowns(); MPDDetector.forceUpdate = () => mpdDetectorInstance.forceUpdate(); // === CRAWLFLIX INTEGRATION === class CrawlFlixIntegration { static async sendToCrawlFlix(isClearchey = false) { const suffix = isClearchey ? 'CK' : ''; const crawlFlixUrl = document.getElementById(`crawlFlixUrl${suffix}`).value || 'http://localhost:4200'; const mpdUrl = document.getElementById(`mpdUrl${suffix}`).value; const resultTextarea = document.getElementById(isClearchey ? 'ckResult' : 'result'); // Validation if (!mpdUrl.trim()) { StatusManager.show('Please enter or select an MPD URL', 'error', suffix); return; } if (!resultTextarea.value.trim()) { StatusManager.show('No keys available to send', 'error', suffix); return; } try { StatusManager.show('Opening CrawlFlix with preloaded data...', 'info', suffix); // Parse et valide les clés pour information const keys = this.parseKeys(resultTextarea.value); if (keys.length === 0) { StatusManager.show('No valid keys found in the format key:value', 'error', suffix); return; } // Ouvrir CrawlFlix directement avec les paramètres this.openCrawlFlixTab(crawlFlixUrl, mpdUrl, resultTextarea.value); // Message de succès const contentType = isClearchey ? 'ClearKey' : 'Widevine'; StatusManager.show(`${contentType} data sent to CrawlFlix! (${keys.length} keys, MPD URL)`, 'success', suffix); } catch (error) { console.error('CrawlFlix send error:', error); StatusManager.show(`Failed to send: ${error.message}`, 'error', suffix); } } static parseKeys(keysText) { return keysText .split('\n') .map(line => line.trim()) .filter(line => line && line.includes(':')) .map(line => { const [key, value] = line.split(':'); return { key: key?.trim(), value: value?.trim() }; }) .filter(k => k.key && k.value && k.key.length > 0 && k.value.length > 0); } static async processMPD(crawlFlixUrl, mpdUrl) { const response = await fetch(`${crawlFlixUrl}/processMPD`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mpdUrl }) }); if (!response.ok) { const errorText = await response.text().catch(() => 'Unknown error'); throw new Error(`MPD processing failed (${response.status}): ${errorText}`); } return await response.json(); } static openCrawlFlixTab(crawlFlixUrl, mpdUrl, keysText) { // Encoder les données pour l'URL const params = new URLSearchParams({ mpdUrl: mpdUrl, keys: keysText, autoLoad: 'true' }); // Ouvrir CrawlFlix avec les paramètres pré-remplis const crawlFlixTab = `${crawlFlixUrl}?${params.toString()}`; chrome.tabs.create({ url: crawlFlixTab }); console.log('Opening CrawlFlix with preloaded data:', crawlFlixTab); } static copyKeys(resultTextareaId) { const textarea = document.getElementById(resultTextareaId); if (!textarea || !textarea.value.trim()) { StatusManager.show('No keys to copy', 'error'); return; } navigator.clipboard.writeText(textarea.value).then(() => { StatusManager.show('Keys copied to clipboard!', 'success'); }).catch(err => { console.error('Copy failed:', err); StatusManager.show('Failed to copy keys', 'error'); }); } static updateAfterKeyExtraction() { // Auto-refresh MPD detection after key extraction setTimeout(() => { MPDDetector.updateDropdowns(); }, 1000); } static loadSavedSettings() { chrome.storage.local.get(['crawlFlixUrl'], (result) => { if (result.crawlFlixUrl) { const inputs = ['crawlFlixUrl', 'crawlFlixUrlCK']; inputs.forEach(id => { const input = document.getElementById(id); if (input && !input.value) { input.value = result.crawlFlixUrl; } }); } }); } static saveSettings() { const inputs = ['crawlFlixUrl', 'crawlFlixUrlCK']; inputs.forEach(id => { const input = document.getElementById(id); if (input) { input.addEventListener('change', (e) => { chrome.storage.local.set({ crawlFlixUrl: e.target.value }); }); } }); } } // === STATUS MANAGER === class StatusManager { static show(message, type, suffix = '') { const statusDiv = document.getElementById(`crawlFlixStatus${suffix}`); if (!statusDiv) return; // Clear any existing content statusDiv.innerHTML = ''; // Create alert element const alert = document.createElement('div'); alert.className = `alert alert-${this.getBootstrapClass(type)} alert-dismissible fade show`; alert.innerHTML = ` ${message.replace(/\n/g, '
')} `; statusDiv.appendChild(alert); // Auto-dismiss after delay if (type === 'success' || type === 'info') { setTimeout(() => { if (alert.parentNode) { alert.classList.remove('show'); setTimeout(() => alert.remove(), 300); } }, 5000); } } static getBootstrapClass(type) { const mapping = { success: 'success', error: 'danger', info: 'info', warning: 'warning' }; return mapping[type] || 'secondary'; } static getIcon(type) { const mapping = { success: 'fa-check-circle', error: 'fa-exclamation-triangle', info: 'fa-info-circle', warning: 'fa-exclamation-triangle' }; return mapping[type] || 'fa-info'; } } // === UI HELPERS === class UIHelpers { static setLoadingState(button, isLoading) { if (!button) return; if (isLoading) { button.disabled = true; const originalText = button.innerHTML; button.dataset.originalText = originalText; button.innerHTML = 'Extracting Keys...'; } else { button.disabled = false; button.innerHTML = ' Extract Widevine Keys'; } } static copyToClipboard(text) { const textarea = document.createElement('textarea'); textarea.value = text; document.body.appendChild(textarea); textarea.select(); document.execCommand('copy'); document.body.removeChild(textarea); } } // === EVENT LISTENERS === class EventManager { static init() { this.setupWidevineListeners(); this.setupCrawlFlixListeners(); this.setupUIListeners(); } static setupWidevineListeners() { const guessButton = document.getElementById('guess'); const resultTextarea = document.getElementById('result'); if (guessButton) { guessButton.addEventListener('click', () => WidevineExtractor.extractKeys()); } if (resultTextarea) { resultTextarea.addEventListener('click', function() { this.select(); navigator.clipboard.writeText(this.value); }); } } static setupCrawlFlixListeners() { // Dropdown selections ['mpdSelect', 'mpdSelectCK'].forEach(selectId => { const select = document.getElementById(selectId); const urlInput = document.getElementById(selectId.replace('Select', 'Url')); if (select && urlInput) { select.addEventListener('change', (e) => { if (e.target.value) { urlInput.value = e.target.value; } }); } }); // Refresh buttons ['refreshMPDs', 'refreshMPDsCK'].forEach(buttonId => { const button = document.getElementById(buttonId); if (button) { button.addEventListener('click', async () => { button.disabled = true; button.innerHTML = ' Refreshing...'; try { await MPDDetector.forceUpdate(); } finally { button.disabled = false; button.innerHTML = ' Refresh'; } }); } }); // Send buttons const sendButton = document.getElementById('sendToCrawlFlix'); if (sendButton) { sendButton.addEventListener('click', () => CrawlFlixIntegration.sendToCrawlFlix(false)); } const sendButtonCK = document.getElementById('sendToCrawlFlixCK'); if (sendButtonCK) { sendButtonCK.addEventListener('click', () => CrawlFlixIntegration.sendToCrawlFlix(true)); } // Copy buttons const copyButton = document.getElementById('copyKeys'); if (copyButton) { copyButton.addEventListener('click', () => CrawlFlixIntegration.copyKeys('result')); } const copyButtonCK = document.getElementById('copyKeysCK'); if (copyButtonCK) { copyButtonCK.addEventListener('click', () => CrawlFlixIntegration.copyKeys('ckResult')); } } static setupUIListeners() { // ClearKey result click handler const ckResult = document.getElementById('ckResult'); if (ckResult) { ckResult.addEventListener('click', function() { this.select(); navigator.clipboard.writeText(this.value); }); } } } // === CORS FETCH HELPER === window.corsFetch = (u, m, h, b) => { return new Promise((resolve, reject) => { chrome.tabs.sendMessage(targetIds[0], { type: "FETCH", u: u, m: m, h: h, b: b }, {frameId: targetIds[1]}, res => { resolve(res); }); }); }; // === INITIALIZATION === document.addEventListener('DOMContentLoaded', () => { EventManager.init(); CrawlFlixIntegration.loadSavedSettings(); CrawlFlixIntegration.saveSettings(); }); // Main initialization logic if (clearkey) { // ClearKey detected document.getElementById('noEME').style.display = 'none'; document.getElementById('ckHome').style.display = 'block'; document.getElementById('ckResult').value = clearkey; MPDDetector.updateDropdowns(); StatusManager.show('ClearKey content detected', 'success'); } else if (psshs.length) { // Widevine detected document.getElementById('noEME').style.display = 'none'; document.getElementById('home').style.display = 'block'; WidevineExtractor.autoSelect().then(async () => { EventManager.init(); await MPDDetector.updateDropdowns(); StatusManager.show('Widevine content detected and configured', 'success'); }); // Réduire la fréquence de refresh automatique setInterval(() => { MPDDetector.updateDropdowns(); // Ne se met à jour que si nécessaire }, 60000); // Augmenté à 10 secondes au lieu de 3 }