// ==UserScript== // @name anime-sama Plus // @namespace http://tampermonkey.net/ // @version 0.1.4 // @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 // @downloadURL none // ==/UserScript== (function () { 'use strict'; // -------------------------------------------------------------------------- // Configuration // Domaines "parent" (site Anime-Sama) const P_DOMAINS = ['anime-sama.fr', 'anime-sama.org']; // 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 = '

Comment voulez-vous restaurer ?

'; 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 = `
${mode === 'backup' ? 'Mot de passe de sauvegarde' : 'Mot de passe de restauration'}
Laissez vide pour utiliser SAMA par défaut.
`; 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 = ` `; 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); const isParentHost = PARENT_DOMAINS.some(d => SITE === d || SITE.endsWith('.' + d)); const ref = document.referrer || ''; let refHost = ''; try { refHost = new URL(ref).hostname; } catch (_) {} const refIsParent = PARENT_DOMAINS.some(d => refHost === d || refHost.endsWith('.' + d)); const fromAnimeParent = isTop && (isParentHost || !!document.getElementById('playerDF')); const 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(); })();