Greasy Fork

来自缓存

Greasy Fork is available in English.

EasyTube V4.0 — 广告跳过、SponsorBlock 和 HD 下载器(无卡顿)

V4.0 — 性能重写版。🚫 多层广告跳过,低CPU占用。⏭ SponsorBlock(9类别)。🎥 4K画质。⬇ 通过evdfrance.fr下载视频。💾 保存设置。绕过广告拦截检测。无卡顿。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name               EasyTube V4.0 — Ad Skip & SponsorBlock & HD Download⬇️🚀 (No Lag)
// @name:vi            EasyTube V4.0 — Bỏ qua quảng cáo, SponsorBlock & Tải HD (Không lag)
// @name:zh-CN         EasyTube V4.0 — 广告跳过、SponsorBlock 和 HD 下载器(无卡顿)
// @name:zh-TW         EasyTube V4.0 — 廣告跳過、SponsorBlock 和 HD 下載器(無卡頓)
// @name:ru            EasyTube V4.0 — Пропуск рекламы, SponsorBlock и HD загрузчик (без лагов)
// @name:ja            EasyTube V4.0 — 広告スキップ・SponsorBlock・HDダウンローダー(ラグなし)
// @name:ko            EasyTube V4.0 — 광고 건너뛰기, SponsorBlock, HD 다운로더 (렉 없음)
// @name:es            EasyTube V4.0 — Saltar anuncios, SponsorBlock y descarga HD (sin lag)
// @name:pt-BR         EasyTube V4.0 — Pular anúncios, SponsorBlock e download HD (sem lag)
// @name:fr            EasyTube V4.0 — Ignorer pubs, SponsorBlock et téléchargeur HD (sans lag)
// @name:de            EasyTube V4.0 — Werbung überspringen, SponsorBlock, HD-Download (kein Lag)
// @name:tr            EasyTube V4.0 — Reklam Atlama, SponsorBlock ve HD İndirici (Lag Yok)
// @name:pl            EasyTube V4.0 — Pomijanie reklam, SponsorBlock, pobieranie HD (bez opóźnień)
// @name:id            EasyTube V4.0 — Lewati Iklan, SponsorBlock & Unduhan HD (Tanpa Lag)
// @name:th            EasyTube V4.0 — ข้ามโฆษณา, SponsorBlock และดาวน์โหลด HD (ไม่กระตุก)
// @name:ar            EasyTube V4.0 — تخطي الإعلانات، SponsorBlock وتنزيل HD (بدون تأخير)

// @description        V4.0 — Rewritten for performance. 🚫 Multi-layer ad skip with minimal CPU usage. ⏭ SponsorBlock (9 cats). 🎥 Force 4K. ⬇ Download via evdfrance.fr. 💾 Remembers settings. Bypass adblock detection. No Cobalt. No lag.
// @description:vi     V4.0 — Viết lại tối ưu hiệu năng. 🚫 Skip quảng cáo đa lớp, CPU thấp. ⏭ SponsorBlock 9 danh mục. 🎥 Ép 4K. ⬇ Tải video qua evdfrance.fr. 💾 Lưu cài đặt. Bypass popup adblock. Không lag.
// @description:zh-CN  V4.0 — 性能重写版。🚫 多层广告跳过,低CPU占用。⏭ SponsorBlock(9类别)。🎥 4K画质。⬇ 通过evdfrance.fr下载视频。💾 保存设置。绕过广告拦截检测。无卡顿。
// @description:zh-TW  V4.0 — 效能重寫版。🚫 多層廣告跳過,低CPU占用。⏭ SponsorBlock(9類別)。🎥 4K畫質。⬇ 透過evdfrance.fr下載影片。💾 儲存設定。繞過廣告攔截偵測。無卡頓。
// @description:ru     V4.0 — Переписано для производительности. 🚫 Мгновенный пропуск рекламы. ⏭ SponsorBlock (9 категорий). 🎥 4K качество. ⬇ Загрузка через evdfrance.fr. 💾 Сохранение настроек. Без лагов.
// @description:ja     V4.0 — パフォーマンス最適化版。🚫 全広告を即時スキップ。⏭ SponsorBlock(9カテゴリ)。🎥 4K画質。⬇ evdfrance.frでダウンロード。💾 設定保存。広告ブロック検出回避。ラグなし。
// @description:ko     V4.0 — 성능 최적화 재작성. 🚫 즉시 광고 스킵, CPU 절약. ⏭ SponsorBlock(9카테고리). 🎥 4K화질. ⬇ evdfrance.fr 다운로드. 💾 설정저장. 광고차단 감지 우회. 렉 없음.
// @description:es     V4.0 — Reescrito para rendimiento. 🚫 Salta anuncios al instante. ⏭ SponsorBlock. 🎥 4K. ⬇ Descarga por evdfrance.fr. 💾 Ajustes guardados. Sin lag.
// @description:fr     V4.0 — Réécrit pour la performance. 🚫 Ignore les pubs instantanément. ⏭ SponsorBlock. 🎥 4K. ⬇ Téléchargement via evdfrance.fr. 💾 Paramètres sauvegardés. Sans lag.
// @description:de     V4.0 — Für Performance neu geschrieben. 🚫 Werbung sofort überspringen. ⏭ SponsorBlock. 🎥 4K. ⬇ Download via evdfrance.fr. 💾 Einstellungen gespeichert. Kein Lag.
// @description:pt-BR  V4.0 — Reescrito para desempenho. 🚫 Pula anúncios instantaneamente. ⏭ SponsorBlock. 🎥 4K. ⬇ Download via evdfrance.fr. 💾 Configurações salvas. Sem lag.
// @description:tr     V4.0 — Performans için yeniden yazıldı. 🚫 Reklamları anında atlar. ⏭ SponsorBlock. 🎥 4K. ⬇ evdfrance.fr ile indirme. 💾 Ayarlar kaydedilir. Lag yok.
// @description:pl     V4.0 — Przepisane dla wydajności. 🚫 Natychmiastowe pomijanie reklam. ⏭ SponsorBlock. 🎥 4K. ⬇ Pobieranie przez evdfrance.fr. 💾 Zapamiętuje ustawienia. Bez opóźnień.
// @description:id     V4.0 — Ditulis ulang untuk performa. 🚫 Lewati iklan seketika. ⏭ SponsorBlock. 🎥 4K. ⬇ Unduh via evdfrance.fr. 💾 Pengaturan tersimpan. Tanpa lag.
// @description:ar     V4.0 — أُعيدت الكتابة لتحسين الأداء. 🚫 تخطي الإعلانات فوراً. ⏭ SponsorBlock. 🎥 4K. ⬇ تنزيل عبر evdfrance.fr. 💾 الإعدادات محفوظة. بدون تأخير.
// @description:th     V4.0 — เขียนใหม่เพื่อประสิทธิภาพ 🚫 ข้ามโฆษณาทันที ⏭ SponsorBlock 🎥 4K ⬇ ดาวน์โหลดผ่าน evdfrance.fr 💾 บันทึกการตั้งค่า ไม่กระตุก

// @namespace          http://greasyfork.icu/users/1510019
// @version            4.0.0
// @author             2pixel (rewrite: performance-optimized)
// @license            MIT
// @icon               https://raw.githubusercontent.com/not2pixel/TampermonkeyProjects/refs/heads/main/EasyTube.png
// @icon64             https://raw.githubusercontent.com/not2pixel/TampermonkeyProjects/refs/heads/main/EasyTube.png

// @match              https://*.youtube.com/*
// @exclude            https://www.youtube.com/live_chat*
// @exclude            https://studio.youtube.com/*

// @grant              GM_addStyle
// @grant              GM_xmlhttpRequest
// @grant              GM_setValue
// @grant              GM_getValue
// @connect            sponsor.ajay.app
// @connect            evdfrance.fr

// @run-at             document-start
// @compatible         chrome   Tampermonkey 4+
// @compatible         firefox  Tampermonkey / Violentmonkey
// @compatible         edge     Tampermonkey 4+
// @compatible         opera    Supported via Tampermonkey / Violentmonkey
// @homepageURL        http://greasyfork.icu/en/scripts/561432
// @supportURL         http://greasyfork.icu/en/scripts/561432/feedback
// ==/UserScript==

'use strict';

// ═══════════════════════════════════════════════════════════════════════════
// LAYER 0 — InnerTube patch (document-start, zero cost)
// Wipes adPlacements before YouTube even schedules them.
// ═══════════════════════════════════════════════════════════════════════════
(function patchYT() {
    // Intercept adPlacements on any object
    try {
        Object.defineProperty(Object.prototype, 'adPlacements', {
            get() { return undefined; },
            set(v) {
                Object.defineProperty(this, 'adPlacements', {
                    value: [], writable: true, configurable: true, enumerable: true,
                });
            },
            configurable: true,
        });
    } catch (_) {}

    // Intercept ytInitialPlayerResponse
    try {
        let _yipr;
        Object.defineProperty(window, 'ytInitialPlayerResponse', {
            get() { return _yipr; },
            set(v) {
                if (v) {
                    if (v.adPlacements) v.adPlacements = [];
                    if (v.auxiliaryUi?.messageRenderers) {
                        try { v.auxiliaryUi.messageRenderers.enforcementMessageViewModel = undefined; } catch {}
                    }
                }
                _yipr = v;
            },
            configurable: true,
        });
    } catch (_) {}
})();

// ═══════════════════════════════════════════════════════════════════════════
// AD CSS — hide ad elements instantly, no JS cost
// ═══════════════════════════════════════════════════════════════════════════
(function injectAdCSS() {
    const style = document.createElement('style');
    style.textContent = `
        ytd-action-companion-ad-renderer, ytd-display-ad-renderer,
        ytd-video-masthead-ad-v3-renderer, ytd-overlay-ad-renderer,
        ytd-promoted-sparkles-web-renderer, ytd-promoted-video-renderer,
        ytd-search-pyv-renderer, ytd-ad-slot-renderer, yt-about-this-ad-renderer,
        .ytd-banner-promo-renderer, #masthead-ad, ytd-mealbar-promo-renderer,
        tp-yt-paper-dialog:has(ytd-mealbar-promo-renderer),
        ytd-in-feed-ad-layout-renderer, ytd-statement-banner-renderer,
        #player-ads, .ytd-ad-slot-renderer,
        ytd-rich-item-renderer:has(ytd-ad-slot-renderer),
        .ytp-ce-element, .ytp-cards-teaser
        { display: none !important; }

        .ytp-ad-text-overlay, .ytp-ad-timed-pie-countdown-container,
        .ytp-ad-image-overlay
        { visibility: hidden !important; opacity: 0 !important; }
    `;
    (document.head || document.documentElement).appendChild(style);
})();

// ═══════════════════════════════════════════════════════════════════════════
// CONFIG
// ═══════════════════════════════════════════════════════════════════════════
const CFG = {
    version: '4.0.0',
    sbApi:  'https://sponsor.ajay.app/api/skipSegments',
    sbCats: ['sponsor','selfpromo','interaction','intro','outro',
             'preview','music_offtopic','filler','exclusive_access'],
    sbTypes: ['skip','mute'],
};

// ═══════════════════════════════════════════════════════════════════════════
// STATE
// ═══════════════════════════════════════════════════════════════════════════
const S = {
    adEnabled:      GM_getValue('et4_ad',      false),
    sbEnabled:      GM_getValue('et4_sb',      false),
    qualityEnabled: GM_getValue('et4_quality', false),

    adCount:        0,
    sbCount:        0,

    adSpeedActive:  false,
    prevVolume:     1,

    sbSegments:     [],
    sbVideoId:      null,
    sbMutedSeg:     null,

    lastUrl:        location.href,
    videoId:        null,

    // Performance: cache DOM nodes
    _player:        null,
    _video:         null,
};

function save(key, val) { GM_setValue(key, val); }

// ═══════════════════════════════════════════════════════════════════════════
// DOM HELPERS — cached, low GC pressure
// ═══════════════════════════════════════════════════════════════════════════
function getPlayer() {
    if (S._player && S._player.isConnected) return S._player;
    S._player = document.getElementById('movie_player');
    return S._player;
}

function getVideo() {
    if (S._video && S._video.isConnected) return S._video;
    const p = getPlayer();
    S._video = p ? p.querySelector('video') : document.querySelector('video');
    return S._video;
}

// Single classList check — much faster than querySelectorAll for hot path
function isAdPlaying() {
    const p = getPlayer();
    if (!p) return false;
    // Check player class (fastest)
    if (p.classList.contains('ad-showing') || p.classList.contains('ad-interrupting')) return true;
    // Check countdown badge (reliable indicator)
    return !!(
        p.querySelector('.ytp-ad-countdown') ||
        p.querySelector('.ytp-ad-simple-ad-badge') ||
        p.querySelector('.ytp-ad-persistent-progress-bar-container')
    );
}

const SKIP_SELECTORS = [
    '.ytp-skip-ad-button',
    '.ytp-ad-skip-button',
    '.ytp-ad-skip-button-modern',
    '.ytp-ad-skip-button-slot button',
    'button[class*="skip-ad"]',
    '[id*="skip-button"] button',
];

function getSkipBtn() {
    const p = getPlayer();
    if (!p) return null;
    for (const sel of SKIP_SELECTORS) {
        const btn = p.querySelector(sel);
        if (btn && btn.offsetParent !== null) return btn;
    }
    return null;
}

function fireClick(el) {
    el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
}

// ═══════════════════════════════════════════════════════════════════════════
// AD ENGINE — debounced, low CPU
// ═══════════════════════════════════════════════════════════════════════════
let _skipRaf = false;

function trySkipAd() {
    if (!S.adEnabled || _skipRaf) return;
    _skipRaf = true;
    // Defer to next frame — batches rapid MutationObserver calls
    requestAnimationFrame(() => {
        _skipRaf = false;
        _doSkip();
    });
}

function _doSkip() {
    const video = getVideo();

    // 1. Skippable ad — seek to end, then click
    const btn = getSkipBtn();
    if (btn) {
        if (video && isFinite(video.duration) && video.duration > 0) {
            video.currentTime = video.duration - 0.1;
        }
        fireClick(btn);
        btn.click();
        if (video) { video.playbackRate = 1; video.muted = false; }
        S.adSpeedActive = false;
        S.adCount++;
        uiSync();
        toast('🚫 Ad skipped!', '#e53935');
        return;
    }

    const adActive = isAdPlaying();

    // 2. Unskippable — 16x speed
    if (video && adActive && !S.adSpeedActive) {
        S.adSpeedActive = true;
        S.prevVolume    = video.volume;
        video.playbackRate = 16;
        video.muted = true;
        toast('⚡ Ad 16×…', '#ff6f00');
    }

    // 3. Restore after ad ends
    if (video && !adActive && S.adSpeedActive) {
        video.playbackRate = 1;
        video.muted  = false;
        video.volume = S.prevVolume;
        S.adSpeedActive = false;
    }

    // 4. Overlay close buttons
    const p = getPlayer();
    if (p) {
        p.querySelectorAll(
            '.ytp-ad-overlay-close-button, .ytp-ad-overlay-slot-close-button'
        ).forEach(b => { try { b.click(); } catch {} });
    }

    // 5. Enforcement popup
    dismissEnforcement();
}

// ═══════════════════════════════════════════════════════════════════════════
// ENFORCEMENT DISMISSAL — runs on a slow timer, not hot path
// ═══════════════════════════════════════════════════════════════════════════
const ENFORCEMENT_SELS = [
    'ytd-enforcement-message-view-model',
    'yt-playability-error-supported-renderers',
    '[id="enforcement-message"]',
    'yt-player-error-message-renderer',
];

function dismissEnforcement() {
    for (const sel of ENFORCEMENT_SELS) {
        const modal = document.querySelector(sel);
        if (!modal) continue;
        const btn = modal.querySelector('yt-button-shape button, button');
        if (btn) { try { btn.click(); } catch {} }
        else {
            const wrap = modal.closest('tp-yt-paper-dialog, ytd-popup-container');
            if (wrap) wrap.remove(); else modal.remove();
        }
    }

    // Patch yt config flags
    try {
        const popup = window.yt?.config_?.openPopupConfig?.supportedPopups;
        if (popup?.adBlockMessageViewModel !== undefined) popup.adBlockMessageViewModel = false;
    } catch {}
    try {
        const d = window.ytcfg?.data_?.PLAYER_VARS;
        if (d) d.ad3_module = '0';
    } catch {}
}

// ═══════════════════════════════════════════════════════════════════════════
// OBSERVER — targeted, filtered, low CPU
// ═══════════════════════════════════════════════════════════════════════════
let _adObserver   = null;
let _bodyObserver = null;

function setupObservers() {
    if (_adObserver)   { _adObserver.disconnect();   _adObserver   = null; }
    if (_bodyObserver) { _bodyObserver.disconnect(); _bodyObserver = null; }

    // Wait for player
    let tries = 0;
    const wait = setInterval(() => {
        const player = getPlayer();
        if (!player && ++tries < 40) return;
        clearInterval(wait);
        if (!player) return;

        // Player observer — only watch class changes (ad-showing is a class toggle)
        _adObserver = new MutationObserver(mutations => {
            if (!S.adEnabled) return;
            for (const m of mutations) {
                if (m.type === 'attributes' || m.addedNodes.length) {
                    trySkipAd();
                    return;
                }
            }
        });
        _adObserver.observe(player, {
            childList:   true,
            subtree:     true,
            attributes:  true,
            attributeFilter: ['class', 'style'],
        });

        // Body observer — only watch for enforcement modal injection
        _bodyObserver = new MutationObserver(mutations => {
            for (const m of mutations) {
                for (const node of m.addedNodes) {
                    if (node.nodeType !== 1) continue;
                    if (
                        node.tagName?.includes('ENFORCEMENT') ||
                        node.id?.includes('enforcement') ||
                        node.querySelector?.('ytd-enforcement-message-view-model')
                    ) {
                        setTimeout(dismissEnforcement, 80);
                        return;
                    }
                }
            }
        });
        _bodyObserver.observe(document.body, { childList: true, subtree: false });
    }, 300);
}

// ═══════════════════════════════════════════════════════════════════════════
// FALLBACK — slow interval, only fires when needed
// ═══════════════════════════════════════════════════════════════════════════
setInterval(() => {
    if (S.adEnabled) _doSkip();
    dismissEnforcement();
}, 800);

setInterval(dismissEnforcement, 2000);

// ═══════════════════════════════════════════════════════════════════════════
// SPONSORBLOCK
// ═══════════════════════════════════════════════════════════════════════════
async function sha256Prefix(str) {
    try {
        const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(str));
        return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2,'0')).join('').slice(0, 4);
    } catch { return null; }
}

function fetchSB(videoId) {
    if (!S.sbEnabled || !videoId || videoId === S.sbVideoId) return;
    S.sbVideoId  = videoId;
    S.sbSegments = [];

    const cats  = CFG.sbCats.map(c => `&category=${c}`).join('');
    const types = CFG.sbTypes.map(t => `&actionType=${t}`).join('');

    function parseSegments(text) {
        try {
            const data = JSON.parse(text);
            let segs = [];
            if (Array.isArray(data) && data[0]?.segments) {
                const match = data.find(d => d.videoID === videoId);
                segs = match?.segments || [];
            } else if (Array.isArray(data) && data[0]?.segment) {
                segs = data;
            }
            S.sbSegments = segs.map(s => ({
                start: s.segment[0], end: s.segment[1],
                cat: s.category, actionType: s.actionType || 'skip', uuid: s.UUID,
            }));
        } catch { S.sbSegments = []; }
    }

    sha256Prefix(videoId).then(prefix => {
        const hashUrl   = prefix ? `https://sponsor.ajay.app/api/skipSegments/${prefix}?${cats.slice(1)}${types}` : null;
        const directUrl = `${CFG.sbApi}?videoID=${videoId}${cats}${types}`;

        GM_xmlhttpRequest({
            method: 'GET', url: hashUrl || directUrl,
            onload(r) {
                if (r.status === 200) { parseSegments(r.responseText); return; }
                if (hashUrl) GM_xmlhttpRequest({
                    method: 'GET', url: directUrl,
                    onload(r2) { if (r2.status === 200) parseSegments(r2.responseText); },
                    onerror() {},
                });
            },
            onerror() {
                if (hashUrl) GM_xmlhttpRequest({
                    method: 'GET', url: directUrl,
                    onload(r2) { if (r2.status === 200) parseSegments(r2.responseText); },
                    onerror() {},
                });
            },
        });
    });
}

// SponsorBlock check — runs on rAF, not interval
let _sbLast = 0;
function checkSB(ts) {
    if (ts - _sbLast >= 400) {
        _sbLast = ts;
        _doCheckSB();
    }
    requestAnimationFrame(checkSB);
}

function _doCheckSB() {
    if (!S.sbEnabled || !S.sbSegments.length) return;
    const video = getVideo();
    if (!video || video.paused) return;
    const t = video.currentTime;

    for (const seg of S.sbSegments) {
        const inSeg = t >= seg.start && t < seg.end - 0.1;
        if (seg.actionType === 'mute') {
            if (inSeg) {
                if (S.sbMutedSeg !== seg.uuid) {
                    S.sbMutedSeg = seg.uuid;
                    video.muted  = true;
                    S.sbCount++; uiSync();
                    toast(`🔇 SB: [${seg.cat}]`, '#7b1fa2');
                }
            } else if (S.sbMutedSeg === seg.uuid) {
                video.muted  = false;
                S.sbMutedSeg = null;
            }
        } else if (inSeg) {
            video.currentTime = seg.end;
            S.sbCount++; uiSync();
            toast(`⏭ SB: [${seg.cat}]`, '#1a73e8');
            break;
        }
    }
}

// ═══════════════════════════════════════════════════════════════════════════
// AUTO QUALITY
// ═══════════════════════════════════════════════════════════════════════════
const QUALITY_LABELS = {
    highres:'8K', hd2160:'4K 2160p', hd1440:'1440p',
    hd1080:'1080p HD', hd720:'720p HD', large:'480p', medium:'360p', small:'240p',
};

function setQuality() {
    if (!S.qualityEnabled) return;
    try {
        const player = getPlayer();
        if (!player?.getAvailableQualityLevels) return;
        const levels = player.getAvailableQualityLevels();
        if (!levels?.length) return;
        const best = levels[0];
        if (player.getPlaybackQuality() !== best) {
            player.setPlaybackQualityRange(best, best);
            player.setPlaybackQuality(best);
            toast(`✨ Quality: ${QUALITY_LABELS[best] || best}`, '#e53935');
        }
    } catch {}
}

// ═══════════════════════════════════════════════════════════════════════════
// DOWNLOAD — evdfrance.fr (fast, no Cobalt timeout lag)
// ═══════════════════════════════════════════════════════════════════════════
function downloadVideo(videoId) {
    if (!videoId) return;
    toast('⬇ Opening download…', '#1565c0');
    window.open(`https://evdfrance.fr/convert/?id=${videoId}`, '_blank');
}

// ═══════════════════════════════════════════════════════════════════════════
// NAVIGATION
// ═══════════════════════════════════════════════════════════════════════════
function getVideoId(url = location.href) {
    return (
        url.match(/[?&]v=([^&#]{11})/)?.[1] ||
        url.match(/shorts\/([^?&#]{11})/)?.[1] ||
        url.match(/youtu\.be\/([^?&#]{11})/)?.[1] ||
        null
    );
}

function getTitle() {
    const sel = [
        'ytd-watch-metadata h1 yt-formatted-string',
        '#title h1 yt-formatted-string',
        'h2 span.yt-core-attributed-string[role="text"]',
        '.title.ytd-video-primary-info-renderer',
    ];
    for (const s of sel) {
        const t = document.querySelector(s)?.textContent?.trim();
        if (t?.length > 1) return t;
    }
    return document.title?.replace(/\s*[-|]\s*YouTube\s*$/i, '').trim() || 'EasyTube';
}

let _navTimer = null;
function onNavigate() {
    clearTimeout(_navTimer);
    _navTimer = setTimeout(() => {
        S._player = null;
        S._video  = null;

        const vid = getVideoId();
        S.videoId = vid;

        if (vid) {
            if (vid !== S.sbVideoId) fetchSB(vid);
            if (S.qualityEnabled) {
                [1500, 3000, 5000].forEach(d => setTimeout(setQuality, d));
            }
        }

        setupObservers();
        uiSync();
    }, 600);
}

// ═══════════════════════════════════════════════════════════════════════════
// UI
// ═══════════════════════════════════════════════════════════════════════════
let _toastTimer = null;
function toast(msg, color = '#333') {
    let el = document.getElementById('et4_toast');
    if (!el) {
        el = document.createElement('div');
        el.id = 'et4_toast';
        Object.assign(el.style, {
            position:'fixed', bottom:'80px', left:'50%',
            transform:'translateX(-50%)',
            color:'#fff', padding:'7px 20px', borderRadius:'999px',
            fontSize:'13px', fontWeight:'700',
            zIndex:'2147483647', pointerEvents:'none',
            fontFamily:'system-ui,sans-serif',
            transition:'opacity .3s, transform .3s',
        });
        document.body.appendChild(el);
    }
    el.textContent = msg;
    el.style.background   = color;
    el.style.opacity      = '1';
    el.style.transform    = 'translateX(-50%) translateY(0)';
    clearTimeout(_toastTimer);
    _toastTimer = setTimeout(() => {
        el.style.opacity   = '0';
        el.style.transform = 'translateX(-50%) translateY(-8px)';
    }, 2500);
}

let _uiRaf = false;
function uiSync() {
    if (_uiRaf) return;
    _uiRaf = true;
    requestAnimationFrame(() => {
        _uiRaf = false;
        document.getElementById('et4_ad_n')?.replaceChildren(document.createTextNode(S.adCount));
        document.getElementById('et4_sb_n')?.replaceChildren(document.createTextNode(S.sbCount));
        syncToggle('et4_sw_ad', S.adEnabled);
        syncToggle('et4_sw_sb', S.sbEnabled);
        syncToggle('et4_sw_q',  S.qualityEnabled);
        const vid     = getVideoId();
        const titleEl = document.getElementById('et4_title');
        if (titleEl) titleEl.textContent = vid ? getTitle() : 'Open a video to start';
        const idEl = document.getElementById('et4_vid_id');
        if (idEl) idEl.textContent = vid || 'N/A';
        // Update download button state
        const dlBtn = document.getElementById('et4_dl_btn');
        if (dlBtn) dlBtn.style.opacity = vid ? '1' : '0.5';
    });
}

function syncToggle(id, state) {
    const sw = document.getElementById(id);
    if (!sw) return;
    sw.classList.toggle('on', !!state);
    const st = document.getElementById(id + '_st');
    if (st) st.textContent = state ? 'ON' : 'OFF';
}

// ═══════════════════════════════════════════════════════════════════════════
// PANEL BUILD — Trusted Types compliant (no innerHTML)
// ═══════════════════════════════════════════════════════════════════════════

// Create an element with optional class, text content, and attributes
function mk(tag, cls, text, attrs) {
    const el = document.createElement(tag);
    if (cls)  el.className = cls;
    if (text != null) el.appendChild(document.createTextNode(text));
    if (attrs) {
        for (const [k, v] of Object.entries(attrs)) {
            if (k === 'id') el.id = v;
            else el.setAttribute(k, v);
        }
    }
    return el;
}

// Build one toggle card (icon + switch + label + state text)
function makeToggleCard(icon, swId, name, stId) {
    const tc  = mk('div', 'e4-tc');
    const top = mk('div', 'e4-tc-top');
    top.appendChild(mk('span', 'e4-tc-ico', icon));
    const btn = mk('button', 'e4-sw', null, { id: swId, type: 'button' });
    btn.appendChild(mk('span', 'e4-thumb'));
    top.appendChild(btn);
    const bot = mk('div', 'e4-tc-bot');
    bot.appendChild(mk('span', 'e4-tc-name', name));
    bot.appendChild(mk('span', 'e4-tc-st', 'OFF', { id: stId }));
    tc.appendChild(top);
    tc.appendChild(bot);
    return tc;
}

function buildPanel() {
    // ── Toggle button ──────────────────────────────────────────────────────
    const tog = mk('div', null, null, { id: 'et4_tog' });
    const svgNS = 'http://www.w3.org/2000/svg';
    const svg = document.createElementNS(svgNS, 'svg');
    svg.setAttribute('viewBox', '0 0 24 24');
    svg.setAttribute('width', '22');
    svg.setAttribute('height', '22');
    const path1 = document.createElementNS(svgNS, 'path');
    path1.setAttribute('d', 'M23.5 6.3a3.1 3.1 0 0 0-2.2-2.2C19.4 3.5 12 3.5 12 3.5s-7.4 0-9.3.6A3.1 3.1 0 0 0 .5 6.3 32.7 32.7 0 0 0 0 12a32.7 32.7 0 0 0 .5 5.7 3.1 3.1 0 0 0 2.2 2.2c1.9.6 9.3.6 9.3.6s7.4 0 9.3-.6a3.1 3.1 0 0 0 2.2-2.2A32.7 32.7 0 0 0 24 12a32.7 32.7 0 0 0-.5-5.7Z');
    path1.setAttribute('fill', '#FF0000');
    const path2 = document.createElementNS(svgNS, 'path');
    path2.setAttribute('d', 'M9.75 15.5V8.5L16 12l-6.25 3.5Z');
    path2.setAttribute('fill', '#FFF');
    svg.appendChild(path1);
    svg.appendChild(path2);
    tog.appendChild(svg);
    document.body.appendChild(tog);

    // ── Panel ──────────────────────────────────────────────────────────────
    const panel = mk('div', null, null, { id: 'et4_panel' });

    // Header
    const hdr  = mk('div', 'e4-hdr', null, { id: 'et4_drag' });
    const hdrL = mk('div', 'e4-hdr-l');
    hdrL.appendChild(mk('div', 'e4-logo', '▶'));
    const hdrTxt = mk('div');
    hdrTxt.appendChild(mk('div', 'e4-hdr-title', 'EasyTube V4.0'));
    hdrTxt.appendChild(mk('div', 'e4-hdr-sub', 'Ad Skip · SponsorBlock · 4K · HD Download'));
    hdrL.appendChild(hdrTxt);
    hdr.appendChild(hdrL);
    hdr.appendChild(mk('div', 'e4-drag-dot', '⋮'));
    panel.appendChild(hdr);

    // Stats bar
    const stats = mk('div', 'e4-stats');
    const pillAd = mk('div', 'e4-pill', '🚫 Ads: ');
    pillAd.appendChild(mk('span', null, '0', { id: 'et4_ad_n' }));
    const pillSb = mk('div', 'e4-pill', '⏭ Sponsors: ');
    pillSb.appendChild(mk('span', null, '0', { id: 'et4_sb_n' }));
    stats.appendChild(pillAd);
    stats.appendChild(pillSb);
    stats.appendChild(mk('div', 'e4-pill e4-ver', `v${CFG.version}`));
    panel.appendChild(stats);

    // Body
    const body = mk('div', 'e4-body');

    // Video card
    const card = mk('div', 'e4-card');
    const cardRow = mk('div', 'e4-card-row');
    cardRow.appendChild(mk('div', 'e4-card-label', 'NOW PLAYING'));
    const badge = mk('span', 'e4-badge', `● v${CFG.version}`);
    cardRow.appendChild(badge);
    card.appendChild(cardRow);
    card.appendChild(mk('div', 'e4-card-title', 'Open a video to start', { id: 'et4_title' }));
    const cardId = mk('div', 'e4-card-id', 'ID: ');
    cardId.appendChild(mk('code', null, 'N/A', { id: 'et4_vid_id' }));
    card.appendChild(cardId);
    body.appendChild(card);

    // ── Download button ────────────────────────────────────────────────────
    const dlBtn = mk('button', 'e4-btn e4-btn-red', null, { id: 'et4_dl_btn', type: 'button' });
    dlBtn.appendChild(mk('span', 'e4-btn-ico', '⬇'));
    dlBtn.appendChild(mk('span', null, 'Download Video (HD/4K)'));
    body.appendChild(dlBtn);

    // Toggle grid
    const grid = mk('div', 'e4-grid');
    grid.appendChild(makeToggleCard('🚫', 'et4_sw_ad', 'Ad Skip',      'et4_sw_ad_st'));
    grid.appendChild(makeToggleCard('⏭',  'et4_sw_sb', 'SponsorBlock', 'et4_sw_sb_st'));
    grid.appendChild(makeToggleCard('✨', 'et4_sw_q',  'Auto 4K',      'et4_sw_q_st'));
    body.appendChild(grid);

    // Info box
    const info = mk('div', 'e4-info-box');
    info.appendChild(mk('span', 'e4-info-icon', '⚡'));
    info.appendChild(mk('span', null, 'V4.0: Rewritten for performance — lower CPU, faster skip, no Cobalt timeout lag.'));
    body.appendChild(info);

    panel.appendChild(body);
    panel.appendChild(mk('div', 'e4-foot', `EasyTube V4.0 · 2pixel · 2025`));

    document.body.appendChild(panel);
    return { panel, tog };
}

// ═══════════════════════════════════════════════════════════════════════════
// DRAG — pointer events, RAF-throttled
// ═══════════════════════════════════════════════════════════════════════════
function initDrag(panel) {
    const hdr = panel.querySelector('#et4_drag');
    const vw = window.innerWidth, vh = window.innerHeight;
    let ox = vw - 375, oy = vh - 580;
    ox = Math.max(8, ox); oy = Math.max(8, oy);
    panel.style.transform = `translate3d(${ox}px,${oy}px,0)`;

    let dragging = false, pid = null, ix = 0, iy = 0, raf = false;
    const pw = 360, ph = 560;

    hdr.addEventListener('pointerdown', e => {
        dragging = true; pid = e.pointerId;
        ix = e.clientX - ox; iy = e.clientY - oy;
        panel.classList.add('e4-drag');
        try { hdr.setPointerCapture(pid); } catch {}
    }, { passive: true });

    hdr.addEventListener('pointermove', e => {
        if (!dragging || e.pointerId !== pid || raf) return;
        raf = true;
        requestAnimationFrame(() => {
            ox = Math.max(8, Math.min(window.innerWidth  - pw - 8, e.clientX - ix));
            oy = Math.max(8, Math.min(window.innerHeight - ph - 8, e.clientY - iy));
            panel.style.transform = `translate3d(${ox}px,${oy}px,0)`;
            raf = false;
        });
    }, { passive: true });

    const endDrag = e => {
        if (e.pointerId !== pid) return;
        dragging = false;
        panel.classList.remove('e4-drag');
    };
    hdr.addEventListener('pointerup',     endDrag, { passive: true });
    hdr.addEventListener('pointercancel', endDrag, { passive: true });
}

// ═══════════════════════════════════════════════════════════════════════════
// EVENTS
// ═══════════════════════════════════════════════════════════════════════════
function bindEvents(panel, tog) {
    let visible = false;
    tog.addEventListener('click', () => {
        visible = !visible;
        panel.classList.toggle('e4-show', visible);
        tog.classList.toggle('e4-active', visible);
        if (visible) uiSync();
    });

    // Download button
    document.getElementById('et4_dl_btn').addEventListener('click', () => {
        const vid = getVideoId();
        if (vid) downloadVideo(vid);
        else toast('⚠ Open a video first', '#f57c00');
    });

    document.getElementById('et4_sw_ad').addEventListener('click', () => {
        S.adEnabled = !S.adEnabled;
        save('et4_ad', S.adEnabled);
        if (!S.adEnabled) {
            const v = getVideo();
            if (v) { v.playbackRate = 1; v.muted = false; }
            S.adSpeedActive = false;
        }
        uiSync();
        toast(S.adEnabled ? '🚫 Ad Skip ON' : '🚫 Ad Skip OFF', S.adEnabled ? '#2e7d32' : '#b71c1c');
    });

    document.getElementById('et4_sw_sb').addEventListener('click', () => {
        S.sbEnabled = !S.sbEnabled;
        save('et4_sb', S.sbEnabled);
        if (S.sbEnabled) { S.sbVideoId = null; fetchSB(getVideoId()); }
        uiSync();
        toast(S.sbEnabled ? '⏭ SponsorBlock ON' : '⏭ SponsorBlock OFF', S.sbEnabled ? '#1565c0' : '#4a148c');
    });

    document.getElementById('et4_sw_q').addEventListener('click', () => {
        S.qualityEnabled = !S.qualityEnabled;
        save('et4_quality', S.qualityEnabled);
        if (S.qualityEnabled) setQuality();
        uiSync();
        toast(S.qualityEnabled ? '✨ Auto 4K ON' : '✨ Auto 4K OFF', S.qualityEnabled ? '#e53935' : '#616161');
    });
}

// ═══════════════════════════════════════════════════════════════════════════
// STYLES
// ═══════════════════════════════════════════════════════════════════════════
GM_addStyle(`
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@600;700;800;900&display=swap');

#et4_panel, #et4_panel * { box-sizing:border-box; font-family:'Nunito',system-ui,sans-serif; }

/* ── Toggle button ─────────────────────────────── */
#et4_tog {
    position:fixed; bottom:90px; right:18px;
    width:54px; height:36px; border-radius:999px;
    background:rgba(255,255,255,0.15);
    border:1px solid rgba(255,255,255,0.25);
    box-shadow:0 6px 20px rgba(0,0,0,.22);
    z-index:2147483646; cursor:pointer;
    display:flex; align-items:center; justify-content:center;
    backdrop-filter:blur(16px);
    transition:transform .18s, box-shadow .18s;
}
#et4_tog:hover { transform:translateY(-2px); box-shadow:0 10px 28px rgba(0,0,0,.3); }
#et4_tog.e4-active svg { transform:rotate(180deg); }
#et4_tog svg { transition:transform .3s; }

/* ── Panel ─────────────────────────────────────── */
#et4_panel {
    position:fixed; top:0; left:0;
    width:355px; max-width:94vw;
    display:flex; flex-direction:column;
    background:rgba(255,255,255,0.12);
    backdrop-filter:blur(32px) saturate(180%);
    -webkit-backdrop-filter:blur(32px) saturate(180%);
    border:1px solid rgba(255,255,255,0.18);
    border-radius:24px;
    box-shadow:0 18px 50px rgba(0,0,0,.30);
    z-index:2147483647; overflow:hidden;
    opacity:0; pointer-events:none;
    transform:scale(.96) translateY(12px);
    transition:opacity .28s, transform .3s cubic-bezier(.25,.46,.45,.94);
    will-change:transform, opacity;
}
#et4_panel.e4-show  { opacity:1; pointer-events:all; transform:scale(1) translateY(0); }
#et4_panel.e4-drag  { transition:none !important; }

/* ── Header ────────────────────────────────────── */
.e4-hdr {
    background:linear-gradient(135deg,#ff1a1a,#b80000);
    padding:12px 14px; cursor:move; user-select:none;
    display:flex; align-items:center; justify-content:space-between;
    border-radius:24px 24px 0 0;
}
.e4-hdr-l    { display:flex; align-items:center; gap:10px; }
.e4-logo     { width:46px; height:32px; background:rgba(255,255,255,.18); border-radius:9px; display:flex; align-items:center; justify-content:center; font-size:20px; color:#fff; font-weight:900; }
.e4-hdr-title { color:#fff; font-size:15px; font-weight:800; line-height:1.2; }
.e4-hdr-sub  { color:rgba(255,255,255,.8); font-size:10px; font-weight:600; }
.e4-drag-dot { color:rgba(255,255,255,.85); font-size:22px; }

/* ── Stats bar ─────────────────────────────────── */
.e4-stats {
    display:flex; gap:5px; padding:7px 12px;
    background:rgba(0,0,0,.06); border-bottom:1px solid rgba(255,255,255,.08);
}
.e4-pill {
    flex:1; background:rgba(255,255,255,.22); border:1px solid rgba(255,255,255,.18);
    border-radius:999px; padding:4px 8px;
    font-size:11px; font-weight:800; color:#0f0f0f;
    display:flex; align-items:center; justify-content:center; gap:3px;
}
.e4-pill span { font-weight:900; font-size:13px; }
.e4-ver { font-size:10px; color:#555; flex:0 0 auto; padding:4px 10px; }

/* ── Body ──────────────────────────────────────── */
.e4-body {
    padding:11px 13px 13px; overflow-y:auto; flex:1 1 auto;
    scrollbar-width:thin; scrollbar-color:rgba(255,255,255,.3) transparent;
    display:flex; flex-direction:column; gap:9px;
}
.e4-body::-webkit-scrollbar { width:6px; }
.e4-body::-webkit-scrollbar-thumb { background:rgba(255,255,255,.25); border-radius:999px; }

/* ── Video card ────────────────────────────────── */
.e4-card {
    background:rgba(255,255,255,.24); border:1px solid rgba(255,255,255,.2);
    border-radius:18px; padding:11px;
}
.e4-card-row   { display:flex; align-items:center; justify-content:space-between; margin-bottom:6px; }
.e4-card-label { font-size:10px; font-weight:800; color:#606060; text-transform:uppercase; letter-spacing:.6px; }
.e4-badge      { font-size:10.5px; font-weight:700; color:#00a152; background:rgba(232,245,233,.9); padding:3px 9px; border-radius:999px; }
.e4-card-title { font-size:13.5px; font-weight:700; color:#0f0f0f; line-height:1.4; margin-bottom:6px; word-break:break-word; }
.e4-card-id    { font-size:11px; font-weight:600; color:#555; }
.e4-card-id code { background:rgba(255,255,255,.65); padding:2px 8px; border-radius:999px; margin-left:4px; }

/* ── Download button ───────────────────────────── */
.e4-btn {
    display:flex; align-items:center; justify-content:center; gap:9px;
    width:100%; padding:12px 16px; border:none; border-radius:18px;
    font-size:14px; font-weight:800; cursor:pointer; color:#fff;
    text-decoration:none; position:relative; overflow:hidden; letter-spacing:.2px;
    font-family:'Nunito',system-ui,sans-serif;
    transition:transform .2s, box-shadow .2s, opacity .2s;
}
.e4-btn:hover { transform:translateY(-2px); }
.e4-btn:active { transform:scale(.98); }
.e4-btn-red { background:linear-gradient(135deg,#ff1a1a,#cc0000); box-shadow:0 4px 14px rgba(255,0,0,.28); }
.e4-btn-ico { font-size:18px; }

/* ── Toggle grid ───────────────────────────────── */
.e4-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:7px; }
.e4-tc {
    background:rgba(255,255,255,.22); border:1px solid rgba(255,255,255,.18);
    border-radius:15px; padding:10px 9px;
    display:flex; flex-direction:column; gap:7px;
    transition:transform .15s;
}
.e4-tc:hover   { transform:translateY(-1px); }
.e4-tc-top     { display:flex; align-items:center; justify-content:space-between; }
.e4-tc-ico     { font-size:19px; line-height:1; }
.e4-tc-bot     { display:flex; align-items:center; justify-content:space-between; gap:3px; }
.e4-tc-name    { font-size:11px; font-weight:800; color:#0f0f0f; }
.e4-tc-st      { font-size:10px; font-weight:700; color:rgba(15,15,15,.4); }

/* ── Switch ────────────────────────────────────── */
.e4-sw {
    width:40px; height:24px; border-radius:999px; border:none;
    background:rgba(120,120,128,.28); position:relative; cursor:pointer;
    transition:background .18s; flex:0 0 auto;
}
.e4-sw.on     { background:rgba(52,199,89,.95); }
.e4-thumb {
    position:absolute; top:2px; left:2px; width:20px; height:20px;
    border-radius:999px; background:#fff;
    box-shadow:0 3px 8px rgba(0,0,0,.18);
    transition:transform .18s;
}
.e4-sw.on .e4-thumb { transform:translateX(16px); }

/* ── Info box ──────────────────────────────────── */
.e4-info-box {
    background:rgba(6,95,212,.12); border:1px solid rgba(6,95,212,.2);
    border-radius:14px; padding:9px 12px;
    font-size:11.5px; font-weight:600; color:#0f0f0f;
    display:flex; align-items:flex-start; gap:8px; line-height:1.5;
}
.e4-info-icon { font-size:16px; flex:0 0 auto; margin-top:1px; }

/* ── Footer ────────────────────────────────────── */
.e4-foot {
    padding:8px 13px; background:rgba(255,255,255,.1);
    border-top:1px solid rgba(255,255,255,.1);
    border-radius:0 0 24px 24px; text-align:center;
    font-size:10.5px; color:#606060; font-weight:600;
}
`);

// ═══════════════════════════════════════════════════════════════════════════
// BOOT
// ═══════════════════════════════════════════════════════════════════════════
function boot() {
    const { panel, tog } = buildPanel();
    initDrag(panel);
    bindEvents(panel, tog);

    uiSync();
    setupObservers();

    // Start SponsorBlock RAF loop
    requestAnimationFrame(checkSB);

    // Periodic UI sync
    setInterval(uiSync, 2500);

    // Navigation detection
    const navObs = new MutationObserver(() => {
        if (location.href !== S.lastUrl) {
            S.lastUrl = location.href;
            S.sbVideoId  = null;
            S.sbSegments = [];
            onNavigate();
        }
    });
    navObs.observe(document.body, { childList: true, subtree: false });

    window.addEventListener('yt-navigate-finish',   () => onNavigate(), { passive: true });
    window.addEventListener('yt-page-data-updated', () => onNavigate(), { passive: true });
    window.addEventListener('yt-navigate-start', () => {
        S.sbVideoId  = null;
        S.sbSegments = [];
        S._player    = null;
        S._video     = null;
    }, { passive: true });

    onNavigate();
}

if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', boot);
} else {
    boot();
}