diff --git a/background.js b/background.js index 0449553..9408d4a 100644 --- a/background.js +++ b/background.js @@ -32,11 +32,11 @@ function updateBadge() { const hasMPD = window.detectedMPDs && window.detectedMPDs.length > 0; if (hasContent && hasMPD) { - browserAction.setBadgeText({ text: "●" }); - browserAction.setBadgeBackgroundColor({ color: "#dc3545" }); + browserAction.setBadgeText({ text: detectedMPDs.length.toString() }); + browserAction.setBadgeBackgroundColor({ color: "#22c55e" }); } else if (hasContent) { browserAction.setBadgeText({ text: "!" }); - browserAction.setBadgeBackgroundColor({ color: "#fd7e14" }); + browserAction.setBadgeTextColor({ color: "#fd7e14" }); } else { browserAction.setBadgeText({ text: "" }); } diff --git a/popup/main.html b/popup/main.html index 87ead62..7a8879a 100644 --- a/popup/main.html +++ b/popup/main.html @@ -142,7 +142,7 @@ CrawlFlix Server + placeholder="http://localhost:4200" value="http://192.168.1.230:6080/">
@@ -237,7 +237,7 @@
+ placeholder="http://localhost:4200" value="http://192.168.1.230:6080/">
diff --git a/popup/main.js b/popup/main.js index d3ad82e..ea2c342 100644 --- a/popup/main.js +++ b/popup/main.js @@ -111,6 +111,131 @@ class WidevineExtractor { } } +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 => ({ @@ -290,77 +415,212 @@ class SmartMPDSelector { } 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 - } + constructor() { + this.cache = new MPDCache(); + this.isUpdating = false; + } - static updateBadgeFromPopup() { - const backgroundPage = chrome.extension.getBackgroundPage(); - if (backgroundPage && backgroundPage.updateExtensionBadge) { - backgroundPage.updateExtensionBadge(); - } - } + 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 updateDropdowns() { - const mpdUrls = this.detectMPDUrls(); - const selects = ['mpdSelect', 'mpdSelectCK']; - - selects.forEach(selectId => { - const select = document.getElementById(selectId); - if (!select) return; + async updateDropdowns() { + // Éviter les mises à jour simultanées + if (this.isUpdating) { + console.log('MPD update already in progress, skipping...'); + 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); - }); - }); + 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; + } - // Auto-sélection intelligente - if (mpdUrls.length > 0) { - SmartMPDSelector.updateUIWithBestMPD(mpdUrls); - } + 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); + } + }); - // Mettre à jour le status avec l'analyse - StatusManager.show( - `Found ${mpdUrls.length} MPD(s) - Auto-selected best candidate`, - 'info' - ); - } + // 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' + ); - static formatUrlForDisplay(url) { - return SmartMPDSelector.truncateUrl(url, 80); - } + 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) { @@ -617,7 +877,17 @@ class EventManager { ['refreshMPDs', 'refreshMPDsCK'].forEach(buttonId => { const button = document.getElementById(buttonId); if (button) { - button.addEventListener('click', () => MPDDetector.updateDropdowns()); + button.addEventListener('click', async () => { + button.disabled = true; + button.innerHTML = ' Refreshing...'; + + try { + await MPDDetector.forceUpdate(); + } finally { + button.disabled = false; + button.innerHTML = ' Refresh'; + } + }); } }); @@ -693,15 +963,14 @@ if (clearkey) { document.getElementById('noEME').style.display = 'none'; document.getElementById('home').style.display = 'block'; - // IMPORTANT: Faire autoSelect avant d'initialiser les event listeners - WidevineExtractor.autoSelect().then(() => { + WidevineExtractor.autoSelect().then(async () => { EventManager.init(); - MPDDetector.updateDropdowns(); + await MPDDetector.updateDropdowns(); StatusManager.show('Widevine content detected and configured', 'success'); }); - // Auto-refresh MPD detection periodically + // Réduire la fréquence de refresh automatique setInterval(() => { - MPDDetector.updateDropdowns(); - }, 3000); + MPDDetector.updateDropdowns(); // Ne se met à jour que si nécessaire + }, 60000); // Augmenté à 10 secondes au lieu de 3 } \ No newline at end of file