anime-sama Plus

Sauvegarde/restauration chiffrée du profil (.sama) + Next/Prev auto & contrôles clavier adaptatifs

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         anime-sama Plus
// @namespace    http://tampermonkey.net/
// @version      0.1.7.2
// @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: false,
        VERSION: '0.1.7.2',
        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;
        }
    };

    // --------------------------------------------------------------------------
    // UI - Choix restauration (Remplacer/Annuler)
    function showChoiceDialog() {
        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&nbsp;?</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 = () => { 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);
        });
    }

    // --------------------------------------------------------------------------
    // UI - Mot de passe
    function showPasswordDialog(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;
                document.body.removeChild(overlay);
                resolve({ pass, remember });
            };
            btnRow.appendChild(mkBtn('Valider', '#4caf50', submit));
            btnRow.appendChild(mkBtn('Annuler', '#555', () => {
                document.body.removeChild(overlay);
                resolve({ pass: null, remember: false });
            }));
            box.appendChild(btnRow);
            overlay.appendChild(box);
            document.body.appendChild(overlay);
            box.querySelector('#asplus-pass').addEventListener('keydown', e => { if (e.key === 'Enter') submit(); });
            box.querySelector('#asplus-pass').focus();
        });
    }

    async function getPassphrase(mode) {
        const sess = localStorage.getItem('asplus.passphrase');
        if (sess && sess.length) return sess;
        const { pass, remember } = await showPasswordDialog(mode);
        const chosen = (pass && pass.length) ? pass : 'SAMA';
        if (remember) localStorage.setItem('asplus.passphrase', chosen);
        return chosen;
    }

    // --------------------------------------------------------------------------
    // Sauvegarde / Restauration (AES-GCM 256)
    async function backupProfile() {
        try {
            const data = {};
            for (let i = 0; i < localStorage.length; i++) {
                const key = localStorage.key(i);
                data[key] = localStorage.getItem(key);
            }
            const json = JSON.stringify(data);
            const encoder = new TextEncoder();
            const passphrase = await getPassphrase('backup');
            const iv = crypto.getRandomValues(new Uint8Array(12));
            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, ['encrypt']
            );
            const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, aesKey, encoder.encode(json));
            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' });
            await pickFileToSave(blob);
        } catch (e) {
            Utils.error('Backup failed:', e);
        }
    }

    async function pickFileToSave(blob) {
        if (window.showSaveFilePicker) {
            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();
        } else {
            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);
        }
    }

    async function restoreProfile() {
        try {
            let file;
            if (window.showOpenFilePicker) {
                const [handle] = await window.showOpenFilePicker({
                    types: [{ description: 'Backup Anime-Sama', accept: { 'application/vnd.animesama.backup': ['.sama'] } }]
                });
                file = await handle.getFile();
            } else {
                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) return;
            const buf = await file.arrayBuffer();
            const arr = new Uint8Array(buf);
            const iv = arr.slice(0, 12);
            const encrypted = arr.slice(12);
            const passphrase = await getPassphrase('restore');
            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']
            );
            const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, aesKey, encrypted);
            const json = new TextDecoder().decode(decrypted);
            const data = JSON.parse(json);
            const choice = await showChoiceDialog();
            if (choice === 'cancel') return;
            if (choice === 'replace') localStorage.clear();
            for (const [k, v] of Object.entries(data)) localStorage.setItem(k, v);
            location.reload();
        } catch (e) {
            Utils.error('Restore failed:', e);
            alert('Échec de la restauration. Vérifiez le mot de passe.');
        }
    }

    // --------------------------------------------------------------------------
    // Profil dropdown (anime-sama uniquement)
        function createProfileDropdown() {
        const nav = document.querySelector('.asn-nav-desktop');
        if (!nav || document.getElementById('tampered-dropdown')) return;

        const profileLink = nav.querySelector('a[href="/profil"]');
        if (!profileLink) return;

        // 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';
        // Copier les classes du lien original + ajouts
        btn.className = profileLink.className + ' inline-flex items-center cursor-pointer';
        // Transférer le contenu HTML du lien (SVG, span, etc.)
        btn.innerHTML = profileLink.innerHTML;
        // Ajouter la flèche dropdown
        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);

        // 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(); 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);

        // 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)';
        });
        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);
    }

    function ensureProfileDropdown() {
        const nav = document.querySelector('.asn-nav-desktop');
        if (nav && !document.querySelector('#tampered-dropdown')) createProfileDropdown();
    }
    let _ensureTimer = null;
    function scheduleEnsure() {
        if (_ensureTimer) return;
        _ensureTimer = setTimeout(() => { _ensureTimer = null; ensureProfileDropdown(); }, 100);
    }
    if (document.readyState !== 'loading') ensureProfileDropdown();
    else window.addEventListener('DOMContentLoaded', ensureProfileDropdown);
    const domObserver = new MutationObserver(scheduleEnsure);
    domObserver.observe(document.documentElement, { childList: true, subtree: true });
    (function hookHistory() {
        const fire = () => window.dispatchEvent(new Event('asplus:navigation'));
        const _push = history.pushState, _replace = history.replaceState;
        history.pushState = function (...a) { const r = _push.apply(this, a); fire(); return r; };
        history.replaceState = function (...a) { const r = _replace.apply(this, a); fire(); return r; };
        window.addEventListener('popstate', fire);
        window.addEventListener('asplus:navigation', scheduleEnsure);
    })();
    document.addEventListener('visibilitychange', () => { if (!document.hidden) scheduleEnsure(); });

    // --------------------------------------------------------------------------
    // Réactiver la sélection de texte
    (function enableSelection() {
        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);

        const unblock = e => { e.stopImmediatePropagation(); };
        ['copy', 'cut', 'paste', 'contextmenu', 'selectstart', 'dragstart']
            .forEach(t => document.addEventListener(t, unblock, true));
        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);
        new MutationObserver(muts => {
            for (const m of muts) {
                if (m.type === 'attributes' && m.attributeName === 'style') fixInline(m.target);
                if (m.addedNodes) m.addedNodes.forEach(n => { if (n.nodeType === 1) fixInline(n); });
            }
        }).observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['style'] });
    })();

    // --------------------------------------------------------------------------
    //************** 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]','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 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);

        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); });

        var ref = document.referrer || '';
        var refHost = '';
        try { refHost = new URL(ref).hostname; } catch (_) {}
        var refIsParent = PARENT_DOMAINS.some(function(p){ return matchHost(refHost, p); });

        var fromAnimeParent = isTop && (isParentHost || !!document.getElementById('playerDF'));
        var fromAnimeIframe = !isTop && refIsParent;

        _log('[init]', { host:SITE, isTop:isTop, fromAnimeParent:fromAnimeParent, fromAnimeIframe:fromAnimeIframe, refHost:refHost });

        var pendingToggle = false;
        try {
            if (sessionStorage.getItem('asp_pendingToggle') === '1') {
                pendingToggle = true;
                sessionStorage.removeItem('asp_pendingToggle');
                _log('[parent] pendingToggle restauré depuis sessionStorage');
            }
        } catch(_) {}
        var prevEp = window.prevEp || function() { _warn('prevEp non défini'); };
        var nextEp = window.nextEp || function() { _warn('nextEp non défini'); };

        var EPS = 3;

        //**** PARENT: raccourcis clavier ****
        function parentKeyHandler(e) {
            if (/input|textarea|select/i.test(e.target.tagName)) return;
            var iframe = document.getElementById('playerDF');

            // Touches navigation : toujours actives
            switch (e.key) {
                case 'n': case 'N':
                    e.preventDefault();
                    pendingToggle = true;
                    try { sessionStorage.setItem('asp_pendingToggle', '1'); } catch(_) {}
                    nextEp();
                    return;
                case 'p': case 'P':
                    e.preventDefault();
                    pendingToggle = true;
                    try { sessionStorage.setItem('asp_pendingToggle', '1'); } catch(_) {}
                    prevEp();
                    return;
            }

            // Touches de contrôle : soumises à CONTROL_DOMAINS
            if (!isControlHost) return;

            switch (e.key) {
                case ' ':
                    e.preventDefault();
                    pendingToggle = true;
                    if (iframe && iframe.contentWindow)
                        iframe.contentWindow.postMessage({ action: 'togglePlay' }, '*');
                    break;
                case 'ArrowRight':
                    e.preventDefault();
                    if (iframe && iframe.contentWindow)
                        iframe.contentWindow.postMessage({ action: 'seekForward', value: 10 }, '*');
                    break;
                case 'ArrowLeft':
                    e.preventDefault();
                    if (iframe && iframe.contentWindow)
                        iframe.contentWindow.postMessage({ action: 'seekBackward', value: 10 }, '*');
                    break;
                case 'ArrowUp':
                    e.preventDefault();
                    if (iframe && iframe.contentWindow)
                        iframe.contentWindow.postMessage({ action: 'volumeUp', value: 10 }, '*');
                    break;
                case 'ArrowDown':
                    e.preventDefault();
                    if (iframe && iframe.contentWindow)
                        iframe.contentWindow.postMessage({ action: 'volumeDown', value: 10 }, '*');
                    break;
                case 'f': case 'F':
                    e.preventDefault();
                    if (iframe && iframe.contentWindow)
                        iframe.contentWindow.postMessage({ action: 'toggleFullscreen' }, '*');
                    break;
            }
        }

        //**** PARENT: message handler ****
        function messageHandler(e) {
            if (!e.data || !e.data.action) return;
            _log('[parent] msg:', e.data.action);
            switch (e.data.action) {
                case 'Istart':
                    if (pendingToggle) {
                        pendingToggle = false;
                        var iframe = document.getElementById('playerDF');
                        if (iframe && iframe.contentWindow)
                            iframe.contentWindow.postMessage({ action: 'togglePlay' }, '*');
                    }
                    break;
                case 'nextEp':
                    pendingToggle = true;
                    try { sessionStorage.setItem('asp_pendingToggle', '1'); } catch(_) {}
                    nextEp();
                    break;
                case 'prevEp':
                    pendingToggle = true;
                    try { sessionStorage.setItem('asp_pendingToggle', '1'); } catch(_) {}
                    prevEp();
                    break;
            }
        }

        //**** IFRAME (<video> natif): raccourcis clavier ****
        function iframeKeyHandler(e) {
            if (/input|textarea|select/i.test(e.target.tagName)) return;
            var v = document.querySelector('video');

            // Touches navigation : toujours actives
            switch (e.key) {
                case 'n': case 'N':
                    e.preventDefault();
                    window.parent.postMessage({ action: 'nextEp' }, '*');
                    return;
                case 'p': case 'P':
                    e.preventDefault();
                    window.parent.postMessage({ action: 'prevEp' }, '*');
                    return;
            }

            // Touches de contrôle : soumises à CONTROL_DOMAINS
            if (!isControlHost || !v) return;

            switch (e.key) {
                case ' ':
                    e.preventDefault();
                    break;
                case 'ArrowRight':
                    e.preventDefault();
                    v.currentTime = Math.min(v.duration, v.currentTime + 10);
                    break;
                case 'ArrowLeft':
                    e.preventDefault();
                    v.currentTime = Math.max(0, v.currentTime - 10);
                    break;
                case 'ArrowUp':
                    e.preventDefault();
                    v.volume = Math.min(1, v.volume + 0.1);
                    break;
                case 'ArrowDown':
                    e.preventDefault();
                    v.volume = Math.max(0, v.volume - 0.1);
                    break;
                case 'f': case 'F':
                    e.preventDefault();
                    if (document.fullscreenElement) document.exitFullscreen();
                    else if (v.requestFullscreen) v.requestFullscreen();
                    break;
            }
        }

        //**** IFRAME: toggle play/pause ****
        function togglePlayPauseAfterDelay() {
            setTimeout(function() {
                var v = document.querySelector('video');
                if (!v) return;
                if (v.paused) v.play().catch(function(){});
                else v.pause();
            }, 300);
        }

        //**** IFRAME (<video> natif): détection fin ****
        function addVideoEndDetectors() {
            var sent = false;
            function sendNext(src) {
                if (sent) return;
                sent = true;
                _log('[iframe] → nextEp via', src);
                window.parent.postMessage({ action: 'nextEp' }, '*');
            }

            function attachToVideo() {
                var v = document.querySelector('video');
                if (!v) return;
                v.addEventListener('ended', function() { sendNext('ended'); });
                var lastT = -1;
                var stallTimer = setInterval(function() {
                    if (sent) { clearInterval(stallTimer); return; }
                    var d = v.duration;
                    if (!isFinite(d) || !d) return;
                    var now = v.currentTime;
                    if (now === lastT && (d - now) <= EPS && v.paused) {
                        sendNext('stall-end');
                        clearInterval(stallTimer);
                    }
                    lastT = now;
                }, 1000);
            }

            if (document.querySelector('video')) attachToVideo();
            else {
                var obs = new MutationObserver(function() {
                    if (document.querySelector('video')) {
                        obs.disconnect();
                        attachToVideo();
                    }
                });
                obs.observe(document.body, { childList: true, subtree: true });
            }
        }

        //**********************
        //* IFRAME JW PLAYER (VidMoly, etc.)
        //**********************
        function detectJWPlayer() {
            try {
                var p = jwplayer();
                return (p && typeof p.getState === 'function') ? p : null;
            } catch (_) { return null; }
        }

        function waitForJWPlayer(timeout) {
            timeout = timeout || 10000;
            return new Promise(function(resolve, reject) {
                var t0 = Date.now();
                var check = setInterval(function() {
                    var p = detectJWPlayer();
                    if (p) { clearInterval(check); resolve(p); }
                    if (Date.now() - t0 > timeout) { clearInterval(check); reject(new Error('JW timeout')); }
                }, 300);
            });
        }

        function forcePlayJW(player) {
            _log('[JW] forcePlay, state:', player.getState());
            var overlays = [
                '.jw-display-icon-container',
                '.jw-icon-display',
                '.jw-controls .jw-icon-playback',
                '.vjs-big-play-button'
            ];
            for (var i = 0; i < overlays.length; i++) {
                var el = document.querySelector(overlays[i]);
                if (el) {
                    _log('[JW] click overlay:', overlays[i]);
                    el.click();
                    break;
                }
            }
            try { player.play(); } catch(e) { _err('[JW] play():', e); }
            setTimeout(function() {
                if (player.getState() !== 'playing') {
                    _log('[JW] retry play');
                    try { player.play(); } catch(_) {}
                }
            }, 500);
            setTimeout(function() {
                if (player.getState() !== 'playing') {
                    _log('[JW] retry2 play + fallback <video>');
                    try { player.play(); } catch(_) {}
                    var v = document.querySelector('video');
                    if (v && v.paused) v.play().catch(function(){});
                }
            }, 1500);
        }

        function jwKeyHandler(player, e) {
            if (/input|textarea|select/i.test(e.target.tagName)) return;

            // Touches navigation : toujours actives
            switch (e.key) {
                case 'n': case 'N':
                    e.preventDefault();
                    window.parent.postMessage({ action: 'nextEp' }, '*');
                    return;
                case 'p': case 'P':
                    e.preventDefault();
                    window.parent.postMessage({ action: 'prevEp' }, '*');
                    return;
            }

            // Touches de contrôle : soumises à CONTROL_DOMAINS
            if (!isControlHost) return;

            switch (e.key) {
                case ' ':
                    e.preventDefault();
                    player.getState() === 'playing' ? player.pause() : player.play();
                    break;
                case 'ArrowRight':
                    e.preventDefault();
                    player.seek(player.getPosition() + 10);
                    break;
                case 'ArrowLeft':
                    e.preventDefault();
                    player.seek(Math.max(0, player.getPosition() - 10));
                    break;
                case 'ArrowUp':
                    e.preventDefault();
                    player.setVolume(Math.min(100, player.getVolume() + 10));
                    break;
                case 'ArrowDown':
                    e.preventDefault();
                    player.setVolume(Math.max(0, player.getVolume() - 10));
                    break;
                case 'f': case 'F':
                    e.preventDefault();
                    player.setFullscreen(!player.getFullscreen());
                    break;
            }
        }

        function attachJWEndDetectors(player) {
            var sent = false;
            function sendNext(src) {
                if (sent) return;
                sent = true;
                _log('[JW] → nextEp via', src);
                window.parent.postMessage({ action: 'nextEp' }, '*');
            }

            // 1) JW complete event
            player.on('complete', function() { sendNext('jw-complete'); });

            // 2) Fallback <video> ended
            var v = document.querySelector('video');
            if (v) {
                v.addEventListener('ended', function() { sendNext('video-ended'); });
            }

            // 3) Détection fin : remaining <= 0.5 pendant 2 checks consécutifs
            var endCount = 0;
            var stallInterval = setInterval(function() {
                if (sent) { 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] fin proche, count:', endCount, 'remaining:', remaining);
                        if (endCount >= 2) {
                            sendNext('jw-end-detect');
                            clearInterval(stallInterval);
                        }
                    } else {
                        endCount = 0;
                    }
                } catch (_) {}
            }, 1000);
        }

        function attachJWIframeHandlers(player) {
            _log('[JW] context=iframe, state:', player.getState());

            document.addEventListener('keydown', function(e) { jwKeyHandler(player, e); }, true);

            window.addEventListener('message', function(e) {
                if (!e.data || !e.data.action) return;
                _log('[JW] msg reçu:', e.data.action);
                switch (e.data.action) {
                    case 'togglePlay':
                        try { window.focus(); } catch(_) {}
                        forcePlayJW(player);
                        break;
                    case 'seekForward':
                        player.seek(player.getPosition() + (e.data.value || 10));
                        break;
                    case 'seekBackward':
                        player.seek(Math.max(0, player.getPosition() - (e.data.value || 10)));
                        break;
                    case 'volumeUp':
                        player.setVolume(Math.min(100, player.getVolume() + (e.data.value || 10)));
                        break;
                    case 'volumeDown':
                        player.setVolume(Math.max(0, player.getVolume() - (e.data.value || 10)));
                        break;
                    case 'toggleFullscreen':
                        player.setFullscreen(!player.getFullscreen());
                        break;
                }
            });

            attachJWEndDetectors(player);
            _log('[JW] → Istart');
            window.parent.postMessage({ action: 'Istart' }, '*');
        }

        //**********************
        //* ROUTAGE PARENT / IFRAME
        //**********************
        function attachParentHandlers() {
            _log('context=parent');
            document.addEventListener('keydown', parentKeyHandler, true);
            window.addEventListener('message', messageHandler);
        }

        function attachIframeHandlers() {
            _log('context=iframe');

            // Détection synchrone JW
            var jwp = detectJWPlayer();
            if (jwp) {
                attachJWIframeHandlers(jwp);
                return;
            }

            // Envoyer Istart + attacher video natif TOUT DE SUITE
            document.addEventListener('keydown', iframeKeyHandler, true);
            window.addEventListener('message', function(e) {
                if (!e.data || !e.data.action) return;
                _log('[iframe] msg reçu:', e.data.action);
                var v;
                switch (e.data.action) {
                    case 'togglePlay':
                        try { window.focus(); } catch(_) {}
                        v = document.querySelector('video');
                        if (v) v.focus();
                        togglePlayPauseAfterDelay();
                        break;
                    case 'seekForward':
                        v = document.querySelector('video');
                        if (v) v.currentTime = Math.min(v.duration, v.currentTime + (e.data.value || 10));
                        break;
                    case 'seekBackward':
                        v = document.querySelector('video');
                        if (v) v.currentTime = Math.max(0, v.currentTime - (e.data.value || 10));
                        break;
                    case 'volumeUp':
                        v = document.querySelector('video');
                        if (v) v.volume = Math.min(1, v.volume + (e.data.value || 10) / 100);
                        break;
                    case 'volumeDown':
                        v = document.querySelector('video');
                        if (v) v.volume = Math.max(0, v.volume - (e.data.value || 10) / 100);
                        break;
                    case 'toggleFullscreen':
                        v = document.querySelector('video');
                        if (v) {
                            if (document.fullscreenElement) document.exitFullscreen();
                            else if (v.requestFullscreen) v.requestFullscreen();
                        }
                        break;
                }
            });
            addVideoEndDetectors();

            // Envoyer Istart dès que possible
            function sendIstart() {
                _log('[iframe] → Istart');
                window.parent.postMessage({ action: 'Istart' }, '*');
            }
            if (document.readyState === 'complete') setTimeout(sendIstart, 100);
            else window.addEventListener('load', function() { setTimeout(sendIstart, 100); });

            // Tenter JW en arrière-plan (upgrade si trouvé)
            waitForJWPlayer(3000).then(function(player) {
                _log('[iframe] JW détecté tardivement, upgrade');
                document.removeEventListener('keydown', iframeKeyHandler, true);
                attachJWIframeHandlers(player);
            }).catch(function() {
                _log('[iframe] Confirmé: pas de JW, video natif actif');
            });
        }

        if (fromAnimeParent)       attachParentHandlers();
        else if (fromAnimeIframe)  attachIframeHandlers();
        else {
            var hasPlayerIframeId = !!document.getElementById('playerDF');
            var hasVideo = !!document.querySelector('video');
            _warn('context unknown -> fallback', { hasPlayerIframeId:hasPlayerIframeId, hasVideo:hasVideo });
            if (isTop && hasPlayerIframeId) attachParentHandlers();
            else if (!isTop && hasVideo)    attachIframeHandlers();
            else if (!isTop)                attachIframeHandlers();
            else                            _warn('fallback -> nothing to attach');
        }
    })();
    `;

    const script = document.createElement('script');
    script.defer = true;
    script.textContent = injectedCode;
    document.documentElement.appendChild(script);
    script.remove();
})();