anime-sama Plus

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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