您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
智能检测弹幕中广告提醒,提取时间戳并自动跳转播放器进度
// ==UserScript== // @name Bilibili-根据弹幕提醒跳过广告 // @namespace https://greasyfork.org/zh-CN/scripts/542541 // @version 1.08 // @description 智能检测弹幕中广告提醒,提取时间戳并自动跳转播放器进度 // @author chemhunter // @match https://www.bilibili.com/video/* // @icon https://i0.hdslb.com/bfs/static/jinkela/long/images/favicon.ico // @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郎秒分:wk++])/i, isFuzzy: true } // 模糊时间戳:纯数字 5.14 ]; const cnNumMap = { "零": 0, "一": 1, "二": 2,"两": 2, "三": 3, "四": 4, "五": 5, "六": 6, "七": 7, "八": 8, "九": 9, "十": 10 }; function parseNumber(char) { return cnNumMap[char] || parseInt(char) || 0; } function parseChineseNumber(ch) { if (ch.length === 1) return parseNumber(ch); if (ch.length === 2) return (ch[1] === "十") ? parseNumber(ch[0]) * 10 : (10 + parseNumber(ch[1])); if (ch.length === 3 && ch[1] === "十") return parseNumber(ch[0]) * 10 + parseNumber(ch[2]); return 0; } const pendingDanmaku = []; const timestampCounter = new Map(); const processedTimestamps = new Map(); const fuzzyCandidates = []; // { timestamp, timeAdded } const TS_REPEAT_COOLDOWN = 20; // 同一时间戳多久后可以再次触发跳转 const TIME_GROUP_THRESHOLD = 10; const FUZZY_TIMEOUT = 10; const MIN_JUMP_INTERVAL = 10; const MIN_COUNT_TO_LOG = 2; let lastJumpTime = 0; function extractTimestamps(text) { if (/[百千万亿wk]/i.test(text)) return null; 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) { const parts = [match[1], match[2]]; const isChinese = parts.map(p => /[一二三四五六七八九十]/.test(p)); const values = parts.map((p, idx) => isChinese[idx] ? parseChineseNumber(p) : parseInt(p) || 0); const ts = values[0] * 60 + values[1]; if (!isNaN(ts) && ts >= 60) { //限制广告时间戳位置在01:00之后 return { timestamp: ts, isAdTs: /[郎朗猜我]/.test(text) || (isChinese[0] !== isChinese[1]), 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 log(...args) { console.log('[B站弹幕跳广告] ', ...args); } function processPendingDanmaku() { const now = Date.now(); // 先记录本轮新的明确时间戳(用于模糊匹配) for (const { text, timestamp: ts, isAdTs } of pendingDanmaku) { if (isAdTs) { 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) { log('[模糊丢弃]', formatTime(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) { log('[模糊转正]', fuzzy.timestamp, '因匹配到', ts); recordTimestamp(fuzzy.timestamp); fuzzyCandidates.splice(i, 1); break; } } } pendingDanmaku.length = 0; handleJumpToAdTimestamps(); timestampCounter.clear(); } function handleJumpToAdTimestamps() { const now = Date.now(); const video = document.querySelector('video'); const current = video.currentTime; const duration = video.duration; for (const [ts, count] of timestampCounter) { const lastHandled = processedTimestamps.get(ts) || 0; const timeSinceLastHandled = now - lastHandled; const timeSinceLastJump = now - lastJumpTime; const shouldJump = count >= MIN_COUNT_TO_LOG && //真正广告时间戳 ts - current > 0 && // 跳转时间戳在当前位置后面 ts - current < 180 && // 跳转时间戳在当前位置后面3分钟内 ts < duration - 60 && //最后60s不跳 current > 30; // 前30s不跳 if (shouldJump) { if (timeSinceLastHandled < TS_REPEAT_COOLDOWN * 1000 || timeSinceLastJump < MIN_JUMP_INTERVAL * 1000 ) { log('[跳转抑制] 防止频繁跳转'); } else { log(`广告时间戳 ${formatTime(ts)},计数:${count},1.5秒后跳转`); video.currentTime = ts; console.log(`✅[跳转成功] 已从 ${formatTime(current)} 跳转至 ⏩${formatTime(ts)}`); showJumpNotice(ts); processedTimestamps.set(ts, now); lastJumpTime = now; } } } } function formatTime(seconds) { const hours = Math.floor(seconds / 3600); const remainingSeconds = seconds % 3600; const minutes = Math.floor(remainingSeconds / 60); const secs = Math.floor(remainingSeconds % 60); return hours > 0 ? `${hours}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}` : `${minutes}:${String(secs).padStart(2, '0')}`; } 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); }, 4000); } 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); //log('[弹幕节点识别]', text, 'from', node); const result = extractTimestamps(text); if (result) { console.log('📌识别时间戳弹幕:', text, formatTime(result.timestamp), result.isFuzzy ? '[疑似]' : '[真实]'); if (result.isFuzzy) { fuzzyCandidates.push({ timestamp: result.timestamp, timeAdded: Date.now() }); } else { pendingDanmaku.push({ text, timestamp: result.timestamp, isAdTs: result.isAdTs }); } } } } } function ObserveDanmaku(container) { 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(); })();