您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
基于弹幕的指路而制作的广告跳过器
// ==UserScript== // @name B站视频恰饭广告跳过 // @namespace https://space.bilibili.com/508469689 // @version 0.8 // @description 基于弹幕的指路而制作的广告跳过器 // @author SerenMaze // @license MIT // @match https://www.bilibili.com/video/* // @icon https://www.google.com/s2/favicons?sz=64&domain=bilibili.com // @grant none // ==/UserScript== (function() { 'use strict'; const modalStyles = ` #bilibili-danmaku-seeker-modal { display: none; position: fixed; z-index: 10000; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.4); } #bilibili-danmaku-seeker-modal-content { background-color: #fff; margin: 15% auto; padding: 0; border: none; border-radius: 8px; width: 80%; max-width: 500px; box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2), 0 6px 20px 0 rgba(0,0,0,0.19); font-family: 'Helvetica Neue', Helvetica, Arial, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif; overflow: hidden; } #bilibili-danmaku-seeker-modal-header { padding: 16px 20px; background-color: #f6f6f6; color: #212121; font-size: 16px; font-weight: 500; border-bottom: 1px solid #e5e9ef; border-radius: 8px 8px 0 0; } #bilibili-danmaku-seeker-modal-body { padding: 0; max-height: 400px; overflow-y: auto; } .bilibili-danmaku-seeker-option { padding: 12px 20px; cursor: pointer; border-bottom: 1px solid #e5e9ef; transition: background-color 0.2s ease; display: flex; justify-content: space-between; align-items: center; } .bilibili-danmaku-seeker-option:hover { background-color: #f4f5f7; } .bilibili-danmaku-seeker-option:last-child { border-bottom: none; } .bilibili-danmaku-seeker-time { font-weight: bold; color: #fb7299; } .bilibili-danmaku-seeker-text { flex-grow: 1; margin-left: 15px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #6d757a; } #bilibili-danmaku-seeker-modal-footer { padding: 12px 20px; background-color: #f6f6f6; text-align: right; border-top: 1px solid #e5e9ef; border-radius: 0 0 8px 8px; } .bilibili-danmaku-seeker-button { padding: 6px 12px; margin-left: 10px; border: 1px solid #ccd0d7; border-radius: 4px; background-color: #fff; color: #6d757a; cursor: pointer; transition: all 0.2s ease; } .bilibili-danmaku-seeker-button:hover { background-color: #f4f5f7; color: #212121; } .bilibili-danmaku-seeker-button-primary { background-color: #00a1d6; border-color: #00a1d6; color: white; } .bilibili-danmaku-seeker-button-primary:hover { background-color: #00b5e5; } `; function waitForElm(selector, timeout = 10000) { return new Promise((resolve, reject) => { if (document.querySelector(selector)) { return resolve(document.querySelector(selector)); } const observer = new MutationObserver(mutations => { if (document.querySelector(selector)) { resolve(document.querySelector(selector)); observer.disconnect(); } }); observer.observe(document.body, { childList: true, subtree: true }); if (timeout > 0) { setTimeout(() => { observer.disconnect(); reject(new Error(`Timeout waiting for element: ${selector}`)); }, timeout); } }); } function parseTimeString(text) { let seconds = -1; const originalText = text; text = text.replace(/:/g, ':'); const timeHMSRegex = /(?:\D|^)(\d{1,2}):([0-5]?\d):([0-5]?\d)(?=\D|$)/; const hmsMatch = text.match(timeHMSRegex); if (hmsMatch) { const hours = parseInt(hmsMatch[1], 10); const minutes = parseInt(hmsMatch[2], 10); const secs = parseInt(hmsMatch[3], 10); if (hours >= 0 && hours <= 99 && minutes >= 0 && minutes <= 59 && secs >= 0 && secs <= 59) { seconds = hours * 3600 + minutes * 60 + secs; return { seconds, format: `${hours}:${minutes < 10 ? '0' + minutes : minutes}:${secs < 10 ? '0' + secs : secs}` }; } } const timeHMChineseRegex = /(?:\D|^)(\d{1,2})小时([0-5]?\d)分(?=\D|$)/; const hmChineseMatch = text.match(timeHMChineseRegex); if (hmChineseMatch) { const hours = parseInt(hmChineseMatch[1], 10); const minutes = parseInt(hmChineseMatch[2], 10); if (hours >= 0 && hours <= 99 && minutes >= 0 && minutes <= 59) { seconds = hours * 3600 + minutes * 60; return { seconds, format: `${hours}小时${minutes}分` }; } } const timeHMSChineseRegex = /(?:\D|^)(\d{1,2})小时([0-5]?\d)分([0-5]?\d)秒(?=\D|$)/; const hmsChineseMatch = text.match(timeHMSChineseRegex); if (hmsChineseMatch) { const hours = parseInt(hmsChineseMatch[1], 10); const minutes = parseInt(hmsChineseMatch[2], 10); const secs = parseInt(hmsChineseMatch[3], 10); if (hours >= 0 && hours <= 99 && minutes >= 0 && minutes <= 59 && secs >= 0 && secs <= 59) { seconds = hours * 3600 + minutes * 60 + secs; return { seconds, format: `${hours}小时${minutes}分${secs}秒` }; } } const timeColonRegex = /(?:\D|^)(\d{1,2}):([0-5]?\d)(?=\D|$)/; const colonMatch = text.match(timeColonRegex); if (colonMatch) { const minutes = parseInt(colonMatch[1], 10); const secs = parseInt(colonMatch[2], 10); if (minutes >= 0 && minutes <= 99 && secs >= 0 && secs <= 59) { seconds = minutes * 60 + secs; return { seconds, format: `${minutes}:${secs < 10 ? '0' + secs : secs}` }; } } const timeChineseRegex = /(?:\D|^)(\d{1,3})分([0-5]?\d)秒(?=\D|$)/; const chineseMatch = text.match(timeChineseRegex); if (chineseMatch) { const minutes = parseInt(chineseMatch[1], 10); const secs = parseInt(chineseMatch[2], 10); if (minutes >= 0 && minutes <= 99 && secs >= 0 && secs <= 59) { seconds = minutes * 60 + secs; return { seconds, format: `${minutes}分${secs}秒` }; } } return { seconds, format: null }; } let wasPlaying = false; let seekerButton = null; let seekerButtonWrap = null; async function createAndInsertButton() { let shareItemWrap = null; let attempts = 0; const maxAttempts = 50; const retryInterval = 300; while (!shareItemWrap && attempts < maxAttempts) { shareItemWrap = document.querySelector('.toolbar-left-item-wrap:has(.video-share), .toolbar-left-item-wrap:has(.video-share-wrap)'); if (!shareItemWrap) { attempts++; console.log(`[B站弹幕跳转] 等待分享按钮容器加载... (尝试 ${attempts}/${maxAttempts})`); await new Promise(resolve => setTimeout(resolve, retryInterval)); } } if (!shareItemWrap) { console.warn("[B站弹幕跳转] 超时:未能找到分享按钮的 .toolbar-left-item-wrap 容器。"); return; } console.log("[B站弹幕跳转] 找到分享按钮容器:", shareItemWrap); if (document.getElementById('bilibili-danmaku-seeker-button-inline')) { console.log("[B站弹幕跳转] 按钮已存在"); return; } seekerButton = document.createElement('button'); seekerButton.id = 'bilibili-danmaku-seeker-button-inline'; seekerButton.textContent = '⏩'; seekerButton.title = '根据当前弹幕刷的时间跳过'; Object.assign(seekerButton.style, { background: 'none', border: 'none', borderRadius: '4px', padding: '0 4px', margin: '0 0 0 4px', cursor: 'pointer', fontSize: '14px', lineHeight: '26px', height: '26px', color: '#9499a0', backgroundColor: 'transparent', transition: 'color 0.3s, background-color 0.3s', verticalAlign: 'middle', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', whiteSpace: 'nowrap', flexShrink: '0' }); seekerButton.addEventListener('mouseenter', () => { seekerButton.style.color = '#61666d'; seekerButton.style.backgroundColor = 'rgba(0,0,0,0.05)'; }); seekerButton.addEventListener('mouseleave', () => { seekerButton.style.color = '#9499a0'; seekerButton.style.backgroundColor = 'transparent'; }); seekerButton.addEventListener('click', showTimeSelectionModal); seekerButtonWrap = document.createElement('div'); seekerButtonWrap.className = 'toolbar-left-item-wrap'; Object.assign(seekerButtonWrap.style, { display: 'inline-flex', alignItems: 'center', height: '32px', }); seekerButtonWrap.appendChild(seekerButton); const toolbarLeftMain = shareItemWrap.parentNode; if (toolbarLeftMain && toolbarLeftMain.classList.contains('video-toolbar-left-main')) { toolbarLeftMain.insertBefore(seekerButtonWrap, shareItemWrap.nextSibling); console.log("[B站弹幕跳转] 按钮已成功插入到分享按钮右侧。"); } else { console.error("[B站弹幕跳转] 无法定位到正确的父容器 .video-toolbar-left-main 来插入按钮。"); if (shareItemWrap.parentNode) { shareItemWrap.parentNode.insertBefore(seekerButtonWrap, shareItemWrap.nextSibling); console.log("[B站弹幕跳转] Fallback: 按钮已插入到分享按钮容器之后。"); } else { console.error("[B站弹幕跳转] Fallback 也失败:shareItemWrap 没有 parentNode。"); } } } async function showTimeSelectionModal() { console.log("--- 开始显示时间选择模态框 ---"); const player = getPlayer(); if (player) { try { if (typeof player.paused === 'boolean') { wasPlaying = !player.paused; } else if (typeof player.getState === 'function') { wasPlaying = player.getState() === 'PLAYING'; } else { wasPlaying = true; } console.log("暂停前播放状态:", wasPlaying); if (wasPlaying) { if (typeof player.pause === 'function') { player.pause(); console.log("视频已暂停"); } else if (player.video && typeof player.video.pause === 'function') { player.video.pause(); console.log("视频已暂停 (通过 player.video)"); } } } catch (e) { console.warn("暂停视频时出错:", e); } } else { console.warn("暂停视频失败:未找到播放器对象"); } const danmakuContainer = document.querySelector('.bpx-player-row-dm-wrap'); if (!danmakuContainer) { console.warn("未找到弹幕容器 '.bpx-player-row-dm-wrap'"); alert('未找到弹幕容器,请确保弹幕已开启且正在播放。'); resumeIfWasPlaying(); return; } const danmakuElements = danmakuContainer.querySelectorAll('.bili-danmaku-x-dm'); console.log(`找到 ${danmakuElements.length} 个弹幕项`); if (danmakuElements.length === 0) { console.warn("弹幕容器内未找到任何弹幕项 '.bili-danmaku-x-dm'"); alert('弹幕容器内未找到弹幕项,请确保弹幕已开启且正在播放。'); resumeIfWasPlaying(); return; } const timeEntries = []; danmakuElements.forEach((element) => { const danmakuText = element.textContent.trim(); if (danmakuText) { const { seconds, format } = parseTimeString(danmakuText); if (seconds >= 0 && format) { const displayText = danmakuText.length > 30 ? danmakuText.substring(0, 30) + '...' : danmakuText; timeEntries.push({ time: seconds, format: format, element: element, fullText: danmakuText, displayText: displayText }); } } }); if (timeEntries.length === 0) { console.log('未在当前屏幕弹幕中找到时间文本'); alert('当前屏幕上没有找到包含时间的弹幕 (格式示例: XX:XX:XX, XX:XX, X时X分X秒, X分X秒)'); resumeIfWasPlaying(); return; } createAndShowModal(timeEntries); } function createAndShowModal(timeEntries) { if (!document.getElementById('bilibili-danmaku-seeker-styles')) { const styleSheet = document.createElement("style"); styleSheet.id = 'bilibili-danmaku-seeker-styles'; styleSheet.innerText = modalStyles; document.head.appendChild(styleSheet); } const modal = document.createElement('div'); modal.id = 'bilibili-danmaku-seeker-modal'; const modalContent = document.createElement('div'); modalContent.id = 'bilibili-danmaku-seeker-modal-content'; const modalHeader = document.createElement('div'); modalHeader.id = 'bilibili-danmaku-seeker-modal-header'; modalHeader.textContent = '请选择要跳转的时间'; const modalBody = document.createElement('div'); modalBody.id = 'bilibili-danmaku-seeker-modal-body'; timeEntries.forEach(entry => { const option = document.createElement('div'); option.className = 'bilibili-danmaku-seeker-option'; option.dataset.time = entry.time; const timeSpan = document.createElement('span'); timeSpan.className = 'bilibili-danmaku-seeker-time'; timeSpan.textContent = entry.format; const textSpan = document.createElement('span'); textSpan.className = 'bilibili-danmaku-seeker-text'; textSpan.textContent = entry.displayText; textSpan.title = entry.fullText; option.appendChild(timeSpan); option.appendChild(textSpan); option.addEventListener('click', () => { const selectedTime = parseFloat(option.dataset.time); console.log(`用户选择了时间: ${entry.format} (${selectedTime}s)`); closeModal(modal); seekToTimeAndResume(selectedTime); }); modalBody.appendChild(option); }); const modalFooter = document.createElement('div'); modalFooter.id = 'bilibili-danmaku-seeker-modal-footer'; const cancelButton = document.createElement('button'); cancelButton.className = 'bilibili-danmaku-seeker-button'; cancelButton.textContent = '取消'; cancelButton.addEventListener('click', () => { console.log("用户取消选择"); closeModal(modal); resumeIfWasPlaying(); }); modalFooter.appendChild(cancelButton); modalContent.appendChild(modalHeader); modalContent.appendChild(modalBody); modalContent.appendChild(modalFooter); modal.appendChild(modalContent); modal.addEventListener('click', (event) => { if (event.target === modal) { console.log("用户点击背景关闭模态框"); closeModal(modal); resumeIfWasPlaying(); } }); document.body.appendChild(modal); modal.style.display = 'block'; console.log("时间选择模态框已显示"); } function closeModal(modalElement) { if (modalElement && modalElement.parentNode) { modalElement.parentNode.removeChild(modalElement); console.log("时间选择模态框已关闭并移除"); } } function getPlayer() { let player = null; if (window.player && (typeof window.player.seek === 'function' || typeof window.player.currentTime === 'number' || typeof window.player.pause === 'function')) { console.log("找到播放器对象在路径: window.player (直接访问)"); return window.player; } const playerPaths = [ 'player', '__INITIAL_STATE__.videoData.player', 'bilibiliPlayer', '__INITIAL_STATE__.player' ]; for (const path of playerPaths) { try { player = path.split('.').reduce((obj, prop) => obj && obj[prop], window); if (player && ( typeof player.seek === 'function' || typeof player.currentTime === 'number' || typeof player.pause === 'function' || (player.video && (typeof player.video.seek === 'function' || typeof player.video.pause === 'function')) )) { console.log(`找到播放器对象在路径: window.${path}`); return player; } player = null; } catch (e) { console.log(`查找播放器路径 ${path} 时出错:`, e.message); } } console.warn("未能找到可用的 B站 播放器对象。"); return null; } function seekToTimeAndResume(targetTime) { const player = getPlayer(); if (!player) { alert(`跳转失败:未找到播放器对象。 请手动跳转到 ${targetTime} 秒。`); return; } try { if (typeof player.seek === 'function') { player.seek(targetTime); } else if (player.video && typeof player.video.seek === 'function') { player.video.seek(targetTime); } else if (typeof player.currentTime !== 'undefined') { player.currentTime = targetTime; } else { throw new Error("播放器对象没有可用的 seek 或 currentTime 方法/属性"); } console.log(`视频已跳转到 ${targetTime} 秒`); setTimeout(() => { if (wasPlaying) { console.log("恢复播放"); if (typeof player.play === 'function') { player.play(); } else if (player.video && typeof player.video.play === 'function') { player.video.play(); } else { console.warn("播放器对象没有可用的 play 方法"); } } else { console.log("视频在暂停前是暂停状态,保持暂停。"); } }, 300); } catch (error) { console.error("跳转或恢复播放过程中发生错误:", error); alert(`操作失败: ${error.message} 请手动跳转到 ${targetTime} 秒。`); setTimeout(() => { if (wasPlaying) resumeIfWasPlaying(true); }, 500); } } function resumeIfWasPlaying(force = false) { if (wasPlaying || force) { const player = getPlayer(); if (player) { console.log("尝试恢复播放 (在 resumeIfWasPlaying 中)"); try { if (typeof player.play === 'function') { player.play(); } else if (player.video && typeof player.video.play === 'function') { player.video.play(); } } catch (e) { console.warn("恢复播放时出错:", e); } } } } function handleKeyDown(event) { if ((event.key === 'o' || event.key === 'O' || event.keyCode === 79) && event.target.tagName !== 'INPUT' && event.target.tagName !== 'TEXTAREA' && !event.target.isContentEditable) { console.log("[B站弹幕跳转] 检测到 'O' 键按下"); showTimeSelectionModal(); } } // 添加全局键盘事件监听器 document.addEventListener('keydown', handleKeyDown); const init = async () => { if (document.getElementById('bilibili-danmaku-seeker-button-inline')) { console.log("内联按钮已存在,跳过创建"); return; } try { await createAndInsertButton(); } catch (e) { console.error("初始化按钮时出错:", e); } }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { setTimeout(init, 2000); } let lastUrl = location.href; new MutationObserver(() => { const url = location.href; if (url !== lastUrl) { lastUrl = url; console.log("检测到URL变化"); setTimeout(() => { if (!document.getElementById('bilibili-danmaku-seeker-button-inline')) { console.log("URL变化后,尝试重新创建按钮..."); init(); } else { console.log("URL变化后,按钮已存在。"); } }, 3000); } else { if (!document.getElementById('bilibili-danmaku-seeker-button-inline')) { if (!window._seekerButtonCheckTimer) { window._seekerButtonCheckTimer = setTimeout(() => { console.log("DOM变化,检查并尝试创建按钮..."); init(); window._seekerButtonCheckTimer = null; }, 2000); } } } }).observe(document, { subtree: true, childList: true }); })();