Greasy Fork

Greasy Fork is available in English.

anime-sama Plus

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

当前为 2025-12-09 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         anime-sama Plus
// @namespace    http://tampermonkey.net/
// @version      0.1.5
// @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        *://*.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/*
// @match        *://*.vidmoly.net/*
// @match        *://*.vidmoly.to/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=anime-sama.org
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    // --------------------------------------------------------------------------
    // Configuration
    // Domaines "parent" (site Anime-Sama)
    const P_DOMAINS = ['anime-sama'];
    // Domaines lecteurs sans contrôles clavier natifs : flèches/espace/plein écran forcés
    const C_DOMAINS = ['sendvid.com'];

    // --------------------------------------------------------------------------
    // 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', 'R', '#0b6'));
            box.appendChild(mk('Annuler', 'C', '#e53e3e'));
            overlay.appendChild(box);
            document.body.appendChild(overlay);
        });
    }

    // --------------------------------------------------------------------------
    // UI - Mot de passe avec fallback "SAMA" + mémorisation
    async function showPasswordDialog(mode /* 'backup'|'restore' */) {
        return new Promise(resolve => {
            const overlay = document.createElement('div');
            Object.assign(overlay.style, {
                position: 'fixed', inset: 0, background: '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: '18px 16px',
                borderRadius: '10px', width: 'min(92vw, 360px)',
                fontFamily: 'sans-serif', boxShadow: '0 10px 30px rgba(0,0,0,.4)'
            });
            box.innerHTML = `
                <div style="font-weight:700;font-size:16px;margin-bottom:8px">
                    ${mode === 'backup' ? 'Mot de passe de sauvegarde' : 'Mot de passe de restauration'}
            </div>
            <div style="font-size:13px;opacity:.9;margin-bottom:10px">
                Laissez vide pour utiliser <b>SAMA</b> par défaut.
            </div>

            <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"/>

                <!-- Avertissement ultra visible -->
                <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>

            <div style="display:flex;gap:8px;justify-content:flex-end">
                <button id="asplus-cancel"
            style="padding:8px 10px;border:0;border-radius:6px;background:#555;color:#fff;cursor:pointer">
                Annuler
            </button>
            <button id="asplus-ok"
            style="padding:8px 10px;border:0;border-radius:6px;background:#0c6;color:#000;font-weight:700;cursor:pointer">
                OK
            </button>
            </div>
            `;
            overlay.appendChild(box);
            document.body.appendChild(overlay);
            const $ = (s) => box.querySelector(s);
            $('#asplus-cancel').onclick = () => { document.body.removeChild(overlay); resolve({ pass: null, remember: false }); };
            $('#asplus-ok').onclick = () => {
                const val = $('#asplus-pass').value || '';
                const remember = $('#asplus-remember').checked;
                document.body.removeChild(overlay);
                resolve({ pass: val, remember });
            };
            $('#asplus-pass').addEventListener('keydown', e => { if (e.key === 'Enter') $('#asplus-ok').click(); });
            $('#asplus-pass').focus();
        });
    }

    async function getPassphrase(mode /* 'backup'|'restore' */) {
        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;
    }

    // --------------------------------------------------------------------------
    // Fichiers .sama (MIME dédié)
    async function pickFileToSave(blob) {
        const EXT = '.sama';
        const MIME = 'application/vnd.animesama.backup';
        if (window.showSaveFilePicker) {
            const opts = {
                suggestedName: 'Profil Anime-Sama' + EXT,
                excludeAcceptAllOption: true,
                types: [{ description: 'Backup Anime-Sama (*.sama)', accept: { [MIME]: [EXT] } }]
            };
            let handle = await window.showSaveFilePicker(opts);
            if (!handle.name.toLowerCase().endsWith(EXT)) {
                handle = await window.showSaveFilePicker({ ...opts, suggestedName: handle.name.replace(/\.[^.]*$/, '') + EXT });
            }
            const writer = await handle.createWritable();
            await writer.write(blob);
            await writer.close();
            return;
        }
        const url = URL.createObjectURL(blob);
        const a = Object.assign(document.createElement('a'), { href: url, download: 'Profil Anime-Sama' + EXT });
        document.body.append(a); a.click(); a.remove();
        URL.revokeObjectURL(url);
    }

    async function pickFileToOpen() {
        const EXT = '.sama';
        const MIME = 'application/vnd.animesama.backup';
        if (window.showOpenFilePicker) {
            const [handle] = await window.showOpenFilePicker({
                excludeAcceptAllOption: true,
                types: [{ description: 'Backup Anime-Sama (*.sama)', accept: { [MIME]: [EXT] } }]
            });
            return await handle.getFile();
        }
        return new Promise(resolve => {
            const input = document.createElement('input');
            input.type = 'file';
            input.accept = EXT;
            input.onchange = () => resolve(input.files[0]);
            input.click();
        });
    }

    // --------------------------------------------------------------------------
    // Sauvegarde / Restauration (AES-GCM 256, IV = salt pour PBKDF2)
    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) {
            console.error('Backup failed:', e);
            alert('Sauvegarde échouée.');
        }
    }

    async function restoreProfile() {
        try {
            const file = await pickFileToOpen();
            const array = await file.arrayBuffer();
            const iv = new Uint8Array(array.slice(0, 12));
            const ciphertext = array.slice(12);
            const encoder = new TextEncoder();
            const passphrase = await getPassphrase('restore');
            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, ciphertext);
            const data = JSON.parse(new TextDecoder().decode(decrypted));
            const choice = await showChoiceDialog();
            if (choice === 'C') return;
            const doReplace = choice === 'R';
            if (doReplace) localStorage.clear();
            Object.keys(data).forEach(key => { if (doReplace) localStorage.setItem(key, data[key]); });
            location.reload();
        } catch (e) {
            console.error('Restore failed:', e);
            alert('Restauration échouée — mot de passe incorrect ou fichier corrompu ?');
        }
    }

    // --------------------------------------------------------------------------
    // Menu Profil (robuste SPA + recréation si supprimé)
    function createProfileDropdown() {
        const nav = document.querySelector('.asn-nav-desktop');
        if (!nav) return;
        if (nav.querySelector('#tampered-dropdown')) return;

        // supprime le lien profil d'origine s'il existe
        const oldLink = nav.querySelector('a[href*="/profil"]');
        if (oldLink) oldLink.remove();

        const wrapper = document.createElement('div');
        wrapper.id = 'tampered-dropdown';
        wrapper.className = 'relative inline-block text-left';
        const btn = document.createElement('button');
        btn.type = 'button';
        btn.className = 'inline-flex uppercase text-base font-extrabold text-white hover:text-sky-500 hover:bg-gray-700 transition-all duration-200 focus:outline-none';
        btn.innerHTML = `
                <svg xmlns="http://www.w3.org/2000/svg" class="h-7 w-7 my-3.5 mx-3 xl:mr-0 text-white" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-6-3a2 2 0 11-4 0 2 2 0 014 0zm-2 4a5 5 0 00-4.546 2.916A5.986 5.986 0 0010 16a5.986 5.986 0 004.546-2.084A5 5 0 0010 11z" clip-rule="evenodd"/></svg>
            <p class="hidden xl:block p-3 m-1">profil</p>
            <svg xmlns="http://www.w3.org/2000/svg" class="h-7 w-7 my-3.5 mx-3 xl:mr-0 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);

        const menu = document.createElement('div');
        Object.assign(menu.style, {
            display: 'none', position: 'absolute', right: '0', marginTop: '0.5rem',
            backgroundColor: 'rgba(0,0,0,0.9)', borderRadius: '0.25rem', border: 'inset',
            boxShadow: '0 2px 8px rgba(0,0,0,0.5)'
        });

        [['Voir Profil', () => window.location.href = '/profil'],
         ['Sauvegarde Profil', backupProfile],
         ['Restauration Profil', restoreProfile]
        ].forEach(([label, action]) => {
            const item = document.createElement('button');
            item.textContent = label;
            Object.assign(item.style, {
                display: 'block', width: '100%', padding: '0.5rem 1rem',
                textAlign: 'left', color: 'white', background: 'transparent', border: 'none', cursor: 'pointer'
            });
            item.addEventListener('mouseenter', () => item.style.background = 'rgba(255,255,255,0.1)');
            item.addEventListener('mouseleave', () => item.style.background = 'transparent');
            item.addEventListener('click', () => { action(); menu.style.display = 'none'; btn.querySelector('svg:last-child').style.transform = ''; });
            menu.appendChild(item);
        });

        wrapper.appendChild(menu);
        nav.appendChild(wrapper);

        btn.addEventListener('click', e => {
            e.stopPropagation();
            const open = menu.style.display === 'block';
            menu.style.display = open ? 'none' : 'block';
            btn.querySelector('svg:last-child').style.transform = open ? '' : 'rotate(180deg)';
            btn.setAttribute('aria-expanded', String(!open));
        });
        document.addEventListener('click', () => { menu.style.display = 'none'; btn.querySelector('svg:last-child').style.transform = ''; btn.setAttribute('aria-expanded', 'false'); });
    }

    function ensureProfileDropdown() {
        const nav = document.querySelector('.asn-nav-desktop');
        const exists = !!document.querySelector('#tampered-dropdown');
        if (nav && !exists) 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 (global)
    (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 () {
              const CONTROL_DOMAINS = ${JSON.stringify(C_DOMAINS)};
              const PARENT_DOMAINS  = ${JSON.stringify(P_DOMAINS)};

              const SITE = location.hostname;
              const 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 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;

              console.log('[ASP][init]', { host:SITE, isTop, fromAnimeParent, fromAnimeIframe, refHost, cDomains: CONTROL_DOMAINS });

              let pendingToggle = false;
              const prevEp = window.prevEp || (() => console.warn('[ASP] prevEp non défini'));
              const nextEp = window.nextEp || (() => console.warn('[ASP] nextEp non défini'));

              function messageHandler(e) {
                  const action = e && e.data && e.data.action;
                  const iframe = document.getElementById('playerDF');
                  if (action === 'prevEp') { pendingToggle = true; prevEp(); }
                  else if (action === 'nextEp') { pendingToggle = true; nextEp(); }
                  if (pendingToggle && action === 'Istart') {
                      if (iframe && iframe.contentWindow) iframe.contentWindow.postMessage({ action: 'togglePlay' }, '*');
                      pendingToggle = false;
                  }
              }

              function iframeKeyHandler(e) {
                  if (/input|textarea/i.test(e.target && e.target.tagName)) return;
                  const host = window.location.hostname;
                  const isControlSite = CONTROL_DOMAINS.some(d => host === d || host.endsWith('.' + d));
                  const video = document.querySelector('video');

                  if (isControlSite && video) {
                      switch (e.key) {
                          case 'ArrowRight': e.preventDefault(); video.currentTime = Math.min(video.duration, video.currentTime + 5); break;
                          case 'ArrowLeft':  e.preventDefault(); video.currentTime = Math.max(0, video.currentTime - 5); break;
                          case ' ': case 'Spacebar': e.preventDefault(); video.paused ? video.play() : video.pause(); break;
                          case 'f': case 'F':
                              e.preventDefault();
                              const fsBtn = document.querySelector('.vjs-fullscreen-control');
                              if (fsBtn) fsBtn.click();
                              else {
                                  if (!document.fullscreenElement && video.requestFullscreen) video.requestFullscreen();
                                  else if (document.exitFullscreen) document.exitFullscreen();
                              }
                              break;
                          case 'ArrowUp':   e.preventDefault(); video.volume = Math.min(1, +(video.volume + 0.1).toFixed(2)); break;
                          case 'ArrowDown': e.preventDefault(); video.volume = Math.max(0, +(video.volume - 0.1).toFixed(2)); break;
                      }
                  }
                  if (e.key === 'p') window.parent.postMessage({ action: 'prevEp' }, '*');
                  else if (e.key === 'n') { console.log('[ASP][iframe] nextEp'); window.parent.postMessage({ action: 'nextEp' }, '*'); }
              }

              function togglePlayPauseAfterDelay() {
                  setTimeout(() => {
                      const video = document.querySelector('video');
                      if (video) video.paused ? video.play() : video.pause();
                  }, 0);
              }

              function addVideoEndDetectors() {
                  const v = document.querySelector('video');
                  if (!v) { setTimeout(addVideoEndDetectors, 500); return; }
                  let sent = false;
                  const sendNext = (reason) => { if (sent) return; sent = true; console.log('[ASP][AutoNext]', reason || 'unknown'); window.parent.postMessage({ action: 'nextEp' }, '*'); };
                  v.addEventListener('ended', () => sendNext('ended'));
                  try {
                      if (window.jwplayer) {
                          const p = window.jwplayer();
                          if (p && typeof p.on === 'function') {
                              p.on('complete', () => sendNext('jw:complete'));
                              p.on('playlistComplete', () => sendNext('jw:playlistComplete'));
                          }
                      }
                  } catch (_) {}
                  const EPS = 1.0, NEED = 3; let nearTicks = 0;
                  v.addEventListener('timeupdate', () => {
                      if (sent) return;
                      const d = v.duration; if (!isFinite(d) || !d) return;
                      const rem = d - v.currentTime;
                      if (rem <= EPS) { if (++nearTicks >= NEED) sendNext('near-end'); }
                      else nearTicks = 0;
                  });
                  let lastT = v.currentTime;
                  const stallTimer = setInterval(() => {
                      if (sent) { clearInterval(stallTimer); return; }
                      const d = v.duration; if (!isFinite(d) || !d) return;
                      const now = v.currentTime;
                      if (now === lastT && (d - now) <= EPS && v.paused) { sendNext('stall-end'); clearInterval(stallTimer); }
                      lastT = now;
                  }, 1000);
              }

              function enablePlayerLogging() {
                  function attach() {
                      const v = document.querySelector('video');
                      if (!v) { setTimeout(attach, 400); return; }
                      if (v.dataset.logAttached) return;
                      v.dataset.logAttached = '1';
                      function stamp(){ return new Date().toLocaleTimeString(); }
                      function log(msg, extra){ console.log('[Player][' + stamp() + '] ' + msg + (extra ? ' ' + extra : '')); }
                      const events = ['play','pause','ended','seeking','seeked','waiting','stalled','error','loadedmetadata','loadeddata','canplay','ratechange','volumechange','timeupdate'];
                      let lastTU = 0;
                      events.forEach((ev) => v.addEventListener(ev, () => {
                          if (ev === 'timeupdate') {
                              const now = performance.now();
                              if (now - lastTU > 1000) { lastTU = now; log('timeupdate', 't=' + v.currentTime.toFixed(1) + '/' + ((v.duration||0).toFixed(1))); }
                              return;
                          }
                          if (ev === 'volumechange') return log('volume', '=' + Math.round(v.volume*100) + '% muted=' + v.muted);
                          if (ev === 'ratechange')   return log('rate', '=' + v.playbackRate);
                          if (ev === 'error')        return log('error', v.error ? ('code=' + v.error.code) : '');
                          log(ev);
                      }, true));
                      v.addEventListener('click', () => log('click(video)'), true);
                      document.addEventListener('fullscreenchange', () => log(document.fullscreenElement ? 'fullscreen:enter' : 'fullscreen:exit'), true);
                      window.addEventListener('message', (e) => { if (e.data && e.data.action) log('postMessage:' + e.data.action); }, true);
                  }
                  attach();
              }

              function attachParentHandlers() {
                  console.log('[ASP] context=parent');
                  window.addEventListener('message', messageHandler);
              }
              function attachIframeHandlers() {
                  console.log('[ASP] context=iframe');
                  document.addEventListener('keydown', iframeKeyHandler, true);
                  window.addEventListener('message', (e) => {
                      if (e.data && e.data.action === 'togglePlay') {
                          try { window.focus(); } catch (_e) {}
                          const v = document.querySelector('video'); if (v) v.focus();
                          togglePlayPauseAfterDelay();
                      }
                  });
                  window.addEventListener('load', () => { setTimeout(() => window.parent.postMessage({ action: 'Istart' }, '*'), 100); });
                  addVideoEndDetectors();
                  //enablePlayerLogging();
              }

              if (fromAnimeParent)      attachParentHandlers();
              else if (fromAnimeIframe) attachIframeHandlers();
              else {
                  const hasPlayerIframeId = !!document.getElementById('playerDF');
                  const hasVideo = !!document.querySelector('video');
                  console.warn('[ASP] context unknown -> fallback', { hasPlayerIframeId, hasVideo });
                  if (isTop && hasPlayerIframeId) attachParentHandlers();
                  else if (!isTop && hasVideo)    attachIframeHandlers();
                  else                             console.warn('[ASP] fallback -> nothing to attach');
              }
          })();
    `;

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