您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
智能检测弹幕中广告提醒,提取时间戳并自动跳转播放器进度
当前为
// ==UserScript== // @name B站根据弹幕提醒跳过广告 // @namespace https://greasyfork.org/zh-CN/scripts/542541 // @version 1.05 // @description 智能检测弹幕中广告提醒,提取时间戳并自动跳转播放器进度 // @author chemhunter // @match https://www.bilibili.com/video/* // @grant none // @run-at document-end // @license MIT // ==/UserScript== (function () { 'use strict'; const timeRegexList = [ { regex: /\b(?!\d[::]0\b)(\d{1,2})[::](\d{1,2})\b/, isFuzzy: false }, // 5:14 { regex: /(\d{1,2}|[一二三四五六七八九十]{1,3})分(\d{1,2}|[零一二三四五六七八九十]{1,3})秒/, isFuzzy: false }, { regex: /(\d{1,2})\.(\d{1,2})[郎朗]/, isFuzzy: false }, { regex: /(\d{1,2}|[一二三四五六七八九十]{1,3})分(\d{1,2}|[零一二三四五六七八九十]{1,3})/, isFuzzy: false }, { regex: /(?<!\d)(\d{1,2})\.(\d{1,2})(?![\d郎秒分:])/, isFuzzy: true } // 模糊时间戳:纯数字 5.14 ]; const cnNumMap = { "零": 0, "一": 1, "二": 2, "三": 3, "四": 4, "五": 5, "六": 6, "七": 7, "八": 8, "九": 9, "十": 10 }; function parseChineseNumber(ch) { if (ch === "十") return 10; if (ch.length === 1) return cnNumMap[ch] || 0; if (ch.length === 2 && ch[0] === "十") return 10 + (cnNumMap[ch[1]] || 0); if (ch.length === 2 && ch[1] === "十") return (cnNumMap[ch[0]] || 0) * 10; if (ch.length === 3 && ch[1] === "十") return (cnNumMap[ch[0]] || 0) * 10 + (cnNumMap[ch[2]] || 0); return 0; } const pendingDanmaku = []; const timestampCounter = new Map(); const processedTimestamps = new Set(); const fuzzyCandidates = []; // { timestamp, timeAdded } const TIME_GROUP_THRESHOLD = 10; const FUZZY_TIMEOUT = 10; const MIN_COUNT_TO_LOG = 2; let lastJumpTime = 0; function extractTimestamps(text) { const cleanText = text.replace(/\s+/g, ''); for (let i = 0; i < timeRegexList.length; i++) { const { regex, isFuzzy } = timeRegexList[i]; const match = regex.exec(cleanText); if (match) { let minutes = /[一二三四五六七八九十]/.test(match[1]) ? parseChineseNumber(match[1]) : parseInt(match[1]) || 0; let seconds = /[一二三四五六七八九十]/.test(match[2]) ? parseChineseNumber(match[2]) : parseInt(match[2]) || 0; const ts = minutes * 60 + seconds; if (!isNaN(ts)) { return { timestamp: ts, hasLang: /[郎朗]/.test(text), isFuzzy }; } } } return null; } function recordTimestamp(ts) { for (const [existingTs, count] of timestampCounter.entries()) { if (Math.abs(existingTs - ts) <= TIME_GROUP_THRESHOLD) { const newKey = Math.min(existingTs, ts); timestampCounter.set(newKey, count + 1); if (existingTs !== newKey) timestampCounter.delete(existingTs); return; } } timestampCounter.set(ts, 1); } function processPendingDanmaku() { const now = Date.now(); // 先记录本轮新的明确时间戳(用于模糊匹配) for (const { text, timestamp: ts, hasLang } of pendingDanmaku) { if (hasLang) { timestampCounter.set(ts, MIN_COUNT_TO_LOG); } else { recordTimestamp(ts); } } // 处理模糊候选 for (let i = fuzzyCandidates.length - 1; i >= 0; i--) { if (now - fuzzyCandidates[i].timeAdded >= FUZZY_TIMEOUT*1000) { console.log('[模糊丢弃]', fuzzyCandidates[i].timestamp); fuzzyCandidates.splice(i, 1); } } for (let i = fuzzyCandidates.length - 1; i >= 0; i--) { const fuzzy = fuzzyCandidates[i]; for (const ts of timestampCounter.keys()) { if (Math.abs(fuzzy.timestamp - ts) <= TIME_GROUP_THRESHOLD) { console.log('[模糊转正]', fuzzy.timestamp, '因匹配到', ts); recordTimestamp(fuzzy.timestamp); fuzzyCandidates.splice(i, 1); break; } } } pendingDanmaku.length = 0; //跳转逻辑 for (const [ts, count] of timestampCounter) { if (ts > 0 && count >= MIN_COUNT_TO_LOG && !processedTimestamps.has(ts)) { if (Date.now() - lastJumpTime >= 20000) { console.log(`[跳转提示] 发现广告时间戳 ${formatTime(ts)},出现次数:${count}`); jumpToTimestamp(ts); showJumpNotice(ts); processedTimestamps.add(ts); lastJumpTime = Date.now(); } else { console.log('[跳转抑制] 离上次跳转不足20秒'); } } } timestampCounter.clear(); } function formatTime(ts) { const min = Math.floor(ts / 60); const sec = String(ts % 60).padStart(2, '0'); return `${min}:${sec}`; } function showJumpNotice(ts) { const container = document.querySelector('.bpx-player-video-wrap'); if (!container) return; const box = document.createElement('div'); box.innerText = '跳转广告 ⏩ ' + formatTime(ts); Object.assign(box.style, { position: 'absolute', top: '10px', left: '50%', transform: 'translateX(-50%)', padding: '6px 12px', backgroundColor: 'rgba(0, 0, 0, 0.4)', color: '#fff', fontSize: '16px', fontWeight: 'bold', borderRadius: '8px', zIndex: '9999', pointerEvents: 'none', opacity: '0', transition: 'opacity 0.3s ease' }); container.style.position = 'relative'; container.appendChild(box); requestAnimationFrame(() => { box.style.opacity = '1'; }); setTimeout(() => { box.style.opacity = '0'; setTimeout(() => box.remove(), 500); }, 3000); } function jumpToTimestamp(ts) { const video = document.querySelector('video'); if (!video || video.duration < ts + 60 || video.currentTime >= ts || video.currentTime < 20) return; video.currentTime = ts; console.log(`[跳转成功] 已跳转至 ${formatTime(ts)}`); } function handleDanmakuMutations(mutationsList) { for (const mutation of mutationsList) { for (const node of mutation.addedNodes) { if (node._danmakuHandled) continue; node._danmakuHandled = true; const text = node.textContent.trim(); if (text.length === 0 ||text === '9麤') continue; //console.log('>>>', text); //console.log('[弹幕节点识别]', text, 'from', node); const result = extractTimestamps(text); if (result) { console.log('识别弹幕:', text, result.timestamp, result.isFuzzy ? '[模糊]' : ''); if (result.isFuzzy) { fuzzyCandidates.push({ timestamp: result.timestamp, timeAdded: Date.now() }); } else { pendingDanmaku.push({ text, timestamp: result.timestamp, hasLang: result.hasLang }); } } } } } function ObserveDanmaku(container) { console.log('[监听启动] 弹幕容器绑定成功'); const observer = new MutationObserver(handleDanmakuMutations); observer.observe(container, { childList: true, subtree: true }); setInterval(processPendingDanmaku, 2500); } function startObserveDanmakuOnceReady() { const check = setInterval(() => { const container = document.querySelector('div.bpx-player-render-dm-wrap > div.bpx-player-dm-mask-wrap > div.bpx-player-row-dm-wrap'); if (container) { clearInterval(check); ObserveDanmaku(container); } }, 1000); } startObserveDanmakuOnceReady(); })();