Working version

This commit is contained in:
Joris Bertomeu
2025-08-26 09:08:04 +02:00
commit ac0dc8286b
63 changed files with 2741 additions and 0 deletions

40
popup/drawList.js Normal file
View File

@@ -0,0 +1,40 @@
let psshs=chrome.extension.getBackgroundPage().psshs;
let requests=chrome.extension.getBackgroundPage().requests;
var userInputs={};
document.getElementById('psshButton').addEventListener("click", () => drawList(psshs, 'pssh'));
document.getElementById('licenseButton').addEventListener("click", () => drawList(requests.map(r => r['url']), 'license'));
function writeListElement(items, outputVar, searchStr) {
document.getElementById("items").innerHTML = '';
items.forEach((item, index) => {
if (!searchStr || item.includes(searchStr)) {
const li = document.createElement('li');
li.textContent = item;
li.addEventListener('click', () => itemSelected(index, item, outputVar));
document.getElementById("items").appendChild(li);
}
});
}
function drawList(items, outputVar) {
document.getElementById('home').style.display='none';
document.getElementById('chooserContainer').style.display='grid';
document.getElementById('toggleHistory').style.display='none';
writeListElement(items, outputVar, null)
document.getElementById("chooserSearch").addEventListener('input', event => {
const searchStr = event.target.value.toLowerCase();
writeListElement(items, outputVar, searchStr)
});
}
function itemSelected(index, item, outputVar){
userInputs[outputVar]=index;
document.getElementById(outputVar).value=item;
document.getElementById('chooserContainer').style.display='none';
document.getElementById('home').style.display='grid';
document.getElementById('toggleHistory').style.display='grid';
document.getElementById("chooserSearch").value=""
}

15
popup/editScheme.js Normal file
View File

@@ -0,0 +1,15 @@
document.getElementById("editSchemeButton").addEventListener("click", () => {
document.getElementById("editSchemeContainer").style.display = "grid"
document.getElementById('home').style.display='none';
document.getElementById('toggleHistory').style.display='none';
})
document.getElementById("editSchemeOK").addEventListener("click", () => {
document.getElementById("editSchemeContainer").style.display = "none"
document.getElementById('home').style.display='grid';
document.getElementById('toggleHistory').style.display='grid';
})
document.getElementById("schemeSelect").addEventListener("input", async () => {
document.getElementById("schemeCode").value = await fetch(`/python/schemes/${document.getElementById("schemeSelect").value}.py`).then(res=>res.text())
})

17
popup/history.html Normal file
View File

@@ -0,0 +1,17 @@
<html>
<head>
<meta charset="UTF-8">
<title>Widevine L3 Guessor - History</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="history">
<a href="./main.html"><button>🔙 Back</button></a>
<button id="saveHistory">💾 Save History</button>
<button id="clearHistory">❌ Clear History</button>
<div id="histDisp"></div>
</div>
</body>
<script src="/libs/jsonview.js"></script>
<script src="./history.js"></script>
</html>

28
popup/history.js Normal file
View File

@@ -0,0 +1,28 @@
let psshs=chrome.extension.getBackgroundPage().psshs;
function showHistory(){
chrome.storage.local.get(null, (data => {
let tree=jsonview.renderJSON(JSON.stringify(data), document.getElementById('histDisp'));
jsonview.toggleNode(tree);
}));
}
function saveHistory(){
chrome.storage.local.get(null, (data => {
let blob = new Blob([JSON.stringify(data, null, "\t")], {type: "text/plain"});
let a = document.createElement('a');
a.download = 'wvgHistory.json';
a.href = URL.createObjectURL(blob);
a.click();
}));
}
function clearHistory(){
if(confirm("Do you really want to clear history?")){
chrome.storage.local.clear();
document.getElementById('histDisp').innerHTML="";
}
}
document.getElementById('saveHistory').addEventListener("click", saveHistory);
document.getElementById('clearHistory').addEventListener("click", clearHistory);
showHistory()

312
popup/main.html Normal file
View File

@@ -0,0 +1,312 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Widevine L3 Guessor 2024</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<script src="/libs/pyodide/pyodide.js"></script>
<style>
body { min-height: 100vh; }
.section-card { margin-bottom: 1.5rem; }
.main-container { max-width: 1200px; }
.extraction-section { background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); }
.crawlflix-section { background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%); }
.clearkey-section { background: linear-gradient(135deg, #fff3e0 0%, #ffcc02 20%); }
.form-label { font-weight: 600; }
.card-header h5 { margin: 0; }
.status-area { min-height: 40px; }
</style>
</head>
<body class="bg-light">
<div class="container main-container p-4">
<!-- Header -->
<div class="row mb-4">
<div class="col-12">
<div class="text-center">
<h2 class="mb-3"><i class="fas fa-shield-alt text-primary"></i> Widevine L3 Guessor 2024</h2>
<p class="text-muted mb-3">Extract Widevine keys and send directly to CrawlFlix for seamless content processing</p>
<a href="./history.html" class="btn btn-outline-secondary">
<i class="fas fa-history"></i> Show History
</a>
</div>
</div>
</div>
<!-- No EME Detection -->
<div id="noEME" class="row">
<div class="col-12">
<div class="alert alert-warning text-center py-4">
<h4><i class="fas fa-exclamation-triangle"></i> No Widevine Content Detected</h4>
<p class="mb-0">Open a widevine-protected website and try again!</p>
</div>
</div>
</div>
<!-- Main Content Grid -->
<div id="home" class="hidden">
<div class="row">
<!-- Key Extraction Column -->
<div class="col-lg-6 col-xl-5">
<div class="card section-card extraction-section">
<div class="card-header py-3">
<h5><i class="fas fa-key text-success"></i> Key Extraction</h5>
</div>
<div class="card-body p-4">
<form id="wvForm">
<div class="mb-3">
<label for="pssh" class="form-label">PSSH</label>
<div class="input-group">
<input type="text" id="pssh" class="form-control font-monospace" disabled>
<button type="button" id="psshButton" class="btn btn-outline-primary">
<i class="fas fa-mouse-pointer"></i> Select
</button>
</div>
</div>
<div class="mb-3">
<label for="license" class="form-label">License URL</label>
<div class="input-group">
<input type="text" id="license" class="form-control font-monospace" disabled>
<button type="button" id="licenseButton" class="btn btn-outline-primary">
<i class="fas fa-mouse-pointer"></i> Select
</button>
</div>
</div>
<div class="row mb-4">
<div class="col-8">
<label for="schemeSelect" class="form-label">Challenge Scheme</label>
<select id="schemeSelect" class="form-select">
<option value="Amazon">Amazon</option>
<option value="Allente">Allente</option>
<option value="CanalPlusVOD">CanalPlus (VOD)</option>
<option value="CanalPlusLive">CanalPlus (Live)</option>
<option value="Comcast">Comcast Xfinity</option>
<option value="CommonWV" selected>CommonWV</option>
<option value="DRMToday">DRMToday</option>
<option value="Fantop">Fantop</option>
<option value="GlobalTV">GlobalTV</option>
<option value="Heuristic">Heuristic</option>
<option value="moTV">moTV</option>
<option value="NosTV">NosTV</option>
<option value="oqee">Oqee</option>
<option value="PolSatBoxGo">PolSatBoxGo</option>
<option value="RedBee">Red Bee Media</option>
<option value="Sling">Sling</option>
<option value="thePlatform">thePlatform</option>
<option value="VdoCipher">VdoCipher</option>
<option value="VUDRM">VUDRM</option>
<option value="Vodafone">Vodafone</option>
<option value="Youku">Youku</option>
<option value="YouTube">YouTube</option>
</select>
</div>
<div class="col-4 d-flex align-items-end">
<button type="button" id="editSchemeButton" class="btn btn-outline-secondary w-100">
<i class="fas fa-edit"></i> Edit
</button>
</div>
</div>
<button type="button" id="guess" class="btn btn-primary btn-lg w-100 mb-4">
<i class="fas fa-magic me-2"></i> Extract Widevine Keys
</button>
<div>
<label for="result" class="form-label">Extracted Keys</label>
<textarea id="result" class="form-control font-monospace" rows="8"
placeholder="Extracted keys will appear here..."></textarea>
</div>
</form>
</div>
</div>
</div>
<!-- CrawlFlix Integration Column -->
<div class="col-lg-6 col-xl-7">
<div class="card section-card crawlflix-section">
<div class="card-header py-3">
<div class="d-flex justify-content-between align-items-center">
<h5><i class="fas fa-download text-info"></i> CrawlFlix Integration</h5>
<span class="badge bg-info">Auto-Workflow</span>
</div>
</div>
<div class="card-body p-4">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="crawlFlixUrl" class="form-label">
<i class="fas fa-server"></i> CrawlFlix Server
</label>
<input type="text" id="crawlFlixUrl" class="form-control"
placeholder="http://localhost:3000" value="http://localhost:3000">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="mpdSelect" class="form-label">
<i class="fas fa-file-video"></i> Detected MPD Manifests
</label>
<div class="input-group">
<select id="mpdSelect" class="form-select">
<option value="">-- Auto-detected MPDs --</option>
</select>
<button type="button" id="refreshMPDs" class="btn btn-success">
<i class="fas fa-sync-alt"></i>
</button>
</div>
</div>
</div>
</div>
<div class="mb-4">
<label for="mpdUrl" class="form-label">MPD URL (Manual Entry)</label>
<input type="text" id="mpdUrl" class="form-control font-monospace"
placeholder="Or paste MPD URL manually...">
</div>
<div class="row mb-4">
<div class="col-md-6">
<button type="button" id="sendToCrawlFlix" class="btn btn-primary btn-lg w-100 mb-2">
<i class="fas fa-paper-plane me-2"></i> Send to CrawlFlix
</button>
</div>
<div class="col-md-6">
<button type="button" id="copyKeys" class="btn btn-outline-secondary btn-lg w-100 mb-2">
<i class="fas fa-copy me-2"></i> Copy Keys Only
</button>
</div>
</div>
<div id="crawlFlixStatus" class="status-area"></div>
<!-- Workflow Steps -->
<div class="mt-4 p-3 bg-light rounded">
<h6 class="text-muted mb-3"><i class="fas fa-list-ol"></i> Automated Workflow</h6>
<div class="row text-center">
<div class="col-3">
<div class="workflow-step">
<i class="fas fa-shield-alt fa-2x text-primary mb-2"></i>
<p class="small mb-0">Extract Keys</p>
</div>
</div>
<div class="col-3">
<div class="workflow-step">
<i class="fas fa-search fa-2x text-success mb-2"></i>
<p class="small mb-0">Detect MPD</p>
</div>
</div>
<div class="col-3">
<div class="workflow-step">
<i class="fas fa-paper-plane fa-2x text-info mb-2"></i>
<p class="small mb-0">Send Data</p>
</div>
</div>
<div class="col-3">
<div class="workflow-step">
<i class="fas fa-download fa-2x text-warning mb-2"></i>
<p class="small mb-0">Start Download</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ClearKey Section -->
<div id="ckHome" class="hidden">
<div class="card section-card">
<div class="card-header py-2">
<h6 class="mb-0"><i class="fas fa-unlock text-warning"></i> ClearKey Detected</h6>
</div>
<div class="card-body p-3">
<label for="ckResult" class="form-label">ClearKey Result:</label>
<textarea id="ckResult" class="form-control font-monospace" rows="6" placeholder="ClearKey data will appear here..."></textarea>
<!-- CrawlFlix for ClearKey -->
<div class="mt-3 pt-3 border-top">
<h6><i class="fas fa-download text-info"></i> Send to CrawlFlix</h6>
<div class="row g-2 mb-2">
<div class="col-12">
<label for="crawlFlixUrlCK" class="form-label">CrawlFlix Server</label>
<input type="text" id="crawlFlixUrlCK" class="form-control form-control-sm"
placeholder="http://localhost:3000" value="http://localhost:3000">
</div>
</div>
<div class="row g-2 mb-3">
<div class="col-10">
<label for="mpdSelectCK" class="form-label">MPD Manifest</label>
<select id="mpdSelectCK" class="form-select form-select-sm">
<option value="">-- Detected MPDs --</option>
</select>
</div>
<div class="col-2 d-flex align-items-end">
<button type="button" id="refreshMPDsCK" class="btn btn-success btn-sm w-100">
<i class="fas fa-sync-alt"></i>
</button>
</div>
</div>
<div class="mb-3">
<input type="text" id="mpdUrlCK" class="form-control form-control-sm"
placeholder="Or paste MPD URL manually...">
</div>
<div class="d-grid gap-2 d-md-flex">
<button type="button" id="sendToCrawlFlixCK" class="btn btn-primary btn-sm flex-fill">
<i class="fas fa-paper-plane"></i> Send to CrawlFlix
</button>
<button type="button" id="copyKeysCK" class="btn btn-outline-secondary btn-sm flex-fill">
<i class="fas fa-copy"></i> Copy Keys
</button>
</div>
<div id="crawlFlixStatusCK" class="mt-2"></div>
</div>
</div>
</div>
</div>
<!-- Hidden containers -->
<div id="chooserContainer" class="hidden">
<div class="card">
<div class="card-body p-2">
<input type="text" id="chooserSearch" class="form-control form-control-sm mb-2" placeholder="Search">
<ul id="items" class="list-group list-group-flush"></ul>
</div>
</div>
</div>
<div id="editSchemeContainer" class="hidden">
<div class="card">
<div class="card-header py-2">
<h6 class="mb-0">Edit Scheme</h6>
</div>
<div class="card-body p-3">
<textarea id="schemeCode" class="form-control font-monospace" rows="8"></textarea>
<button type="button" id="editSchemeOK" class="btn btn-primary btn-sm mt-2">OK</button>
</div>
</div>
</div>
<div id="updateNotice" class="alert alert-info alert-sm hidden">
<i class="fas fa-info-circle"></i>
Version =VER= update available!
<a href="https://github.com/FoxRefire/wvg/archive/=HASH=.zip" class="alert-link">Download</a>
</div>
</div>
<script src="./main.js" type="module"></script>
<script src="./drawList.js"></script>
<script src="./editScheme.js"></script>
<script src="./updateNotice.js"></script>
</body>
</html>

701
popup/main.js Normal file
View File

@@ -0,0 +1,701 @@
// === 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 = '<option value="">-- Auto-detected MPDs --</option>';
// 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 = `
<i class="fas ${this.getIcon(type)} me-2"></i>
${message.replace(/\n/g, '<br>')}
<button type="button" class="btn-close btn-close-sm" data-bs-dismiss="alert"></button>
`;
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 = '<i class="fas fa-spinner fa-spin me-2"></i>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);
}

99
popup/style.css Normal file
View File

@@ -0,0 +1,99 @@
.hidden {
display: none;
}
html, body {
display: grid;
height: 100%;
width: 100%;
margin: 0; /* Reset default margin */
padding: 0; /* Reset default padding */
}
body {
background-size: cover;
background-repeat: no-repeat;
background-position: center;
background: linear-gradient(45deg, #0d364c, #062535, #02141e);
grid-template-rows: auto 1fr auto;
}
#noEME {
justify-self: center;
align-self: center;
color: white;
}
#updateNotice {
justify-self: center;
align-self: end;
color: white;
}
#updateNotice a{
color: aqua;
}
#wvForm {
display: grid;
grid-template-rows: auto;
color: white;
}
#wvForm label {
justify-self: center;
}
#pssh, #license, #schemeSelect {
width: 80%;
justify-self: center;
}
#psshButton, #licenseButton, #editSchemeButton {
width: 20%;
justify-self: center;
}
#toggleHistory {
display: grid;
}
#toggleHistory button {
width: 20%;
height: auto;
}
#guess {
width: 20%;
justify-self: center;
margin-top: 5%;
}
#result {
width: 90%;
overflow-y: scroll;
overflow-x: hidden;
resize: none;
justify-self: center;
}
#chooserContainer {
color: white;
}
#ckHome h3, label {
color: white;
justify-self: center;
}
#ckHome label {
align-self: end;
}
#ckResult {
width: 90%;
overflow-y: scroll;
overflow-x: hidden;
resize: none;
justify-self: center;
}

10
popup/updateNotice.js Normal file
View File

@@ -0,0 +1,10 @@
document.addEventListener('DOMContentLoaded', async function() {
cManifest=await fetch("/manifest.json").then(r=>r.json());
rManifest=await fetch("https://raw.githubusercontent.com/FoxRefire/wvg/next/manifest.json").then(r=>r.json());
if(cManifest.version < rManifest.version){
let notice = document.getElementById("updateNotice");
notice.style.display='block';
notice.innerHTML = notice.innerHTML.replace("=VER=", rManifest.version);
notice.innerHTML = notice.innerHTML.replace("=HASH=", rManifest.version_name);
}
});