Sauvegarde/restauration chiffrée du profil (.sama) + Next/Prev auto & contrôles clavier adaptatifs
// ==UserScript==
// @name anime-sama Plus
// @namespace http://tampermonkey.net/
// @version 0.1.7.3
// @description Sauvegarde/restauration chiffrée du profil (.sama) + Next/Prev auto & contrôles clavier adaptatifs
// @author MASTERD
// @include /^https?\:\/\/.*\.anime-sama\..*\/.*$/
// @include /^https?\:\/\/.*\anime-sama\..*\/.*$/
// @match *://*.callistanise.com/*
// @match *://*.dingtezuni.com/*
// @match *://*.embed4me.com/*
// @match *://*.oneupload.to/*
// @match *://*.oneupload.net/*
// @match *://*.sendvid.com/*
// @match *://*.sibnet.ru/*
// @match *://*.smoothpre.com/*
// @match *://*.vk.com/*
// @match *://*.vkvideo.ru/*
// @include /^https?\:\/\/.*vidmoly\..*\/.*$/
// @icon https://www.google.com/s2/favicons?sz=64&domain=anime-sama.org
// @grant none
// ==/UserScript==
(function () {
'use strict';
// --------------------------------------------------------------------------
//************** CONFIGURATION GÉNÉRALE **************/
const PREFIX = 'ASP';
const SCRIPT_CONFIG = {
DEBUG: true,
VERSION: '0.1.7.3',
P_DOMAINS: ['anime-sama'],
C_DOMAINS: ['sendvid.com', 'exemple.com']
};
//************** UTILITAIRES **************/
const Utils = {
log: (...args) => SCRIPT_CONFIG.DEBUG && console.log(`%c[${PREFIX}:LOG]`, 'color:#F47521;font-weight:bold', ...args),
error: (...args) => console.error(`%c[${PREFIX}:ERROR]`, 'color:red;font-weight:bold', ...args),
warn: (...args) => SCRIPT_CONFIG.DEBUG && console.warn(`%c[${PREFIX}:WARN]`, 'color:orange;font-weight:bold', ...args),
info: (...args) => SCRIPT_CONFIG.DEBUG && console.log(`%c[${PREFIX}:INFO]`, 'color:#2196F3;font-weight:bold', ...args),
skip: (...args) => SCRIPT_CONFIG.DEBUG && console.log(`%c[${PREFIX}:SKIP]`, 'color:#4CAF50;font-weight:bold', ...args),
msg: (...args) => SCRIPT_CONFIG.DEBUG && console.log(`%c[${PREFIX}:MSG]`, 'color:#9C27B0;font-weight:bold', ...args),
dom: (...args) => SCRIPT_CONFIG.DEBUG && console.log(`%c[${PREFIX}:DOM]`, 'color:#FF5722;font-weight:bold', ...args),
key: (...args) => SCRIPT_CONFIG.DEBUG && console.log(`%c[${PREFIX}:KEY]`, 'color:#00BCD4;font-weight:bold', ...args),
nav: (...args) => SCRIPT_CONFIG.DEBUG && console.log(`%c[${PREFIX}:NAV]`, 'color:#795548;font-weight:bold', ...args),
debounce: (func, delay) => {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), delay);
};
},
waitForElement: (selector, timeout = 10000) => {
return new Promise((resolve, reject) => {
const el = document.querySelector(selector);
if (el) return resolve(el);
const observer = new MutationObserver((_, obs) => {
const found = document.querySelector(selector);
if (found) {
obs.disconnect();
resolve(found);
}
});
observer.observe(document.documentElement, { childList: true, subtree: true });
setTimeout(() => { observer.disconnect(); reject(new Error(`Timeout: ${selector}`)); }, timeout);
});
},
keyToSymbol: (key) => {
const KEY_SYMBOLS = {
ArrowLeft: '←', ArrowRight: '→', ArrowUp: '↑', ArrowDown: '↓',
Enter: '⏎', Escape: 'Esc', Tab: '⇥', Shift: '⇧',
Control: 'Ctrl', Alt: 'Alt',
Meta: navigator.platform.toUpperCase().includes('MAC') ? '⌘' : '❖',
' ': '────────', Space: '────────',
Backspace: '⌫', Delete: '⌦', Insert: 'Ins',
Home: 'Home', End: 'End', PageUp: 'Pg↑', PageDown: 'Pg↓',
CapsLock: '⇪', Dead: '◌',
AudioVolumeUp: '🔊', AudioVolumeDown: '🔉', AudioVolumeMute: '🔇',
MediaPlayPause: '⏯', MediaTrackNext: '⏭', MediaTrackPrevious: '⏮'
};
if (KEY_SYMBOLS[key]) return KEY_SYMBOLS[key];
if (key.length === 1 && key.match(/[a-z]/i)) return key.toUpperCase();
return key;
}
};
Utils.log(`[${PREFIX}] v${SCRIPT_CONFIG.VERSION} — init | origin: ${location.origin} | context: ${window.self === window.top ? 'TOP' : 'IFRAME'}`);
// --------------------------------------------------------------------------
// UI - Choix restauration (Remplacer/Annuler)
function showChoiceDialog() {
Utils.dom('showChoiceDialog → ouverture dialogue choix restauration');
return new Promise(resolve => {
const overlay = document.createElement('div');
Object.assign(overlay.style, {
position: 'fixed', inset: 0, backgroundColor: 'rgba(0,0,0,0.5)',
display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 10000
});
const box = document.createElement('div');
Object.assign(box.style, {
background: '#111', color: '#fff', padding: '20px', borderRadius: '10px',
width: 'min(92vw, 360px)', textAlign: 'center', fontFamily: 'sans-serif',
boxShadow: '0 10px 30px rgba(0,0,0,.4)'
});
box.innerHTML = '<p style="margin-bottom:12px;font-weight:700">Comment voulez-vous restaurer ?</p>';
const mk = (label, code, bg) => {
const b = document.createElement('button');
b.textContent = label;
Object.assign(b.style, {
margin: '0 8px', padding: '8px 12px', border: 'none', borderRadius: '6px',
cursor: 'pointer', fontWeight: 700, background: bg, color: '#fff'
});
b.onclick = () => {
Utils.dom(`showChoiceDialog → choix sélectionné : "${code}"`);
document.body.removeChild(overlay);
resolve(code);
};
return b;
};
box.appendChild(mk('Restaurer (Remplacer)', 'replace', '#e53935'));
box.appendChild(mk('Annuler', 'cancel', '#555'));
overlay.appendChild(box);
document.body.appendChild(overlay);
Utils.dom('showChoiceDialog → dialogue injecté dans le DOM');
});
}
// --------------------------------------------------------------------------
// UI - Mot de passe
function showPasswordDialog(mode) {
Utils.dom(`showPasswordDialog → ouverture dialogue mot de passe [mode: ${mode}]`);
return new Promise(resolve => {
const overlay = document.createElement('div');
Object.assign(overlay.style, {
position: 'fixed', inset: 0, backgroundColor: 'rgba(0,0,0,0.55)',
display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 10001
});
const box = document.createElement('div');
Object.assign(box.style, {
background: '#111', color: '#fff', padding: '24px', borderRadius: '12px',
width: 'min(92vw, 380px)', fontFamily: 'sans-serif',
boxShadow: '0 10px 30px rgba(0,0,0,.5)'
});
const title = mode === 'backup' ? 'Mot de passe de sauvegarde' : 'Mot de passe de restauration';
box.innerHTML = `
<p style="font-weight:700;margin-bottom:10px;text-align:center">${title}</p>
<input id="asplus-pass" type="password" autocomplete="current-password"
placeholder="(vide = SAMA)"
style="width:100%;padding:8px;border-radius:6px;border:1px solid #333;background:#0b0b0b;color:#fff;margin-bottom:10px"/>
<div id="asplus-risk-banner" style="
display:block;margin:10px 0 12px 0;padding:10px 12px;border-radius:10px;
border:2px solid #ff5252;background:linear-gradient(90deg,#3a0000,#180000);
box-shadow:0 0 0 2px rgba(255,82,82,.25) inset, 0 0 18px rgba(255,82,82,.2);">
<label for="asplus-remember" style="display:flex;gap:12px;align-items:flex-start;cursor:pointer;">
<input id="asplus-remember" type="checkbox" style="transform:scale(1.35);margin-top:2px"/>
<div>
<div style="color:#ff5252;font-weight:900;letter-spacing:.3px;text-transform:uppercase;font-size:14px;">
⚠️ MÉMORISER (LOCAL SANS CHIFFREMENT)
</div>
<div style="color:#ffb3b3;font-size:12px;margin-top:2px;line-height:1.25;">
Le mot de passe sera stocké tel quel dans ce navigateur.
N'activez que si vous comprenez le risque.
</div>
</div>
</label>
</div>`;
const btnRow = document.createElement('div');
btnRow.style.cssText = 'display:flex;gap:10px;justify-content:center;margin-top:8px';
const mkBtn = (label, bg, cb) => {
const b = document.createElement('button');
b.textContent = label;
Object.assign(b.style, {
padding: '8px 16px', border: 'none', borderRadius: '6px',
cursor: 'pointer', fontWeight: 700, background: bg, color: '#fff'
});
b.onclick = cb;
return b;
};
const submit = () => {
const pass = box.querySelector('#asplus-pass').value;
const remember = box.querySelector('#asplus-remember').checked;
Utils.dom(`showPasswordDialog → soumission [remember: ${remember}, pass: ${pass.length ? '(défini)' : '(vide → SAMA)'}]`);
document.body.removeChild(overlay);
resolve({ pass, remember });
};
btnRow.appendChild(mkBtn('Valider', '#4caf50', submit));
btnRow.appendChild(mkBtn('Annuler', '#555', () => {
Utils.dom('showPasswordDialog → annulé par l\'utilisateur');
document.body.removeChild(overlay);
resolve({ pass: null, remember: false });
}));
box.appendChild(btnRow);
overlay.appendChild(box);
document.body.appendChild(overlay);
Utils.dom('showPasswordDialog → dialogue injecté dans le DOM, focus sur le champ mot de passe');
box.querySelector('#asplus-pass').addEventListener('keydown', e => {
if (e.key === 'Enter') {
Utils.key('showPasswordDialog → touche Entrée détectée → soumission');
submit();
}
});
box.querySelector('#asplus-pass').focus();
});
}
async function getPassphrase(mode) {
Utils.log(`getPassphrase → [mode: ${mode}]`);
const sess = localStorage.getItem('asplus.passphrase');
if (sess && sess.length) {
Utils.info('getPassphrase → passphrase trouvée en localStorage (mémorisée), utilisation directe');
return sess;
}
Utils.info('getPassphrase → aucune passphrase mémorisée, ouverture dialogue');
const { pass, remember } = await showPasswordDialog(mode);
if (pass === null) {
Utils.warn('getPassphrase → dialogue annulé, utilisation de la passphrase par défaut "SAMA"');
}
const chosen = (pass && pass.length) ? pass : 'SAMA';
if (remember) {
Utils.warn('getPassphrase → passphrase mémorisée en localStorage (non chiffrée) ⚠️');
localStorage.setItem('asplus.passphrase', chosen);
}
Utils.info(`getPassphrase → passphrase retournée : ${chosen === 'SAMA' ? '"SAMA" (défaut)' : '(personnalisée)'}`);
return chosen;
}
// --------------------------------------------------------------------------
// Sauvegarde / Restauration (AES-GCM 256)
async function backupProfile() {
Utils.log('backupProfile → début de la sauvegarde du profil');
try {
const data = {};
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
data[key] = localStorage.getItem(key);
}
Utils.info(`backupProfile → ${Object.keys(data).length} clé(s) localStorage collectée(s)`);
const json = JSON.stringify(data);
const encoder = new TextEncoder();
const passphrase = await getPassphrase('backup');
Utils.info('backupProfile → passphrase obtenue, génération IV aléatoire');
const iv = crypto.getRandomValues(new Uint8Array(12));
const baseKey = await crypto.subtle.importKey('raw', encoder.encode(passphrase), { name: 'PBKDF2' }, false, ['deriveKey']);
Utils.info('backupProfile → clé PBKDF2 importée');
const aesKey = await crypto.subtle.deriveKey(
{ name: 'PBKDF2', salt: iv, iterations: 100000, hash: 'SHA-256' },
baseKey, { name: 'AES-GCM', length: 256 }, false, ['encrypt']
);
Utils.info('backupProfile → clé AES-GCM 256 dérivée (100 000 itérations PBKDF2)');
const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, aesKey, encoder.encode(json));
Utils.info(`backupProfile → chiffrement AES-GCM terminé [taille payload: ${12 + encrypted.byteLength} octets]`);
const payload = new Uint8Array(iv.byteLength + encrypted.byteLength);
payload.set(iv, 0);
payload.set(new Uint8Array(encrypted), iv.byteLength);
const blob = new Blob([payload], { type: 'application/vnd.animesama.backup' });
Utils.info('backupProfile → Blob .sama créé, ouverture du sélecteur de fichier');
await pickFileToSave(blob);
Utils.log('backupProfile → sauvegarde terminée avec succès ✓');
} catch (e) {
Utils.error('backupProfile → échec de la sauvegarde :', e);
}
}
async function pickFileToSave(blob) {
Utils.log('pickFileToSave → tentative d\'enregistrement du fichier');
if (window.showSaveFilePicker) {
Utils.info('pickFileToSave → API showSaveFilePicker disponible, utilisation du sélecteur natif');
const handle = await window.showSaveFilePicker({
suggestedName: `anime-sama_${new Date().toISOString().slice(0, 10)}.sama`,
types: [{ description: 'Backup Anime-Sama', accept: { 'application/vnd.animesama.backup': ['.sama'] } }]
});
const w = await handle.createWritable();
await w.write(blob);
await w.close();
Utils.log('pickFileToSave → fichier écrit via FileSystemWritableFileStream ✓');
} else {
Utils.warn('pickFileToSave → showSaveFilePicker indisponible, fallback via <a download>');
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `anime-sama_${new Date().toISOString().slice(0, 10)}.sama`;
a.click();
URL.revokeObjectURL(a.href);
Utils.log('pickFileToSave → téléchargement déclenché via lien temporaire ✓');
}
}
async function restoreProfile() {
Utils.log('restoreProfile → début de la restauration du profil');
try {
let file;
if (window.showOpenFilePicker) {
Utils.info('restoreProfile → API showOpenFilePicker disponible, utilisation du sélecteur natif');
const [handle] = await window.showOpenFilePicker({
types: [{ description: 'Backup Anime-Sama', accept: { 'application/vnd.animesama.backup': ['.sama'] } }]
});
file = await handle.getFile();
} else {
Utils.warn('restoreProfile → showOpenFilePicker indisponible, fallback via <input type="file">');
file = await new Promise(resolve => {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.sama';
input.onchange = () => resolve(input.files[0]);
input.click();
});
}
if (!file) {
Utils.warn('restoreProfile → aucun fichier sélectionné, abandon');
return;
}
Utils.info(`restoreProfile → fichier sélectionné : "${file.name}" [${file.size} octets]`);
const buf = await file.arrayBuffer();
const arr = new Uint8Array(buf);
const iv = arr.slice(0, 12);
const encrypted = arr.slice(12);
Utils.info(`restoreProfile → IV extrait (12 octets), données chiffrées : ${encrypted.byteLength} octets`);
const passphrase = await getPassphrase('restore');
Utils.info('restoreProfile → passphrase obtenue, dérivation de la clé AES-GCM');
const encoder = new TextEncoder();
const baseKey = await crypto.subtle.importKey('raw', encoder.encode(passphrase), { name: 'PBKDF2' }, false, ['deriveKey']);
const aesKey = await crypto.subtle.deriveKey(
{ name: 'PBKDF2', salt: iv, iterations: 100000, hash: 'SHA-256' },
baseKey, { name: 'AES-GCM', length: 256 }, false, ['decrypt']
);
Utils.info('restoreProfile → clé AES-GCM 256 dérivée, tentative de déchiffrement');
const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, aesKey, encrypted);
const json = new TextDecoder().decode(decrypted);
const data = JSON.parse(json);
Utils.info(`restoreProfile → déchiffrement réussi ✓ [${Object.keys(data).length} clé(s) à restaurer]`);
const choice = await showChoiceDialog();
if (choice === 'cancel') {
Utils.warn('restoreProfile → restauration annulée par l\'utilisateur');
return;
}
if (choice === 'replace') {
Utils.warn('restoreProfile → mode "replace" → effacement du localStorage courant');
localStorage.clear();
}
for (const [k, v] of Object.entries(data)) localStorage.setItem(k, v);
Utils.log(`restoreProfile → ${Object.keys(data).length} entrée(s) restaurée(s) dans localStorage ✓`);
Utils.log('restoreProfile → rechargement de la page...');
location.reload();
} catch (e) {
Utils.error('restoreProfile → échec de la restauration :', e);
alert('Échec de la restauration. Vérifiez le mot de passe.');
}
}
// --------------------------------------------------------------------------
// Profil dropdown (anime-sama uniquement)
function createProfileDropdown() {
Utils.dom('createProfileDropdown → tentative de création du dropdown profil');
const nav = document.querySelector('.asn-nav-desktop');
if (!nav) {
Utils.skip('createProfileDropdown → .asn-nav-desktop introuvable, abandon');
return;
}
if (document.getElementById('tampered-dropdown')) {
Utils.skip('createProfileDropdown → #tampered-dropdown déjà présent, abandon');
return;
}
const profileLink = nav.querySelector('a[href="/profil"]');
if (!profileLink) {
Utils.skip('createProfileDropdown → lien /profil introuvable dans la nav, abandon');
return;
}
Utils.dom('createProfileDropdown → lien /profil trouvé, construction du wrapper');
// Créer le wrapper
const wrapper = document.createElement('div');
wrapper.id = 'tampered-dropdown';
wrapper.className = 'relative inline-block text-left';
// Créer le bouton en copiant le contenu du lien Profil
const btn = document.createElement('button');
btn.type = 'button';
btn.className = profileLink.className + ' inline-flex items-center cursor-pointer';
btn.innerHTML = profileLink.innerHTML;
btn.insertAdjacentHTML('beforeend', `
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 ml-1 text-white transform transition-transform duration-200" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.293l3.71-4.06a.75.75 0 111.08 1.04l-4.25 4.656a.75.75 0 01-1.08 0L5.21 8.27a.75.75 0 01.02-1.06z" clip-rule="evenodd"/>
</svg>`);
wrapper.appendChild(btn);
Utils.dom('createProfileDropdown → bouton principal créé avec flèche SVG');
// Menu dropdown
const menu = document.createElement('div');
menu.className = 'absolute right-0 mt-2 w-48 rounded-md shadow-lg bg-gray-800 ring-1 ring-black ring-opacity-5 z-50';
menu.style.display = 'none';
const mkItem = (icon, label, action) => {
const a = document.createElement('a');
a.href = '#';
a.className = 'block px-4 py-2 text-sm text-white hover:bg-gray-700';
a.textContent = `${icon} ${label}`;
a.addEventListener('click', (e) => {
e.preventDefault();
Utils.dom(`createProfileDropdown → item cliqué : "${label}"`);
menu.style.display = 'none';
action();
});
return a;
};
// Lien vers profil original
const profItem = document.createElement('a');
profItem.href = '/profil';
profItem.className = 'block px-4 py-2 text-sm text-white hover:bg-gray-700';
profItem.textContent = '👤 Profil';
menu.appendChild(profItem);
menu.appendChild(mkItem('💾', 'Sauvegarder', backupProfile));
menu.appendChild(mkItem('📂', 'Restaurer', restoreProfile));
wrapper.appendChild(menu);
Utils.dom('createProfileDropdown → menu dropdown construit (3 items : Profil, Sauvegarder, Restaurer)');
// Toggle
const arrow = btn.querySelector('svg:last-child');
btn.addEventListener('click', (e) => {
e.stopPropagation();
const open = menu.style.display !== 'none';
menu.style.display = open ? 'none' : 'block';
if (arrow) arrow.style.transform = open ? '' : 'rotate(180deg)';
Utils.dom(`createProfileDropdown → toggle menu → ${open ? 'fermé' : 'ouvert'}`);
});
document.addEventListener('click', (e) => {
if (!wrapper.contains(e.target)) {
menu.style.display = 'none';
if (arrow) arrow.style.transform = '';
}
});
// Remplacer le lien par le wrapper
profileLink.replaceWith(wrapper);
Utils.dom('createProfileDropdown → lien /profil remplacé par le wrapper dropdown ✓');
}
function ensureProfileDropdown() {
const nav = document.querySelector('.asn-nav-desktop');
if (nav && !document.querySelector('#tampered-dropdown')) {
Utils.dom('ensureProfileDropdown → nav présente et dropdown absent → appel createProfileDropdown');
createProfileDropdown();
} else if (!nav) {
Utils.skip('ensureProfileDropdown → .asn-nav-desktop absent, rien à faire');
} else {
Utils.skip('ensureProfileDropdown → dropdown déjà présent, rien à faire');
}
}
let _ensureTimer = null;
function scheduleEnsure() {
if (_ensureTimer) return;
_ensureTimer = setTimeout(() => {
_ensureTimer = null;
Utils.dom('scheduleEnsure → délai écoulé → appel ensureProfileDropdown');
ensureProfileDropdown();
}, 100);
}
if (document.readyState !== 'loading') {
Utils.dom(`createProfileDropdown → document déjà chargé (readyState: "${document.readyState}") → appel immédiat`);
ensureProfileDropdown();
} else {
Utils.dom('createProfileDropdown → document en cours de chargement → attente DOMContentLoaded');
window.addEventListener('DOMContentLoaded', () => {
Utils.dom('createProfileDropdown → DOMContentLoaded reçu → appel ensureProfileDropdown');
ensureProfileDropdown();
});
}
const domObserver = new MutationObserver(scheduleEnsure);
domObserver.observe(document.documentElement, { childList: true, subtree: true });
Utils.dom('createProfileDropdown → MutationObserver actif sur document.documentElement');
(function hookHistory() {
Utils.dom('hookHistory → injection des hooks pushState / replaceState / popstate');
const fire = () => {
Utils.nav('hookHistory → navigation détectée → dispatch asplus:navigation');
window.dispatchEvent(new Event('asplus:navigation'));
};
const _push = history.pushState, _replace = history.replaceState;
history.pushState = function (...a) {
const r = _push.apply(this, a);
Utils.nav(`hookHistory → pushState intercepté → url: ${a[2] ?? '(aucune)'}`);
fire();
return r;
};
history.replaceState = function (...a) {
const r = _replace.apply(this, a);
Utils.nav(`hookHistory → replaceState intercepté → url: ${a[2] ?? '(aucune)'}`);
fire();
return r;
};
window.addEventListener('popstate', () => {
Utils.nav('hookHistory → popstate détecté');
fire();
});
window.addEventListener('asplus:navigation', () => {
Utils.nav('hookHistory → asplus:navigation reçu → scheduleEnsure');
scheduleEnsure();
});
Utils.dom('hookHistory → hooks history installés ✓');
})();
document.addEventListener('visibilitychange', () => {
if (!document.hidden) {
Utils.dom('visibilitychange → page redevenue visible → scheduleEnsure');
scheduleEnsure();
}
});
// --------------------------------------------------------------------------
// Réactiver la sélection de texte
(function enableSelection() {
Utils.dom('enableSelection → injection du CSS user-select:text global');
const css = `html, body, * {
-webkit-user-select: text !important;
-moz-user-select: text !important;
-ms-user-select: text !important;
user-select: text !important;
-webkit-touch-callout: default !important;
}`;
const style = document.createElement('style');
style.id = 'asplus-enable-selection';
style.appendChild(document.createTextNode(css));
(document.head || document.documentElement).appendChild(style);
Utils.dom('enableSelection → balise <style id="asplus-enable-selection"> injectée ✓');
const BLOCKED_EVENTS = ['copy', 'cut', 'paste', 'contextmenu', 'selectstart', 'dragstart'];
const unblock = e => { e.stopImmediatePropagation(); };
BLOCKED_EVENTS.forEach(t => document.addEventListener(t, unblock, true));
Utils.dom(`enableSelection → stopImmediatePropagation activé sur : [${BLOCKED_EVENTS.join(', ')}]`);
const fixInline = el => {
if (!el || !el.style) return;
el.style.setProperty('user-select', 'text', 'important');
el.style.setProperty('-webkit-user-select', 'text', 'important');
el.style.setProperty('-moz-user-select', 'text', 'important');
el.style.setProperty('-ms-user-select', 'text', 'important');
el.style.setProperty('-webkit-touch-callout', 'default', 'important');
};
fixInline(document.body);
Utils.dom('enableSelection → fixInline appliqué sur document.body');
new MutationObserver(muts => {
for (const m of muts) {
if (m.type === 'attributes' && m.attributeName === 'style') {
Utils.dom(`enableSelection → MutationObserver → style modifié sur <${m.target.tagName?.toLowerCase()}>, correction inline`);
fixInline(m.target);
}
if (m.addedNodes) {
m.addedNodes.forEach(n => {
if (n.nodeType === 1) {
Utils.dom(`enableSelection → MutationObserver → nœud ajouté <${n.tagName?.toLowerCase()}>, fixInline appliqué`);
fixInline(n);
}
});
}
}
}).observe(document.documentElement, {
childList: true, subtree: true,
attributes: true, attributeFilter: ['style']
});
Utils.dom('enableSelection → MutationObserver actif (style + addedNodes) ✓');
})();
// --------------------------------------------------------------------------
//************** INJECTION LECTEUR (parent/iframe) + auto-next + raccourcis **************/
const injectedCode =`
(function () {
//************** LOGGER (miroir de Utils) **************/
var _DEBUG = ${SCRIPT_CONFIG.DEBUG};
var _log = function() { if (!_DEBUG) return; var a = ['%c[ASP:LOG]', 'color:#F47521;font-weight:bold'].concat(Array.prototype.slice.call(arguments)); console.log.apply(console, a); };
var _warn = function() { if (!_DEBUG) return; var a = ['%c[ASP:WARN]', 'color:orange;font-weight:bold'].concat(Array.prototype.slice.call(arguments)); console.warn.apply(console, a); };
var _err = function() { var a = ['%c[ASP:ERROR]','color:red;font-weight:bold'].concat(Array.prototype.slice.call(arguments)); console.error.apply(console, a); };
var _info = function() { if (!_DEBUG) return; var a = ['%c[ASP:INFO]', 'color:#2196F3;font-weight:bold'].concat(Array.prototype.slice.call(arguments)); console.log.apply(console, a); };
var _dom = function() { if (!_DEBUG) return; var a = ['%c[ASP:DOM]', 'color:#FF5722;font-weight:bold'].concat(Array.prototype.slice.call(arguments)); console.log.apply(console, a); };
var _msg = function() { if (!_DEBUG) return; var a = ['%c[ASP:MSG]', 'color:#9C27B0;font-weight:bold'].concat(Array.prototype.slice.call(arguments)); console.log.apply(console, a); };
var _skip = function() { if (!_DEBUG) return; var a = ['%c[ASP:SKIP]', 'color:#4CAF50;font-weight:bold'].concat(Array.prototype.slice.call(arguments)); console.log.apply(console, a); };
var _key = function() { if (!_DEBUG) return; var a = ['%c[ASP:KEY]', 'color:#00BCD4;font-weight:bold'].concat(Array.prototype.slice.call(arguments)); console.log.apply(console, a); };
var _nav = function() { if (!_DEBUG) return; var a = ['%c[ASP:NAV]', 'color:#CDDC39;font-weight:bold'].concat(Array.prototype.slice.call(arguments)); console.log.apply(console, a); };
_log('[init] → script injecté démarré', { version: '${SCRIPT_CONFIG.VERSION}', href: location.href });
var CONTROL_DOMAINS = ${JSON.stringify(SCRIPT_CONFIG.C_DOMAINS)};
var PARENT_DOMAINS = ${JSON.stringify(SCRIPT_CONFIG.P_DOMAINS)};
var SITE = location.hostname;
var isTop = (window.self === window.top);
_info('[init] → domaines chargés', { CONTROL_DOMAINS: CONTROL_DOMAINS, PARENT_DOMAINS: PARENT_DOMAINS });
function matchHost(host, pattern) {
if (!host || !pattern) return false;
if (pattern.indexOf('.') !== -1) {
return host === pattern || host.endsWith('.' + pattern);
}
var esc = pattern.replace(/[-/\\\\^$*+?.()|[\\]{}]/g, '\\\\$&');
return new RegExp('(?:^|\\\\.)' + esc + '\\\\.', 'i').test(host);
}
var isControlHost = CONTROL_DOMAINS.some(function(p){ return matchHost(SITE, p); });
var isParentHost = PARENT_DOMAINS.some(function(p){ return matchHost(SITE, p); });
_info('[init] → détection hôte', { SITE: SITE, isControlHost: isControlHost, isParentHost: isParentHost, isTop: isTop });
var ref = document.referrer || '';
var refHost = '';
try {
refHost = new URL(ref).hostname;
_info('[init] → referrer parsé', { ref: ref, refHost: refHost });
} catch (_) {
_warn('[init] → referrer invalide ou absent', { ref: ref });
}
var refIsParent = PARENT_DOMAINS.some(function(p){ return matchHost(refHost, p); });
var fromAnimeParent = isTop && (isParentHost || !!document.getElementById('playerDF'));
var fromAnimeIframe = !isTop && refIsParent;
_log('[init] → contexte déterminé', {
host: SITE,
isTop: isTop,
fromAnimeParent:fromAnimeParent,
fromAnimeIframe:fromAnimeIframe,
refHost: refHost,
refIsParent: refIsParent,
playerDFPresent:!!document.getElementById('playerDF')
});
if (!fromAnimeParent && !fromAnimeIframe && !isControlHost) {
_skip('[init] → contexte non reconnu (ni parent, ni iframe, ni controlHost) → sortie');
return;
}
// ── pendingToggle ─────────────────────────────────────────────────────────
var pendingToggle = false;
try {
if (sessionStorage.getItem('asp_pendingToggle') === '1') {
pendingToggle = true;
sessionStorage.removeItem('asp_pendingToggle');
_log('[parent] → pendingToggle restauré depuis sessionStorage ✓');
} else {
_info('[parent] → asp_pendingToggle absent en sessionStorage');
}
} catch(e) {
_warn('[parent] → échec lecture sessionStorage asp_pendingToggle :', e);
}
// ── prevEp / nextEp ───────────────────────────────────────────────────────
var prevEp = window.prevEp || function() { _warn('[nav] → prevEp non défini sur window'); };
var nextEp = window.nextEp || function() { _warn('[nav] → nextEp non défini sur window'); };
if (window.prevEp) {
_info('[nav] → window.prevEp trouvé ✓');
} else {
_warn('[nav] → window.prevEp absent → fallback no-op');
}
if (window.nextEp) {
_info('[nav] → window.nextEp trouvé ✓');
} else {
_warn('[nav] → window.nextEp absent → fallback no-op');
}
var EPS = 3;
_info('[init] → EPS (seuil fin de vidéo) défini à', EPS, 'secondes');
//**** PARENT: raccourcis clavier ****
function parentKeyHandler(e) {
if (/input|textarea|select/i.test(e.target.tagName)) {
_skip('[parent:key] → cible est un champ de saisie, ignoré :', e.target.tagName);
return;
}
var iframe = document.getElementById('playerDF');
// Touches navigation : toujours actives
switch (e.key) {
case 'n': case 'N':
e.preventDefault();
_key('[parent:key] → "N" → nextEp, pendingToggle=true, sessionStorage mis à jour');
pendingToggle = true;
try { sessionStorage.setItem('asp_pendingToggle', '1'); } catch(err) { _warn('[parent:key] → échec sessionStorage setItem :', err); }
nextEp();
return;
case 'p': case 'P':
e.preventDefault();
_key('[parent:key] → "P" → prevEp, pendingToggle=true, sessionStorage mis à jour');
pendingToggle = true;
try { sessionStorage.setItem('asp_pendingToggle', '1'); } catch(err) { _warn('[parent:key] → échec sessionStorage setItem :', err); }
prevEp();
return;
}
// Touches de contrôle : soumises à CONTROL_DOMAINS
if (!isControlHost) {
_skip('[parent:key] → touche "' + e.key + '" ignorée (hôte non controlHost) :', SITE);
return;
}
if (!iframe || !iframe.contentWindow) {
_warn('[parent:key] → #playerDF introuvable ou sans contentWindow, postMessage impossible');
}
switch (e.key) {
case ' ':
e.preventDefault();
_key('[parent:key] → Espace → togglePlay');
pendingToggle = true;
if (iframe && iframe.contentWindow)
iframe.contentWindow.postMessage({ action: 'togglePlay' }, '*');
break;
case 'ArrowRight':
e.preventDefault();
_key('[parent:key] → ArrowRight → seekForward +10s');
if (iframe && iframe.contentWindow)
iframe.contentWindow.postMessage({ action: 'seekForward', value: 10 }, '*');
break;
case 'ArrowLeft':
e.preventDefault();
_key('[parent:key] → ArrowLeft → seekBackward -10s');
if (iframe && iframe.contentWindow)
iframe.contentWindow.postMessage({ action: 'seekBackward', value: 10 }, '*');
break;
case 'ArrowUp':
e.preventDefault();
_key('[parent:key] → ArrowUp → volumeUp +10');
if (iframe && iframe.contentWindow)
iframe.contentWindow.postMessage({ action: 'volumeUp', value: 10 }, '*');
break;
case 'ArrowDown':
e.preventDefault();
_key('[parent:key] → ArrowDown → volumeDown -10');
if (iframe && iframe.contentWindow)
iframe.contentWindow.postMessage({ action: 'volumeDown', value: 10 }, '*');
break;
case 'f': case 'F':
e.preventDefault();
_key('[parent:key] → "F" → toggleFullscreen');
if (iframe && iframe.contentWindow)
iframe.contentWindow.postMessage({ action: 'toggleFullscreen' }, '*');
break;
default:
_skip('[parent:key] → touche "' + e.key + '" non gérée');
}
}
//**** PARENT: message handler ****
function messageHandler(e) {
if (!e.data || !e.data.action) {
_skip('[parent:msg] → message reçu sans action, ignoré', e.data);
return;
}
_msg('[parent:msg] → action reçue :', e.data.action, '| origin :', e.origin);
switch (e.data.action) {
case 'Istart': {
_msg('[parent:msg] → Istart reçu | pendingToggle =', pendingToggle);
if (pendingToggle) {
pendingToggle = false;
var iframe = document.getElementById('playerDF');
if (iframe && iframe.contentWindow) {
_msg('[parent:msg] → Istart → focus iframe + postMessage togglePlay + focusPlayer');
iframe.contentWindow.postMessage({ action: 'togglePlay' }, '*');
iframe.contentWindow.postMessage({ action: 'focusPlayer' }, '*');
} else {
_warn('[parent:msg] → Istart → #playerDF introuvable, togglePlay annulé');
}
} else {
_skip('[parent:msg] → Istart → pendingToggle=false, togglePlay ignoré');
}
break;
}
case 'nextEp': {
_nav('[parent:msg] → nextEp reçu → pendingToggle=true, sessionStorage, nextEp()');
pendingToggle = true;
try { sessionStorage.setItem('asp_pendingToggle', '1'); } catch(err) { _warn('[parent:msg] → échec sessionStorage :', err); }
nextEp();
break;
}
case 'prevEp': {
_nav('[parent:msg] → prevEp reçu → pendingToggle=true, sessionStorage, prevEp()');
pendingToggle = true;
try { sessionStorage.setItem('asp_pendingToggle', '1'); } catch(err) { _warn('[parent:msg] → échec sessionStorage :', err); }
prevEp();
break;
}
default: {
_skip('[parent:msg] → action non gérée :', e.data.action);
}
}
}
//**** IFRAME (<video> natif): raccourcis clavier ****
function iframeKeyHandler(e) {
if (/input|textarea|select/i.test(e.target.tagName)) {
_skip('[iframe:key] → cible est un champ de saisie, ignoré :', e.target.tagName);
return;
}
var v = document.querySelector('video');
// Touches navigation : toujours actives
switch (e.key) {
case 'n': case 'N': {
e.preventDefault();
_key('[iframe:key] → "N" → postMessage nextEp → parent');
window.parent.postMessage({ action: 'nextEp' }, '*');
return;
}
case 'p': case 'P': {
e.preventDefault();
_key('[iframe:key] → "P" → postMessage prevEp → parent');
window.parent.postMessage({ action: 'prevEp' }, '*');
return;
}
}
// Touches de contrôle : soumises à CONTROL_DOMAINS
if (!isControlHost) {
_skip('[iframe:key] → touche "' + e.key + '" ignorée (hôte non controlHost) :', SITE);
return;
}
if (!v) {
_warn('[iframe:key] → aucun élément <video> trouvé, contrôle impossible');
return;
}
switch (e.key) {
case ' ': {
e.preventDefault();
_key('[iframe:key] → Espace → togglePlay');
if (v.paused) {
v.play().catch(function(err) { _warn('[iframe:key] → play() rejeté :', err); });
} else {
v.pause();
}
break;
}
case 'ArrowRight': {
e.preventDefault();
_key('[iframe:key] → ArrowRight → seekForward +10s |', v.currentTime.toFixed(1), '→', Math.min(v.duration, v.currentTime + 10).toFixed(1));
v.currentTime = Math.min(v.duration, v.currentTime + 10);
break;
}
case 'ArrowLeft': {
e.preventDefault();
_key('[iframe:key] → ArrowLeft → seekBackward -10s |', v.currentTime.toFixed(1), '→', Math.max(0, v.currentTime - 10).toFixed(1));
v.currentTime = Math.max(0, v.currentTime - 10);
break;
}
case 'ArrowUp': {
e.preventDefault();
_key('[iframe:key] → ArrowUp → volumeUp +0.1 |', v.volume.toFixed(2), '→', Math.min(1, v.volume + 0.1).toFixed(2));
v.volume = Math.min(1, v.volume + 0.1);
break;
}
case 'ArrowDown': {
e.preventDefault();
_key('[iframe:key] → ArrowDown → volumeDown -0.1 |', v.volume.toFixed(2), '→', Math.max(0, v.volume - 0.1).toFixed(2));
v.volume = Math.max(0, v.volume - 0.1);
break;
}
case 'f': case 'F': {
e.preventDefault();
if (document.fullscreenElement) {
_key('[iframe:key] → "F" → exitFullscreen');
document.exitFullscreen();
} else if (v.requestFullscreen) {
_key('[iframe:key] → "F" → requestFullscreen');
v.requestFullscreen();
} else {
_warn('[iframe:key] → "F" → requestFullscreen non supporté');
}
break;
}
default: {
_skip('[iframe:key] → touche "' + e.key + '" non gérée');
}
}
}
//**** IFRAME: toggle play/pause ****
function togglePlayPauseAfterDelay() {
_dom('[iframe:togglePlay] → setTimeout 300ms avant toggle');
setTimeout(function() {
var v = document.querySelector('video');
if (!v) {
_warn('[iframe:togglePlay] → aucun <video> trouvé après délai');
return;
}
if (v.paused) {
_dom('[iframe:togglePlay] → vidéo en pause → play()');
v.play().catch(function(err) { _warn('[iframe:togglePlay] → play() rejeté :', err); });
} else {
_dom('[iframe:togglePlay] → vidéo en lecture → pause()');
v.pause();
}
}, 300);
}
//**** IFRAME (<video> natif): détection fin ****
function addVideoEndDetectors() {
_dom('[iframe:endDetect] → initialisation des détecteurs de fin de vidéo');
var sent = false;
function sendNext(src) {
if (sent) {
_skip('[iframe:endDetect] → sendNext déjà envoyé, doublon ignoré (src:', src, ')');
return;
}
sent = true;
_log('[iframe:endDetect] → nextEp envoyé via :', src);
window.parent.postMessage({ action: 'nextEp' }, '*');
}
function attachToVideo() {
var v = document.querySelector('video');
if (!v) {
_warn('[iframe:endDetect] → attachToVideo appelé mais aucun <video> trouvé');
return;
}
_dom('[iframe:endDetect] → <video> trouvé, attache événement "ended" + stallTimer');
v.addEventListener('ended', function() {
_log('[iframe:endDetect] → événement "ended" déclenché');
sendNext('ended');
});
var lastT = -1;
var stallTimer = setInterval(function() {
if (sent) {
_skip('[iframe:endDetect] → stallTimer → nextEp déjà envoyé, clearInterval');
clearInterval(stallTimer);
return;
}
var d = v.duration;
if (!isFinite(d) || !d) return;
var now = v.currentTime;
var remaining = d - now;
if (now === lastT && remaining <= EPS && v.paused) {
_log('[iframe:endDetect] → stallTimer → stall détecté en fin de vidéo',
'| currentTime:', now.toFixed(2), '| remaining:', remaining.toFixed(2), '| EPS:', EPS);
sendNext('stall-end');
clearInterval(stallTimer);
}
lastT = now;
}, 1000);
}
if (document.querySelector('video')) {
_dom('[iframe:endDetect] → <video> présent immédiatement → attachToVideo()');
attachToVideo();
} else {
_dom('[iframe:endDetect] → <video> absent → MutationObserver en attente');
var obs = new MutationObserver(function() {
if (document.querySelector('video')) {
_dom('[iframe:endDetect] → MutationObserver → <video> détecté → attachToVideo()');
obs.disconnect();
attachToVideo();
}
});
obs.observe(document.body, { childList: true, subtree: true });
}
}
//**********************
//* IFRAME JW PLAYER (VidMoly, etc.)
//**********************
function detectJWPlayer() {
try {
var p = jwplayer();
var ok = (p && typeof p.getState === 'function');
if (ok) {
_dom('[JW:detect] → jwplayer() trouvé et opérationnel | state:', p.getState());
} else {
_skip('[JW:detect] → jwplayer() présent mais getState absent');
}
return ok ? p : null;
} catch (err) {
_skip('[JW:detect] → jwplayer() non disponible :', err.message);
return null;
}
}
function waitForJWPlayer(timeout) {
timeout = timeout || 10000;
_dom('[JW:wait] → démarrage polling JWPlayer | timeout:', timeout, 'ms');
return new Promise(function(resolve, reject) {
var t0 = Date.now();
var attempts = 0;
var check = setInterval(function() {
attempts++;
var p = detectJWPlayer();
if (p) {
_dom('[JW:wait] → JWPlayer détecté après', attempts, 'tentative(s) |', (Date.now() - t0), 'ms');
clearInterval(check);
resolve(p);
return;
}
if (Date.now() - t0 > timeout) {
_warn('[JW:wait] → timeout atteint après', attempts, 'tentatives |', timeout, 'ms');
clearInterval(check);
reject(new Error('JW timeout'));
}
}, 300);
});
}
function forcePlayJW(player) {
var state = player.getState();
_dom('[JW:forcePlay] → état actuel :', state);
var overlays = [
'.jw-display-icon-container',
'.jw-icon-display',
'.jw-controls .jw-icon-playback',
'.vjs-big-play-button'
];
var clicked = false;
for (var i = 0; i < overlays.length; i++) {
var el = document.querySelector(overlays[i]);
if (el) {
_dom('[JW:forcePlay] → click overlay trouvé :', overlays[i]);
el.click();
clicked = true;
break;
}
}
if (!clicked) {
_skip('[JW:forcePlay] → aucun overlay cliquable trouvé');
}
try {
player.play();
_dom('[JW:forcePlay] → player.play() appelé');
} catch(e) {
_err('[JW:forcePlay] → player.play() échoué :', e);
}
setTimeout(function() {
var s = player.getState();
if (s !== 'playing') {
_warn('[JW:forcePlay] → retry #1 après 500ms | state:', s);
try { player.play(); } catch(e) { _warn('[JW:forcePlay] → retry #1 play() échoué :', e); }
} else {
_dom('[JW:forcePlay] → retry #1 inutile, déjà en lecture ✓');
}
}, 500);
setTimeout(function() {
var s = player.getState();
if (s !== 'playing') {
_warn('[JW:forcePlay] → retry #2 après 1500ms + fallback <video> | state:', s);
try { player.play(); } catch(e) { _warn('[JW:forcePlay] → retry #2 play() échoué :', e); }
var v = document.querySelector('video');
if (v && v.paused) {
_dom('[JW:forcePlay] → fallback <video>.play()');
v.play().catch(function(err) { _warn('[JW:forcePlay] → fallback <video>.play() rejeté :', err); });
} else if (!v) {
_warn('[JW:forcePlay] → fallback : aucun <video> trouvé');
} else {
_skip('[JW:forcePlay] → fallback : <video> déjà en lecture');
}
} else {
_dom('[JW:forcePlay] → retry #2 inutile, déjà en lecture ✓');
}
}, 1500);
}
function jwKeyHandler(player, e) {
if (/input|textarea|select/i.test(e.target.tagName)) {
_skip('[JW:key] → cible champ de saisie, ignoré :', e.target.tagName);
return;
}
// Touches navigation : toujours actives
switch (e.key) {
case 'n': case 'N':
e.preventDefault();
_key('[JW:key] → "N" → postMessage nextEp → parent');
window.parent.postMessage({ action: 'nextEp' }, '*');
return;
case 'p': case 'P':
e.preventDefault();
_key('[JW:key] → "P" → postMessage prevEp → parent');
window.parent.postMessage({ action: 'prevEp' }, '*');
return;
}
// Touches de contrôle : soumises à CONTROL_DOMAINS
if (!isControlHost) {
_skip('[JW:key] → touche "' + e.key + '" ignorée (hôte non controlHost) :', SITE);
return;
}
var pos, vol;
switch (e.key) {
case ' ':
e.preventDefault();
var st = player.getState();
_key('[JW:key] → Espace → togglePlay | state:', st);
st === 'playing' ? player.pause() : player.play();
break;
case 'ArrowRight':
e.preventDefault();
pos = player.getPosition();
_key('[JW:key] → ArrowRight → seekForward +10s |', pos.toFixed(1), '→', (pos + 10).toFixed(1));
player.seek(pos + 10);
break;
case 'ArrowLeft':
e.preventDefault();
pos = player.getPosition();
_key('[JW:key] → ArrowLeft → seekBackward -10s |', pos.toFixed(1), '→', Math.max(0, pos - 10).toFixed(1));
player.seek(Math.max(0, pos - 10));
break;
case 'ArrowUp':
e.preventDefault();
vol = player.getVolume();
_key('[JW:key] → ArrowUp → volumeUp +10 |', vol, '→', Math.min(100, vol + 10));
player.setVolume(Math.min(100, vol + 10));
break;
case 'ArrowDown':
e.preventDefault();
vol = player.getVolume();
_key('[JW:key] → ArrowDown → volumeDown -10 |', vol, '→', Math.max(0, vol - 10));
player.setVolume(Math.max(0, vol - 10));
break;
case 'f': case 'F':
e.preventDefault();
var fs = player.getFullscreen();
_key('[JW:key] → "F" → toggleFullscreen |', fs ? 'fullscreen → normal' : 'normal → fullscreen');
player.setFullscreen(!fs);
break;
default:
_skip('[JW:key] → touche "' + e.key + '" non gérée');
}
}
function attachJWEndDetectors(player) {
_dom('[JW:endDetect] → initialisation des détecteurs de fin');
var sent = false;
function sendNext(src) {
if (sent) {
_skip('[JW:endDetect] → sendNext doublon ignoré (src:', src, ')');
return;
}
sent = true;
_log('[JW:endDetect] → nextEp envoyé via :', src);
window.parent.postMessage({ action: 'nextEp' }, '*');
}
// 1) JW complete event
player.on('complete', function() {
_log('[JW:endDetect] → événement "complete" JWPlayer déclenché');
sendNext('jw-complete');
});
// 2) Fallback <video> ended
var v = document.querySelector('video');
if (v) {
_dom('[JW:endDetect] → <video> trouvé, attache événement "ended" fallback');
v.addEventListener('ended', function() {
_log('[JW:endDetect] → événement "ended" <video> déclenché');
sendNext('video-ended');
});
} else {
_warn('[JW:endDetect] → aucun <video> trouvé pour fallback "ended"');
}
// 3) Détection fin : remaining <= 0.5 pendant 2 checks consécutifs
var endCount = 0;
_dom('[JW:endDetect] → stallInterval démarré (check toutes les 1s)');
var stallInterval = setInterval(function() {
if (sent) {
_skip('[JW:endDetect] → stallInterval → nextEp déjà envoyé, clearInterval');
clearInterval(stallInterval);
return;
}
try {
var dur = player.getDuration();
var pos = player.getPosition();
if (!isFinite(dur) || dur <= 0) return;
var remaining = dur - pos;
if (remaining <= 0.5) {
endCount++;
_log('[JW:endDetect] → fin proche | count:', endCount, '| remaining:', remaining.toFixed(3), 's | pos:', pos.toFixed(2), '/ dur:', dur.toFixed(2));
if (endCount >= 2) {
_log('[JW:endDetect] → seuil atteint (count ≥ 2) → sendNext jw-end-detect');
sendNext('jw-end-detect');
clearInterval(stallInterval);
}
} else {
if (endCount > 0) {
_skip('[JW:endDetect] → endCount réinitialisé (remaining:', remaining.toFixed(3), ')');
}
endCount = 0;
}
} catch (err) {
_warn('[JW:endDetect] → stallInterval erreur :', err.message);
}
}, 1000);
}
function attachJWIframeHandlers(player) {
_log('[JW:iframe] → initialisation handlers iframe | state:', player.getState());
document.addEventListener('keydown', function(e) { jwKeyHandler(player, e); }, true);
_dom('[JW:iframe] → keydown listener attaché (capture=true)');
window.addEventListener('message', function(e) {
if (!e.data || !e.data.action) {
_skip('[JW:iframe:msg] → message sans action ignoré', e.data);
return;
}
_msg('[JW:iframe:msg] → action reçue :', e.data.action, '| value:', e.data.value, '| origin:', e.origin);
switch (e.data.action) {
case 'togglePlay': {
_dom('[JW:iframe:msg] → togglePlay → window.focus() + forcePlayJW');
forcePlayJW(player);
break;
}
case 'focusPlayer': {
try {
// Tente focus sur le conteneur JW
var jwEl = document.querySelector('.jwplayer, #player, .jw-video');
if (jwEl) { jwEl.focus(); }
else {
var v = document.querySelector('video');
if (v) v.focus();
}
_log('[JW:iframe:msg] → focusPlayer → focus appliqué');
} catch(_) {}
break;
}
case 'seekForward': {
var val = e.data.value || 10;
var pos = player.getPosition();
_dom('[JW:iframe:msg] → seekForward +' + val + 's |', pos.toFixed(1), '→', (pos + val).toFixed(1));
player.seek(pos + val);
break;
}
case 'seekBackward': {
var val2 = e.data.value || 10;
var pos2 = player.getPosition();
_dom('[JW:iframe:msg] → seekBackward -' + val2 + 's |', pos2.toFixed(1), '→', Math.max(0, pos2 - val2).toFixed(1));
player.seek(Math.max(0, pos2 - val2));
break;
}
case 'volumeUp': {
var val3 = e.data.value || 10;
var v = player.getVolume();
_dom('[JW:iframe:msg] → volumeUp +' + val3 + ' |', v, '→', Math.min(100, v + val3));
player.setVolume(Math.min(100, v + val3));
break;
}
case 'volumeDown': {
var val4 = e.data.value || 10;
var v2 = player.getVolume();
_dom('[JW:iframe:msg] → volumeDown -' + val4 + ' |', v2, '→', Math.max(0, v2 - val4));
player.setVolume(Math.max(0, v2 - val4));
break;
}
case 'toggleFullscreen': {
var fs = player.getFullscreen();
_dom('[JW:iframe:msg] → toggleFullscreen |', fs ? 'fullscreen → normal' : 'normal → fullscreen');
player.setFullscreen(!fs);
break;
}
default:
_skip('[JW:iframe:msg] → action non gérée :', e.data.action);
}
});
_dom('[JW:iframe] → message listener attaché ✓');
attachJWEndDetectors(player);
_msg('[JW:iframe] → postMessage Istart → parent');
window.parent.postMessage({ action: 'Istart' }, '*');
_log('[JW:iframe] → initialisation complète ✓');
}
//**********************
//* ROUTAGE PARENT / IFRAME
//**********************
function attachParentHandlers() {
_log('[route:parent] → initialisation handlers parent');
document.addEventListener('keydown', parentKeyHandler, true);
_dom('[route:parent] → keydown listener attaché (capture=true) ✓');
window.addEventListener('message', messageHandler);
_dom('[route:parent] → message listener attaché ✓');
_log('[route:parent] → initialisation complète ✓');
}
function attachIframeHandlers() {
_log('[route:iframe] → initialisation handlers iframe');
// Détection synchrone JW
var jwp = detectJWPlayer();
if (jwp) {
_log('[route:iframe] → JWPlayer détecté synchronement → attachJWIframeHandlers');
attachJWIframeHandlers(jwp);
return;
}
_skip('[route:iframe] → pas de JWPlayer synchrone → fallback vidéo natif');
// Envoyer Istart + attacher video natif TOUT DE SUITE
document.addEventListener('keydown', iframeKeyHandler, true);
_dom('[route:iframe] → keydown listener (natif) attaché (capture=true) ✓');
window.addEventListener('message', function(e) {
if (!e.data || !e.data.action) {
_skip('[route:iframe:msg] → message sans action ignoré', e.data);
return;
}
_msg('[route:iframe:msg] → action reçue :', e.data.action, '| value:', e.data.value, '| origin:', e.origin);
var v;
switch (e.data.action) {
case 'togglePlay': {
_dom('[route:iframe:msg] → togglePlay → focus + togglePlayPauseAfterDelay');
try { window.focus(); } catch(err) { _warn('[route:iframe:msg] → window.focus() échoué :', err); }
v = document.querySelector('video');
if (v) {
v.focus();
_dom('[route:iframe:msg] → <video> focus ✓');
} else {
_warn('[route:iframe:msg] → togglePlay : aucun <video> trouvé pour focus');
}
togglePlayPauseAfterDelay();
break;
}
case 'seekForward': {
v = document.querySelector('video');
if (v) {
var sfVal = e.data.value || 10;
var sfNew = Math.min(v.duration, v.currentTime + sfVal);
_dom('[route:iframe:msg] → seekForward +' + sfVal + 's |', v.currentTime.toFixed(1), '→', sfNew.toFixed(1));
v.currentTime = sfNew;
} else {
_warn('[route:iframe:msg] → seekForward : aucun <video>');
}
break;
}
case 'seekBackward': {
v = document.querySelector('video');
if (v) {
var sbVal = e.data.value || 10;
var sbNew = Math.max(0, v.currentTime - sbVal);
_dom('[route:iframe:msg] → seekBackward -' + sbVal + 's |', v.currentTime.toFixed(1), '→', sbNew.toFixed(1));
v.currentTime = sbNew;
} else {
_warn('[route:iframe:msg] → seekBackward : aucun <video>');
}
break;
}
case 'volumeUp': {
v = document.querySelector('video');
if (v) {
var vuVal = (e.data.value || 10) / 100;
var vuNew = Math.min(1, v.volume + vuVal);
_dom('[route:iframe:msg] → volumeUp +' + (vuVal * 100) + '% |', v.volume.toFixed(2), '→', vuNew.toFixed(2));
v.volume = vuNew;
} else {
_warn('[route:iframe:msg] → volumeUp : aucun <video>');
}
break;
}
case 'volumeDown': {
v = document.querySelector('video');
if (v) {
var vdVal = (e.data.value || 10) / 100;
var vdNew = Math.max(0, v.volume - vdVal);
_dom('[route:iframe:msg] → volumeDown -' + (vdVal * 100) + '% |', v.volume.toFixed(2), '→', vdNew.toFixed(2));
v.volume = vdNew;
} else {
_warn('[route:iframe:msg] → volumeDown : aucun <video>');
}
break;
}
case 'toggleFullscreen':
v = document.querySelector('video');
if (v) {
if (document.fullscreenElement) {
_dom('[route:iframe:msg] → toggleFullscreen → exitFullscreen');
document.exitFullscreen();
} else if (v.requestFullscreen) {
_dom('[route:iframe:msg] → toggleFullscreen → requestFullscreen');
v.requestFullscreen();
} else {
_warn('[route:iframe:msg] → toggleFullscreen : requestFullscreen non supporté');
}
} else {
_warn('[route:iframe:msg] → toggleFullscreen : aucun <video>');
}
break;
default:
_skip('[route:iframe:msg] → action non gérée :', e.data.action);
}
});
_dom('[route:iframe] → message listener (natif) attaché ✓');
addVideoEndDetectors();
// Envoyer Istart dès que possible
function sendIstart() {
_msg('[route:iframe] → postMessage Istart → parent');
window.parent.postMessage({ action: 'Istart' }, '*');
}
if (document.readyState === 'complete') {
_dom('[route:iframe] → readyState=complete → sendIstart dans 100ms');
setTimeout(sendIstart, 100);
} else {
_dom('[route:iframe] → readyState=' + document.readyState + ' → sendIstart sur événement "load"');
window.addEventListener('load', function() {
_dom('[route:iframe] → événement "load" reçu → sendIstart dans 100ms');
setTimeout(sendIstart, 100);
});
}
// Tenter JW en arrière-plan (upgrade si trouvé)
_dom('[route:iframe] → waitForJWPlayer (3s) lancé en arrière-plan');
waitForJWPlayer(3000).then(function(player) {
_log('[route:iframe] → JWPlayer détecté tardivement → upgrade → removeEventListener iframeKeyHandler');
document.removeEventListener('keydown', iframeKeyHandler, true);
attachJWIframeHandlers(player);
}).catch(function() {
_skip('[route:iframe] → waitForJWPlayer timeout → confirmé : pas de JW, vidéo natif actif');
});
}
// ── Routage principal ──────────────────────────────────────────────────────
_log('[route] → démarrage routage | fromAnimeParent:', fromAnimeParent, '| fromAnimeIframe:', fromAnimeIframe, '| isTop:', isTop);
if (fromAnimeParent) {
_log('[route] → contexte PARENT détecté → attachParentHandlers');
attachParentHandlers();
} else if (fromAnimeIframe) {
_log('[route] → contexte IFRAME détecté → attachIframeHandlers');
attachIframeHandlers();
} else {
var hasPlayerIframeId = !!document.getElementById('playerDF');
var hasVideo = !!document.querySelector('video');
_warn('[route] → contexte INCONNU | fallback | hasPlayerIframeId:', hasPlayerIframeId, '| hasVideo:', hasVideo, '| isTop:', isTop, '| host:', SITE);
if (isTop && hasPlayerIframeId) {
_warn('[route:fallback] → isTop + #playerDF trouvé → attachParentHandlers');
attachParentHandlers();
} else if (!isTop && hasVideo) {
_warn('[route:fallback] → iframe + <video> trouvé → attachIframeHandlers');
attachIframeHandlers();
} else if (!isTop) {
_warn('[route:fallback] → iframe sans <video> détecté → attachIframeHandlers (spéculatif)');
attachIframeHandlers();
} else {
_err('[route:fallback] → aucune condition remplie → rien attaché | host:', SITE, '| href:', location.href);
}
}
})();
`;
Utils.log('[inject] → création du script injecté');
const script = document.createElement('script');
script.defer = true;
script.textContent = injectedCode;
Utils.log('[inject] → injection dans documentElement');
document.documentElement.appendChild(script);
script.remove();
Utils.log('[inject] → script injecté et retiré du DOM');
})();