// ==UserScript== // @name 抖音直播净化(+自动抢福袋) // @namespace https://local/douyin-live-lite // @version 1.2.0 // @author 小德捡银子 // @description 直播净化 + 自动抢福袋(可独立开关) // @license GPL-3.0-only // @match *://*.douyin.com/* // @match *://*.iesdouyin.com/* // @exclude *://creator.douyin.com/* // @require https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant unsafeWindow // @run-at document-start // @tag 抖音 // @tag 自动抢免费福袋 // @tag 抖音直播 // ==/UserScript== (function () { "use strict"; /* ========================================================= * 基础工具与配置 * ========================================================= */ const NS = "dylive-lite"; const log = (...args) => console.log(`[${NS}]`, ...args); const _win = (() => (typeof unsafeWindow !== "undefined" ? unsafeWindow : window))(); const DEFAULTS = { // 总开关 enable_all: true, // 悬浮窗位置记忆默认值 btn_pos: { right: 16, bottom: 120 }, // 布局屏蔽-聊天室 block_message_broadcast: true, // 屏蔽信息播报 block_bottom_area: true, // 屏蔽底部遮挡区域 // 功能 quality_select: "", // 清晰度选择 enable_lucky_bag: true, // 抢福袋 // 布局屏蔽-视频区域内 block_gift_effects: true, // 屏蔽礼物特效 block_yellow_car: true, // 屏蔽小黄车 block_exhibition_tooltip: true, // 屏蔽提示 block_danmaku: true, // 屏蔽弹幕 block_gifts_container: true, // 屏蔽礼物栏容器 // 聊天室消息过滤器 filter_enable: true, // 过滤器总开关 filter_block_gift_msg: false, // 屏蔽送礼消息 filter_block_lucky_bag: false, // 屏蔽福袋口令 filter_block_emoji: false, // 屏蔽emoji消息 filter_block_room_message: true, // 屏蔽房间播报 filter_custom_rules: "", // 自定义正则 }; const GM = { get(key) { const v = GM_getValue(`${NS}:${key}`, undefined); return v === undefined ? DEFAULTS[key] : v; }, set(key, value) { GM_setValue(`${NS}:${key}`, value); }, }; function addStyle(id, css) { let $style = document.getElementById(id); if (!$style) { $style = document.createElement("style"); $style.id = id; (document.head || document.documentElement).appendChild($style); } $style.textContent = css || ""; return $style; } function removeStyle(id) { const $style = document.getElementById(id); if ($style) $style.remove(); } function debounce(fn, wait) { let timer = null; return function (...args) { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), wait); }; } function onReady(cb) { if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", cb, { once: true }); } else { cb(); } } /* ========================================================= * 直播页面判断 * ========================================================= */ function isLivePage() { try { const { hostname, pathname } = location; if (hostname === "live.douyin.com") return true; if ( (hostname === "www.douyin.com" || hostname === "douyin.com") && (pathname.startsWith("/follow/live/") || pathname.startsWith("/root/live/")) ) { return true; } } catch (e) {} return false; } function watchRouteChange(callback) { let lastHref = location.href; const check = () => { if (location.href !== lastHref) { lastHref = location.href; callback(); } }; ["pushState", "replaceState"].forEach((method) => { const origin = history[method]; history[method] = function (...args) { const ret = origin.apply(this, args); setTimeout(check, 0); return ret; }; }); window.addEventListener("popstate", () => setTimeout(check, 0)); setInterval(check, 1000); } /* ========================================================= * CSS 布局屏蔽 * ========================================================= */ const CssBlocks = { message_broadcast: ` #chatroom .webcast-chatroom___bottom-message, #chatroom > div > div > pace-island:has(div[style*="new_grade_enter"]), #chatroom > div > div > div:has(div[style*="new_grade_enter"]) { display: none !important; } `, bottom_area: ` .webcast-chatroom___list { clip-path: none !important; } `, gift_effects: ` #GiftTrayLayout, #GiftEffectLayout, #GiftMenuLayout, div[id^="gift_effect_bg_"] { display: none !important; } `, yellow_car: ` div[id^="living_room_player_container"] .basicPlayer > div:has(div[data-e2e="yellowCart-container"]), #EcmoCardLayout { display: none !important; } `, exhibition_tooltip: ` [data-e2e="exhibition-banner"] .dylive-tooltip { display: none !important; } `, danmaku: ` xg-danmu.xgplayer-danmu, #DanmakuLayout { display: none !important; } `, gifts_container: ` div[data-e2e="gifts-container"] { display: none !important; visibility: hidden !important; height: 0 !important; overflow: hidden !important; } `, }; function applyCssBlocks() { const enableAll = GM.get("enable_all"); const map = { "dylive-lite-style-message-broadcast": ["block_message_broadcast", CssBlocks.message_broadcast], "dylive-lite-style-bottom-area": ["block_bottom_area", CssBlocks.bottom_area], "dylive-lite-style-gift-effects": ["block_gift_effects", CssBlocks.gift_effects], "dylive-lite-style-yellow-car": ["block_yellow_car", CssBlocks.yellow_car], "dylive-lite-style-exhibition-tooltip": ["block_exhibition_tooltip", CssBlocks.exhibition_tooltip], "dylive-lite-style-danmaku": ["block_danmaku", CssBlocks.danmaku], "dylive-lite-style-gifts-container": ["block_gifts_container", CssBlocks.gifts_container], }; Object.entries(map).forEach(([styleId, [key, css]]) => { if (enableAll && GM.get(key)) addStyle(styleId, css); else removeStyle(styleId); }); } /* ========================================================= * 功能-清晰度选择 * ========================================================= */ const VideoQualityMap = { auto: { label: "自动", sign: 0 }, origin: { label: "原画", sign: 5 }, uhd: { label: "蓝光", sign: 4 }, hd: { label: "超清", sign: 3 }, sd: { label: "高清", sign: 2 }, ld: { label: "标清", sign: 1 }, }; function getReactProps(dom) { if (!dom) return null; const key = Object.keys(dom).find((k) => k.startsWith("__reactProps$")); return key ? dom[key] : null; } function getReactFiber(dom) { if (!dom) return null; const key = Object.keys(dom).find( (k) => k.startsWith("__reactFiber$") || k.startsWith("__reactInternalInstance$") ); return key ? dom[key] : null; } function waitForElement(selector, timeoutMs = 8000, intervalMs = 200) { return new Promise((resolve) => { const start = Date.now(); const timer = setInterval(() => { let $el = null; try { $el = typeof selector === "function" ? selector() : document.querySelector(selector); } catch (e) {} if ($el || Date.now() - start > timeoutMs) { clearInterval(timer); resolve($el || null); } }, intervalMs); }); } function pickHighestAvailableQuality(list) { const sorted = list .slice() .filter((q) => VideoQualityMap[q]) .sort((a, b) => VideoQualityMap[a].sign - VideoQualityMap[b].sign); return sorted[sorted.length - 1]; } let qualityApplyToken = 0; async function applyQuality() { const quality = GM.get("quality_select"); if (!GM.get("enable_all") || !quality || !VideoQualityMap[quality]) return; if (!isLivePage()) return; const myToken = ++qualityApplyToken; try { localStorage.setItem("webcast_local_quality", quality); } catch (e) {} try { document.cookie = `webcast_local_quality=${quality}; domain=.douyin.com; path=/`; document.cookie = `live_local_quality=${quality}; domain=.douyin.com; path=/`; } catch (e) {} const $ctrl = await waitForElement( () => document.querySelector('xg-inner-controls xg-right-grid > div:has([data-e2e="quality-selector"])'), 8000 ); if (myToken !== qualityApplyToken || !isLivePage()) return; if ($ctrl) { const props = getReactProps($ctrl); const qualityHandler = props?.children?.props?.children?.props?.qualityHandler; if (qualityHandler && typeof qualityHandler.setCurrentQuality === "function") { let target = quality; const list = typeof qualityHandler.getCurrentQualityList === "function" ? qualityHandler.getCurrentQualityList() : []; if (Array.isArray(list) && list.length && !list.includes(quality)) { target = pickHighestAvailableQuality(list) || quality; log(`当前直播没有【${VideoQualityMap[quality].label}】画质,自动选择最高画质【${VideoQualityMap[target]?.label || target}】`); } qualityHandler.setCurrentQuality(target); log(`已设置画质为【${VideoQualityMap[target]?.label || target}】`); return; } } const $plugin = await waitForElement("#PlayerLayout .douyin-player-controls .QualitySwitchNewPlugin > div", 8000); if (myToken !== qualityApplyToken || !isLivePage()) return; if ($plugin) { const fiber = getReactFiber($plugin); const qualityHandler = fiber?.return?.memoizedProps?.qualityHandler; const qualityList = fiber?.return?.memoizedProps?.qualityList; if (qualityHandler && typeof qualityHandler.setCurrentQuality === "function") { let target = quality; if (Array.isArray(qualityList) && qualityList.length && !qualityList.includes(quality)) { target = pickHighestAvailableQuality(qualityList) || quality; log(`当前直播没有【${VideoQualityMap[quality].label}】画质,自动选择最高画质【${VideoQualityMap[target]?.label || target}】`); } qualityHandler.setCurrentQuality(target); log(`已设置画质为【${VideoQualityMap[target]?.label || target}】`); return; } } log("未找到画质控制器,本次设置画质失败(可能直播间尚未加载完成)"); } /* ========================================================= * 聊天室消息过滤器 * ========================================================= */ const MessageFilter = { rules: [], initRules() { this.rules = []; const text = GM.get("filter_custom_rules") || ""; text .split("\n") .map((line) => line.trim()) .filter((line) => line !== "") .forEach((line) => { try { this.rules.push(new RegExp(line)); } catch (e) { log("自定义规则解析失败,已跳过:", line, e); } }); }, check(messageInst, method) { if (!GM.get("enable_all") || !GM.get("filter_enable")) return false; const payload = messageInst?.payload || messageInst; const message = payload?.content || payload?.common?.describe; method = method ?? messageInst?.method ?? payload?.method; const chat_by = payload?.chat_by; const public_area_common = payload?.public_area_common || {}; let flag = false; if (method === "WebcastGiftMessage") { if (GM.get("filter_block_gift_msg")) flag = true; } else if (method === "WebcastChatMessage") { const isLuckyBag = chat_by === "9" || chat_by === "10" || Object.keys(public_area_common?.individual_strategy_result || {}).length !== 0; if (isLuckyBag && GM.get("filter_block_lucky_bag")) flag = true; } else if (method === "WebcastRoomMessage") { if (GM.get("filter_block_room_message")) flag = true; } else if (method === "WebcastEmojiChatMessage") { if (GM.get("filter_block_emoji")) flag = true; } if (!flag && typeof message === "string" && this.rules.length) { flag = this.rules.some((re) => { try { return re.test(message); } catch (e) { return false; } }); } return flag; }, }; function extractMessageFromFiber(fiber) { if (!fiber) return null; let node = fiber; for (let i = 0; i < 8 && node; i++) { const props = node.memoizedProps; if (props?.message) return props.message; if (props?.children?.props?.message) return props.children.props.message; if (props?.children?.props?.children?.props?.message) return props.children.props.children.props.message; node = node.return; } return null; } async function tryHookDecoder(timeoutMs = 8000) { const start = Date.now(); return new Promise((resolve) => { const timer = setInterval(() => { const decoder = _win["__MESSAGE_INSTANCE__"]?.decoder; const found = decoder && typeof decoder.decode === "function"; if (found || Date.now() - start > timeoutMs) { clearInterval(timer); if (!found) { resolve(false); return; } const originDecode = decoder.decode; decoder.decode = async function (...args) { const [, method] = args; const payload = await Reflect.apply(originDecode, this, args); try { if (MessageFilter.check({ payload }, method)) { return {}; } } catch (e) { log("消息过滤出错:", e); } return payload; }; log("已挂钩直播消息解码器(decoder hook 生效)"); resolve(true); } }, 200); }); } function startDomFallbackFilter() { log("未找到消息解码器,启用DOM兜底过滤方案"); addStyle( "dylive-lite-style-chatroom-fix", `.webcast-chatroom___list > div { height: 100% !important; }` ); const scan = () => { if (!isLivePage()) return; const nodes = document.querySelectorAll( '#chatroom .webcast-chatroom___item:not([data-dylive-filter-checked])' ); nodes.forEach(($item) => { $item.setAttribute("data-dylive-filter-checked", "true"); try { const fiber = getReactFiber($item); const message = extractMessageFromFiber(fiber); if (message) { if (MessageFilter.check(message)) { $item.remove(); } } } catch (e) {} }); }; const observer = new MutationObserver(debounce(scan, 80)); observer.observe(document.documentElement, { childList: true, subtree: true }); scan(); } async function initMessageFilter() { MessageFilter.initRules(); const hooked = await tryHookDecoder(); if (!hooked) startDomFallbackFilter(); } /* ========================================================= * 功能-抢福袋(集成自“抖音快速抢福袋”) * ========================================================= */ let luckyBagTimer = null; let luckyBagCt = 0; let luckyBagWt = 0; // 颜色输出(仅用于控制台) function cc(color, ...args) { const colorsMap = { reset: '\x1b[0m', red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', cyan: '\x1b[36m' }; console.log(colorsMap[color] + args.join('') + colorsMap.reset); } function timestampToDatetime(timestamp) { return new Date(timestamp).toLocaleString(); } function startLuckyBag() { if (luckyBagTimer) { clearInterval(luckyBagTimer); luckyBagTimer = null; } if (!GM.get("enable_all") || !GM.get("enable_lucky_bag") || !isLivePage()) { return; } cc('cyan', '========== 启动抢福袋 =========='); luckyBagTimer = setInterval(function() { var currentDate = new Date(); var currentTime = currentDate.toLocaleString(); var currentms = currentDate.getTime(); var lofind = 0; // ========== 福袋弹窗处理 ========== $('#lottery_close_cotainer').each(function() { var tt = $(this).text(); lofind = 1; // 1. 没抽中福袋,点击知道了 if (tt.indexOf('没抽中福袋') > -1) { cc('red', '❌ 没抽中福袋 ', currentTime); $(this).find('div[role="button"]').each(function() { if (tt.indexOf('知道了') > -1) { console.log('点击知道了', $(this)[0]); $(this).click(); luckyBagCt = currentms; } }); return; } // 时间间隔检查 if (currentms > luckyBagCt + 200 && currentms > luckyBagWt + 2000) { luckyBagWt = currentms; // 提取倒计时时间 var ix = tt.match(/[0-9]{1,2}:[0-9]{1,2}/g); if (ix) { ix = ix[0].split(':'); ix = ix[0] * 60 + parseInt(ix[1]); if (ix > 0) { console.log('福袋倒计时 ', ix, ' 秒 ', currentTime); luckyBagWt += ix * 1000 - 1000; console.log('福袋开始时间:', timestampToDatetime(luckyBagWt)); } } // 2. 已参与福袋,点击关闭弹窗 if (tt.indexOf('已参与') > -1) { $(this).find('div > div > div.YnybGvCL,div > div > div.hJ3SHYaQ').last().each(function() { if ($(this).text().length < 3) { $(this).click(); console.log('已参与福袋,点击关闭', $(this)[0]); luckyBagCt = currentms; } }); return; } // 3. 倒计时中、加载中或可以一键评论参与 if (tt.indexOf('倒计时') > -1 || tt.indexOf('加载中') > -1 || tt.indexOf('一键发评论参与福袋') > -1) { $(this).find('div[role="button"]').each(function() { if (tt.indexOf('一键发评论参与福袋') > -1) { console.log('点击一键发评论参与福袋', $(this)[0]); $(this).click(); luckyBagCt = currentms; } }); return; } // 4. 恭喜中奖 if (tt.indexOf('恭喜你') > -1) { cc('green', '🎉 ' + tt, currentTime); $(this).find('div > div > div.YnybGvCL,div > div > div.hJ3SHYaQ').last().each(function() { if ($(this).text().length < 3) { $(this).click(); console.log('已得奖,点击关闭', $(this)[0]); luckyBagCt = currentms; } }); } // 5. 条件不满足(如需要升级或其他条件,且不需要加粉丝团) else if (tt.indexOf('加入') < 0 && tt.indexOf('提升') < 0) { cc('yellow', '⚠️ ' + tt, currentTime); $(this).find('div > div > div.YnybGvCL,div > div > div.hJ3SHYaQ').last().each(function() { if ($(this).text().length < 3) { $(this).click(); console.log('条件不满足,点击关闭', $(this)[0]); luckyBagCt = currentms; } }); } else { console.log('⚠️ 福袋需要加入粉丝团或提升等级,跳过'); } } }); // ========== 点击挂件进入福袋界面 ========== if (lofind === 0) { $('#ShortTouchLayout > div > div > div > div > div').each(function() { var ttx = $(this).text(); if (ttx.indexOf(':') > -1 && ttx.indexOf('00:00') < 0) { if (currentms - luckyBagWt > 0 && currentms - luckyBagCt > 60000) { console.log('发现福袋挂件,点击打开', ttx, currentTime); $(this).click(); luckyBagCt = currentms; } } }); } }, 1000); cc('green', '✅ 纯净福袋自动抢夺已启动'); } function stopLuckyBag() { if (luckyBagTimer) { clearInterval(luckyBagTimer); luckyBagTimer = null; cc('yellow', '⏹ 抢福袋已停止'); } } // 页面离开或切换时停止,进入直播时启动(由init调度) /* ========================================================= * 设置面板 UI(还原经典界面样式 + 增加位置记忆移动) * ========================================================= */ const PANEL_ID = "dylive-lite-panel"; const BUTTON_ID = "dylive-lite-toggle-btn"; function injectPanelStyle() { addStyle( "dylive-lite-panel-style", ` #${BUTTON_ID} { position: fixed; width: 44px; height: 44px; border-radius: 50%; background: linear-gradient(135deg, #fe2c55, #ff6b81); box-shadow: 0 4px 12px rgba(0,0,0,0.25); display: flex; align-items: center; justify-content: center; cursor: move; z-index: 999999; color: #fff; font-size: 20px; user-select: none; transition: transform .2s ease; } #${BUTTON_ID}:hover { transform: scale(1.08); } #${PANEL_ID} { position: fixed; width: 320px; max-height: 70vh; overflow-y: auto; background: #1e1e22; color: #f2f2f2; border-radius: 14px; box-shadow: 0 10px 30px rgba(0,0,0,0.4); z-index: 999999; font-size: 13px; display: none; padding: 14px 14px 18px; box-sizing: border-box; } #${PANEL_ID}.show { display: block; } #${PANEL_ID} .dl-title { font-size: 15px; font-weight: 600; margin-bottom: 10px; display: flex; align-items: center; justify-content: space-between; } #${PANEL_ID} .dl-title .dl-close { cursor: pointer; opacity: .6; font-size: 16px; } #${PANEL_ID} .dl-title .dl-close:hover { opacity: 1; } #${PANEL_ID} .dl-section { margin-bottom: 12px; border-top: 1px solid rgba(255,255,255,0.08); padding-top: 8px; } #${PANEL_ID} .dl-section:first-of-type { border-top: none; padding-top: 0; } #${PANEL_ID} .dl-section-title { font-size: 12px; color: #ff6b81; margin-bottom: 6px; font-weight: 600; } #${PANEL_ID} .dl-row { display: flex; align-items: center; justify-content: space-between; padding: 6px 0; } #${PANEL_ID} .dl-row-label { flex: 1; margin-right: 8px; line-height: 1.3; } #${PANEL_ID} .dl-row-desc { display: block; font-size: 11px; color: #9a9a9a; margin-top: 2px; } #${PANEL_ID} .dl-switch { position: relative; width: 36px; height: 20px; border-radius: 10px; background: #444; flex-shrink: 0; cursor: pointer; transition: background .2s ease; } #${PANEL_ID} .dl-switch.on { background: #fe2c55; } #${PANEL_ID} .dl-switch::after { content: ""; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; border-radius: 50%; background: #fff; transition: left .2s ease; } #${PANEL_ID} .dl-switch.on::after { left: 18px; } #${PANEL_ID} .dl-select { flex-shrink: 0; background: #101012; color: #eee; border: 1px solid rgba(255,255,255,0.15); border-radius: 6px; padding: 3px 6px; font-size: 12px; max-width: 110px; } #${PANEL_ID} textarea { width: 100%; height: 120px; margin-top: 6px; box-sizing: border-box; background: #101012; color: #eee; border: 1px solid rgba(255,255,255,0.12); border-radius: 8px; padding: 8px; font-size: 12px; resize: vertical; } #${PANEL_ID} .dl-hint { font-size: 11px; color: #9a9a9a; margin-top: 4px; } ` ); } function buildSelectRow(label, key, options, desc, onChange) { const $row = document.createElement("div"); $row.className = "dl-row"; const current = GM.get(key); const optionsHtml = options .map((opt) => ``) .join(""); $row.innerHTML = `