// === 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 = {}; // === WIDEVINE KEY EXTRACTION === class WidevineExtractor { static async extractKeys() { const guessButton = document.getElementById("guess"); const resultTextarea = document.getElementById('result'); try { // 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); } } } // === SMART MPD SELECTOR === 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 { 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 } static updateBadgeFromPopup() { const backgroundPage = chrome.extension.getBackgroundPage(); if (backgroundPage && backgroundPage.updateExtensionBadge) { backgroundPage.updateExtensionBadge(); } } static updateDropdowns() { const mpdUrls = this.detectMPDUrls(); const selects = ['mpdSelect', 'mpdSelectCK']; selects.forEach(selectId => { const select = document.getElementById(selectId); if (!select) return; select.innerHTML = ''; // Utiliser le smart selector pour trier les MPDs const rankedMPDs = SmartMPDSelector.scoreAndRankMPDs(mpdUrls); rankedMPDs.forEach((mpdInfo, index) => { const option = document.createElement('option'); option.value = mpdInfo.url; // Ajouter des indicateurs visuels const prefix = index === 0 ? '⭐ [BEST] ' : mpdInfo.score > 0 ? '✓ ' : '⚠ '; option.textContent = prefix + this.formatUrlForDisplay(mpdInfo.url); // Ajouter des classes CSS pour styling if (index === 0) { option.style.fontWeight = 'bold'; option.style.color = '#28a745'; } else if (mpdInfo.score <= 0) { option.style.color = '#dc3545'; } select.appendChild(option); }); }); // Auto-sélection intelligente if (mpdUrls.length > 0) { SmartMPDSelector.updateUIWithBestMPD(mpdUrls); } // Mettre à jour le status avec l'analyse StatusManager.show( `Found ${mpdUrls.length} MPD(s) - Auto-selected best candidate`, 'info' ); } static formatUrlForDisplay(url) { return SmartMPDSelector.truncateUrl(url, 80); } } // === CRAWLFLIX INTEGRATION === class CrawlFlixIntegration { static async sendToCrawlFlix(isClearchey = false) { const suffix = isClearchey ? 'CK' : ''; const crawlFlixUrl = document.getElementById(`crawlFlixUrl${suffix}`).value || 'http://localhost:3000'; 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('Processing MPD and sending to CrawlFlix...', 'info', suffix); // Parse and validate keys const keys = this.parseKeys(resultTextarea.value); if (keys.length === 0) { StatusManager.show('No valid keys found in the format key:value', 'error', suffix); return; } // Test CrawlFlix connection and process MPD const mpdData = await this.processMPD(crawlFlixUrl, mpdUrl); // Create success message const message = `✓ Successfully sent to CrawlFlix! ${keys.length} key(s) • ${mpdData.videoTracks.length} video track(s) • ${mpdData.audioTracks.length} audio track(s) • ${mpdData.subtitles.length} subtitle(s)`; StatusManager.show(message, 'success', suffix); // Open CrawlFlix with pre-filled data this.openCrawlFlixTab(crawlFlixUrl, mpdUrl, resultTextarea.value); } 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) { const params = new URLSearchParams({ mpdUrl: mpdUrl, keys: keysText, source: 'widevine-plugin' }); const crawlFlixTab = `${crawlFlixUrl}?${params.toString()}`; chrome.tabs.create({ url: 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 = button.dataset.originalText || button.innerHTML; } } 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', () => MPDDetector.updateDropdowns()); } }); // 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'); MPDDetector.updateBadgeFromPopup(); } else if (psshs.length) { // Widevine detected document.getElementById('noEME').style.display = 'none'; document.getElementById('home').style.display = 'block'; WidevineExtractor.autoSelect(); MPDDetector.updateDropdowns(); StatusManager.show('Widevine content detected', 'success'); MPDDetector.updateBadgeFromPopup(); // Auto-refresh MPD detection periodically setInterval(() => { MPDDetector.updateDropdowns(); }, 3000); }