自动跳过抖音直播间

自动跳过抖音直播间,且可以输入关键词进行指定跳过

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         自动跳过抖音直播间
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  自动跳过抖音直播间,且可以输入关键词进行指定跳过
// @author       洛洛罗
// @match        https://www.douyin.com/?recommend=1
// @icon         https://www.google.com/s2/favicons?sz=64&domain=douyin.com
// @grant        none
// @license      MIT
// ==/UserScript==
(function() {
    'use strict';

    // --- 配置常量 ---
    const PLUGIN_ID = "quicker_tiktok_sidebar_plugin";
    const TIMER_KEY = "quicker_tiktok_timer";
    const STORAGE_KEY = "quicker_tiktok_block_keywords";
    const DY_RED = "#FE2C55"; // 抖音红
    const ICON_PATH_ON = "M4 5C4 4.44772 4.44772 4 5 4H19C19.5523 4 20 4.44772 20 5V7.12683C20 7.42765 19.8643 7.71285 19.6309 7.89808L14 12.3667V17.7081C14 18.2323 13.6019 18.6713 13.085 18.7497L13 18.7626L10.5 19.7241C10.0827 19.8846 9.62678 19.5768 9.62678 19.1296V12.4419L4.47167 7.93117C4.17329 7.67009 4 7.29381 4 6.89668V5Z";

    // 防止重复注入
    if (document.getElementById(PLUGIN_ID)) return;

    // --- 全局变量 ---
    let isCoolingDown = false;
    let blockList = [];
    let isRunning = false;

    // --- 数据持久化 ---
    function loadConfig() {
        var saved = localStorage.getItem(STORAGE_KEY);
        if (saved) { try { blockList = JSON.parse(saved); } catch (e) { blockList = []; } }
    }
    function saveConfig(str) {
        var arr = str.replace(/,/g, ",").split(",");
        var finalArr = [];
        for(var i=0; i<arr.length; i++) {
            var s = arr[i].trim();
            if(s.length > 0) finalArr.push(s);
        }
        blockList = finalArr;
        localStorage.setItem(STORAGE_KEY, JSON.stringify(blockList));
    }

    // --- 核心工具:获取当前正在播放的卡片 ---
    function getActiveSlide() {
        var el = document.querySelector('[data-e2e="feed-active-video"]');
        if (el) return el;
        var videos = document.querySelectorAll('video');
        for (var i = 0; i < videos.length; i++) {
            var v = videos[i];
            if (!v.paused && v.style.display !== 'none' && v.readyState > 2) {
                return v.closest('[data-e2e="feed-item"]') || v.closest('.swiper-slide') || v.parentElement.parentElement;
            }
        }
        return null;
    }

    // --- 模拟按键跳过 ---
    function triggerSkip(reason) {
        console.log(`[自动净化] 跳过原因: ${reason}`);
        var event = new KeyboardEvent('keydown', { bubbles: true, cancelable: true, key: 'ArrowDown', code: 'ArrowDown', keyCode: 40, which: 40 });
        document.body.dispatchEvent(event);
    }

    // --- 核心判定逻辑 ---
    function skipLogic() {
        if (isCoolingDown) return;
        try {
            var activeSlide = getActiveSlide();
            if (!activeSlide) return;

            var shouldSkip = false;
            
            // 1. 属性检测 (最准)
            if (activeSlide.querySelector('[data-e2e="feed-live"]')) shouldSkip = true;

            // 2. 标签文字检测
            if (!shouldSkip) {
                var tagContent = activeSlide.querySelector('.semi-tag-content');
                if (tagContent && tagContent.innerText.indexOf("直播中") !== -1) shouldSkip = true;
            }

            // 3. 通用文本检测
            if (!shouldSkip) {
                var text = activeSlide.innerText;
                if (text.indexOf("进入直播间") !== -1 || text.indexOf("直播加载中") !== -1) shouldSkip = true;
            }

            // 4. 屏蔽词检测
            if (!shouldSkip && blockList.length > 0) {
                var fullText = activeSlide.innerText;
                for (var i = 0; i < blockList.length; i++) {
                    if (fullText.indexOf(blockList[i]) !== -1) { shouldSkip = true; break; }
                }
            }

            if (shouldSkip) {
                triggerSkip("命中规则");
                isCoolingDown = true;
                updateIconState(true);
                setTimeout(() => {
                    isCoolingDown = false;
                    updateIconState(false);
                }, 1500);
            }
        } catch (e) { console.error("SkipLogic Error:", e); }
    }

    // --- UI 构建:克隆原生按钮 (解决错位问题的核心) ---
    function initUI() {
        var checkTimer = setInterval(function() {
            // 寻找一个参照物,比如“短剧”、“放映厅”或者“首页”
            // 我们需要找到它最外层的容器,通常是 li 或者 div
            var anchors = document.querySelectorAll('a[href*="/"], div[role="button"]');
            var refElement = null;
            
            for (let el of anchors) {
                if (el.innerText.includes("短剧") || el.innerText.includes("放映厅") || el.innerText.includes("直播")) {
                     // 向上找,找到侧边栏列表的直接子元素 (通常有 class 控制布局)
                     // 结构通常是: ul > li > a > ...
                     // 或者是 div.sidebar > div.item > a > ...
                     refElement = el.parentElement; 
                     // 简单验证:确保这个父元素不是 body,且它的父级是侧边栏容器
                     if (refElement && refElement.parentElement && refElement.parentElement.childElementCount > 3) {
                         break;
                     }
                }
            }

            if (refElement) {
                clearInterval(checkTimer);
                injectSidebarBtn(refElement);
            }
        }, 1000);
    }

    function injectSidebarBtn(refItem) {
        // 1. 克隆原生节点
        var cloneItem = refItem.cloneNode(true);
        cloneItem.id = PLUGIN_ID;
        
        // 2. 获取内部的链接元素 (a标签 或 role=button 的 div)
        var linkEl = cloneItem.querySelector('a') || cloneItem.querySelector('[role="button"]');
        if (!linkEl) linkEl = cloneItem.querySelector('div'); // 兜底

        // 3. 清除原有的跳转属性和事件
        if (linkEl.tagName === 'A') {
            linkEl.removeAttribute('href');
            linkEl.removeAttribute('target');
        }

        // 4. 替换图标 (SVG)
        var svgEl = cloneItem.querySelector('svg');
        if (svgEl) {
            // 保留 SVG 的 class,以维持大小和颜色样式,只替换 path
            svgEl.id = "qt_icon_svg"; // 标记方便后续改变颜色
            svgEl.innerHTML = `<path d="${ICON_PATH_ON}" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path>`;
        }

        // 5. 替换文字
        var spanEl = cloneItem.querySelector('span');
        if (spanEl) {
            spanEl.id = "qt_text_span";
            spanEl.innerText = "自动净化";
        }

        // 6. 绑定点击事件
        // 使用 capture 阶段或覆盖 onclick 防止原生 React 事件干扰
        linkEl.onclick = function(e) {
            e.preventDefault();
            e.stopPropagation();
            toggleState();
        };

        // 7. 绑定右键设置
        linkEl.oncontextmenu = function(e) {
            e.preventDefault();
            e.stopPropagation();
            showSettings();
        };

        // 8. 插入到侧边栏 (插入到参照物的后面)
        if (refItem.nextSibling) {
            refItem.parentElement.insertBefore(cloneItem, refItem.nextSibling);
        } else {
            refItem.parentElement.appendChild(cloneItem);
        }

        createSettingsPanel();
        loadConfig();
        toggleState(); // 默认开启
    }

    // --- 状态更新 ---
    function updateIconState(isSkippingAction) {
        var svg = document.getElementById("qt_icon_svg");
        var text = document.getElementById("qt_text_span");
        
        // 获取原生文字的默认颜色 (通常通过 computedStyle 或者父级继承)
        // 我们利用 CSS 变量或者直接操作 style
        
        if (isRunning) {
            if (svg) svg.style.color = DY_RED; // 图标变红
            if (text) {
                text.innerText = isSkippingAction ? "跳过中..." : "净化开启";
                // text.style.color = DY_RED; // 文字可选变红,或者保持白色
            }
        } else {
            if (svg) svg.style.color = ""; // 恢复默认颜色 (继承 CSS)
            if (text) {
                text.innerText = "净化关闭";
                // text.style.color = "";
            }
        }
    }

    function toggleState() {
        if (isRunning) {
            clearInterval(window[TIMER_KEY]);
            window[TIMER_KEY] = null;
            isRunning = false;
        } else {
            skipLogic();
            window[TIMER_KEY] = setInterval(skipLogic, 800);
            isRunning = true;
        }
        updateIconState(false);
    }

    // --- 设置面板 (无需改动) ---
    function createSettingsPanel() {
        if (document.getElementById("qt_settings_panel")) return;
        var panel = document.createElement("div");
        panel.id = "qt_settings_panel";
        panel.style.cssText = "display:none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 320px; background: #161823; border: 1px solid rgba(255,255,255,0.1); border-radius: 12px; padding: 20px; z-index: 999999; box-shadow: 0 10px 30px rgba(0,0,0,0.5); color: #fff; font-family: PingFang SC, sans-serif;";
        panel.innerHTML =
            '<h3 style="margin:0 0 15px 0; font-size:16px; color:#FE2C55;">屏蔽词设置</h3>' +
            '<p style="font-size:12px; color:rgba(255,255,255,0.5); margin-bottom:10px;">输入关键词,用逗号分隔:</p>' +
            '<textarea id="qt_settings_input" style="width:100%; height:100px; background:rgba(255,255,255,0.05); border:none; color:#eee; border-radius:8px; padding:10px; box-sizing:border-box; resize:none; font-size:12px; outline:none;"></textarea>' +
            '<div style="display:flex; justify-content:flex-end; gap:10px; margin-top:20px;">' +
                '<button id="qt_close_btn" style="padding:6px 16px; border-radius:4px; border:none; cursor:pointer; background:rgba(255,255,255,0.1); color:#fff;">取消</button>' +
                '<button id="qt_save_btn" style="padding:6px 16px; border-radius:4px; border:none; cursor:pointer; background:#FE2C55; color:#fff; font-weight:bold;">保存</button>' +
            '</div>';
        document.body.appendChild(panel);
        document.getElementById("qt_close_btn").onclick = function() { panel.style.display = "none"; };
        document.getElementById("qt_save_btn").onclick = function() {
            saveConfig(document.getElementById("qt_settings_input").value);
            panel.style.display = "none";
            var t = document.getElementById("qt_text_span");
            if(t) t.innerText = "已保存";
            setTimeout(function(){ updateIconState(false); }, 1500);
        };
    }

    function showSettings() {
        var panel = document.getElementById("qt_settings_panel");
        var input = document.getElementById("qt_settings_input");
        if(panel) {
            input.value = blockList.join(", ");
            panel.style.display = "block";
        }
    }

    initUI();
})();