Some lil fixes + select best MPB based on video track duration

This commit is contained in:
Joris Bertomeu
2025-08-26 14:09:00 +02:00
parent 1851ca9241
commit 424f2e9c52
3 changed files with 344 additions and 75 deletions

View File

@@ -32,11 +32,11 @@ function updateBadge() {
const hasMPD = window.detectedMPDs && window.detectedMPDs.length > 0; const hasMPD = window.detectedMPDs && window.detectedMPDs.length > 0;
if (hasContent && hasMPD) { if (hasContent && hasMPD) {
browserAction.setBadgeText({ text: "●" }); browserAction.setBadgeText({ text: detectedMPDs.length.toString() });
browserAction.setBadgeBackgroundColor({ color: "#dc3545" }); browserAction.setBadgeBackgroundColor({ color: "#22c55e" });
} else if (hasContent) { } else if (hasContent) {
browserAction.setBadgeText({ text: "!" }); browserAction.setBadgeText({ text: "!" });
browserAction.setBadgeBackgroundColor({ color: "#fd7e14" }); browserAction.setBadgeTextColor({ color: "#fd7e14" });
} else { } else {
browserAction.setBadgeText({ text: "" }); browserAction.setBadgeText({ text: "" });
} }

View File

@@ -142,7 +142,7 @@
<i class="fas fa-server"></i> CrawlFlix Server <i class="fas fa-server"></i> CrawlFlix Server
</label> </label>
<input type="text" id="crawlFlixUrl" class="form-control" <input type="text" id="crawlFlixUrl" class="form-control"
placeholder="http://localhost:4200" value="http://localhost:4200"> placeholder="http://localhost:4200" value="http://192.168.1.230:6080/">
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
@@ -237,7 +237,7 @@
<div class="col-12"> <div class="col-12">
<label for="crawlFlixUrlCK" class="form-label">CrawlFlix Server</label> <label for="crawlFlixUrlCK" class="form-label">CrawlFlix Server</label>
<input type="text" id="crawlFlixUrlCK" class="form-control form-control-sm" <input type="text" id="crawlFlixUrlCK" class="form-control form-control-sm"
placeholder="http://localhost:4200" value="http://localhost:4200"> placeholder="http://localhost:4200" value="http://192.168.1.230:6080/">
</div> </div>
</div> </div>

View File

@@ -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 { class SmartMPDSelector {
static scoreAndRankMPDs(mpdUrls) { static scoreAndRankMPDs(mpdUrls) {
const scoredMPDs = mpdUrls.map(url => ({ const scoredMPDs = mpdUrls.map(url => ({
@@ -290,6 +415,11 @@ class SmartMPDSelector {
} }
class MPDDetector { class MPDDetector {
constructor() {
this.cache = new MPDCache();
this.isUpdating = false;
}
static detectMPDUrls() { static detectMPDUrls() {
const backgroundPage = chrome.extension.getBackgroundPage(); const backgroundPage = chrome.extension.getBackgroundPage();
const detectedMPDs = backgroundPage.detectedMPDs || []; const detectedMPDs = backgroundPage.detectedMPDs || [];
@@ -302,40 +432,135 @@ class MPDDetector {
.filter(url => !url.includes('/chunk-') && !url.includes('/segment-')); // Filter segments .filter(url => !url.includes('/chunk-') && !url.includes('/segment-')); // Filter segments
} }
static updateBadgeFromPopup() { async updateDropdowns() {
const backgroundPage = chrome.extension.getBackgroundPage(); // Éviter les mises à jour simultanées
if (backgroundPage && backgroundPage.updateExtensionBadge) { if (this.isUpdating) {
backgroundPage.updateExtensionBadge(); 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;
} }
} }
static updateDropdowns() { async processNewMPDs(urls) {
const mpdUrls = this.detectMPDUrls(); 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']; const selects = ['mpdSelect', 'mpdSelectCK'];
selects.forEach(selectId => { selects.forEach(selectId => {
const select = document.getElementById(selectId); const select = document.getElementById(selectId);
if (!select) return; if (!select) return;
select.innerHTML = '<option value="">-- Auto-detected MPDs --</option>'; select.innerHTML = '<option value="">-- Validated MPDs --</option>';
// Utiliser le smart selector pour trier les MPDs validMPDs.forEach((mpdInfo, index) => {
const rankedMPDs = SmartMPDSelector.scoreAndRankMPDs(mpdUrls);
rankedMPDs.forEach((mpdInfo, index) => {
const option = document.createElement('option'); const option = document.createElement('option');
option.value = mpdInfo.url; option.value = mpdInfo.url;
// Ajouter des indicateurs visuels // Indicateurs visuels améliorés
const prefix = index === 0 ? '⭐ [BEST] ' : let prefix = '';
mpdInfo.score > 0 ? '✓ ' : '⚠ '; 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.textContent = prefix + this.formatUrlForDisplay(mpdInfo.url);
option.title = `Score: ${mpdInfo.score} | ${mpdInfo.reason}`;
// Ajouter des classes CSS pour styling // Styling
if (index === 0) { if (index === 0) {
option.style.fontWeight = 'bold'; option.style.fontWeight = 'bold';
option.style.color = '#28a745'; option.style.color = '#28a745';
} else if (mpdInfo.duration > 1000) { // Plus de 16 minutes
option.style.color = '#17a2b8';
} else if (mpdInfo.score <= 0) { } else if (mpdInfo.score <= 0) {
option.style.color = '#dc3545'; option.style.color = '#dc3545';
} }
@@ -344,22 +569,57 @@ class MPDDetector {
}); });
}); });
// Auto-sélection intelligente // Auto-sélection du meilleur
if (mpdUrls.length > 0) { if (validMPDs.length > 0) {
SmartMPDSelector.updateUIWithBestMPD(mpdUrls); this.selectBestMPD(validMPDs[0]);
} }
// Mettre à jour le status avec l'analyse // Mettre à jour le status
StatusManager.show( const totalDetected = MPDDetector.detectMPDUrls().length;
`Found ${mpdUrls.length} MPD(s) - Auto-selected best candidate`, const validCount = validMPDs.length;
'info' const invalidCount = totalDetected - validCount;
);
let statusMessage = `Found ${validCount} valid MPD(s)`;
if (invalidCount > 0) {
statusMessage += ` (${invalidCount} filtered out)`;
} }
static formatUrlForDisplay(url) { 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); 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 === // === CRAWLFLIX INTEGRATION ===
class CrawlFlixIntegration { class CrawlFlixIntegration {
@@ -617,7 +877,17 @@ class EventManager {
['refreshMPDs', 'refreshMPDsCK'].forEach(buttonId => { ['refreshMPDs', 'refreshMPDsCK'].forEach(buttonId => {
const button = document.getElementById(buttonId); const button = document.getElementById(buttonId);
if (button) { if (button) {
button.addEventListener('click', () => MPDDetector.updateDropdowns()); button.addEventListener('click', async () => {
button.disabled = true;
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Refreshing...';
try {
await MPDDetector.forceUpdate();
} finally {
button.disabled = false;
button.innerHTML = '<i class="fas fa-sync"></i> Refresh';
}
});
} }
}); });
@@ -693,15 +963,14 @@ if (clearkey) {
document.getElementById('noEME').style.display = 'none'; document.getElementById('noEME').style.display = 'none';
document.getElementById('home').style.display = 'block'; document.getElementById('home').style.display = 'block';
// IMPORTANT: Faire autoSelect avant d'initialiser les event listeners WidevineExtractor.autoSelect().then(async () => {
WidevineExtractor.autoSelect().then(() => {
EventManager.init(); EventManager.init();
MPDDetector.updateDropdowns(); await MPDDetector.updateDropdowns();
StatusManager.show('Widevine content detected and configured', 'success'); StatusManager.show('Widevine content detected and configured', 'success');
}); });
// Auto-refresh MPD detection periodically // Réduire la fréquence de refresh automatique
setInterval(() => { setInterval(() => {
MPDDetector.updateDropdowns(); MPDDetector.updateDropdowns(); // Ne se met à jour que si nécessaire
}, 3000); }, 60000); // Augmenté à 10 secondes au lieu de 3
} }