// ==UserScript== // @name B站自动跳过视频内广告 // @namespace http://tampermonkey.net/ // @version 1.3.0 // @description 基于 BilibiliSponsorBlock,实现 B 站自动跳过片段、进度条染色渲染、以及可视化片段上报功能。 // @author GEMINI // @match *://*.bilibili.com/video/* // @match *://www.bilibili.com/list/watchlater* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @connect bsbsb.top // @license MIT // ==/UserScript== (function() { 'use strict'; // ================== 配置与常量 ================== const API_BASE = "https://bsbsb.top/api"; const HEADERS = { "origin": "tampermonkey-bilibili-skip", "x-ext-version": "1.3.0" }; const CATEGORIES = { "sponsor": "赞助广告", "intro": "片头 / 动画", "outro": "片尾 / 鸣谢", "interaction": "互动提醒 (求三连)", "selfpromo": "自我推广", "music_offtopic": "音乐 / 非主题", "preview": "下集预告", "filler": "无意义填充", "poi_highlight": "高光时刻 (不跳过)" }; const CATEGORY_COLORS = { "sponsor": "#00d400", "intro": "#00ffff", "outro": "#0202ed", "interaction": "#cc00ff", "selfpromo": "#ffff00", "music_offtopic": "#ff9900", "preview": "#008fd6", "filler": "#7300FF", "poi_highlight": "#ff1684" }; const DEFAULT_SETTINGS = { "sponsor": true, "intro": true, "outro": true, "interaction": false, "selfpromo": false, "music_offtopic": false, "preview": false, "filler": false }; // ================== 全局变量 ================== let userSettings = GM_getValue("skipSettings", DEFAULT_SETTINGS); let currentSegments = []; // 所有片段 (用于渲染进度条) let activeSkipZones = []; // 预过滤后的执行片段 (用于秒级比对跳过) // [新增] 状态追踪,用于防止重复发包和误跳过 let recordedViews = new Set(); // 记录已经发过 recordView 请求的 UUID let ignoredZones = new Set(); // 记录用户手动拖进去的片段,临时忽略 let lastTimeUpdate = 0; // 记录上一帧的时间,用于判断是否为手动拖拽(Seek) let videoElement = null; let lastVideoKey = ""; let skipLockTimer = null; // ================== 核心功能 ================== function getOrGenerateUserID() { let id = GM_getValue("sponsorUserID"); if (!id || id.length < 30) { id = Array.from({length: 32}, () => Math.random().toString(36).charAt(2)).join(''); GM_setValue("sponsorUserID", id); } return id; } function getVideoInfo() { let bvid = location.pathname.match(/BV[a-zA-Z0-9]+/) ? location.pathname.match(/BV[a-zA-Z0-9]+/)[0] : ""; let cid = ""; try { // 兼容脚本猫与篡改猴的变量读取方式 const targetWindow = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window; if (targetWindow.__INITIAL_STATE__) { cid = targetWindow.__INITIAL_STATE__.videoData?.cid || targetWindow.__INITIAL_STATE__.epInfo?.cid || ""; } } catch (e) {} return { bvid, cid }; } function compileActiveZones() { activeSkipZones = []; currentSegments.forEach(seg => { if (userSettings[seg.category] && (!seg.actionType || seg.actionType === "skip")) { activeSkipZones.push({ start: seg.segment[0], end: seg.segment[1], uuid: seg.UUID, catName: CATEGORIES[seg.category] || seg.category }); } }); } function fetchSegments(bvid, cid) { if (!bvid) return; let url = `${API_BASE}/skipSegments?videoID=${bvid}${cid ? `&cid=${cid}` : ''}`; // 切换视频时,清空之前记录的防重复和忽略名单 recordedViews.clear(); ignoredZones.clear(); GM_xmlhttpRequest({ method: "GET", url: url, headers: HEADERS, onload: function(response) { if (response.status === 200) { try { currentSegments = JSON.parse(response.responseText); compileActiveZones(); renderProgressMarkers(); } catch (e) {} } else { currentSegments = []; activeSkipZones = []; } } }); } function submitSegment(start, end, category) { const { bvid, cid } = getVideoInfo(); if (!bvid || !videoElement) return showToast("未能获取视频状态,请稍后再试。"); if (start >= end) return showToast("结束时间必须大于起始时间!"); const data = { videoID: bvid, cid: cid, userID: getOrGenerateUserID(), userAgent: "Tampermonkey-Bilibili-Skip/1.3.0", videoDuration: videoElement.duration, segments: [{ segment: [parseFloat(start), parseFloat(end)], category: category, actionType: category === 'poi_highlight' ? "poi" : "skip" }] }; GM_xmlhttpRequest({ method: "POST", url: `${API_BASE}/skipSegments`, headers: Object.assign({"Content-Type": "application/json"}, HEADERS), data: JSON.stringify(data), onload: res => { if ([200, 201].includes(res.status)) { showToast("上报成功!感谢你的贡献。"); document.getElementById("skip-report-modal")?.remove(); fetchSegments(bvid, cid); } else if (res.status === 403) showToast("被自动审核拒绝。"); else if (res.status === 409) showToast("片段已存在。"); else showToast("上报失败:" + res.status); } }); } function recordView(uuid) { GM_xmlhttpRequest({ method: "POST", url: `${API_BASE}/viewedVideoSponsorTime`, headers: Object.assign({"Content-Type": "application/json"}, HEADERS), data: JSON.stringify({ UUID: uuid }) }); } // ================== DOM 交互与事件 ================== function renderProgressMarkers() { if (!videoElement || isNaN(videoElement.duration) || videoElement.duration <= 0) return; const progressBars = document.querySelectorAll('.bpx-player-progress-schedule, .bpx-player-shadow-progress-schedule'); if (progressBars.length === 0) return; document.querySelectorAll('.sponsor-segment-marker').forEach(e => e.remove()); const duration = videoElement.duration; const fragment = document.createDocumentFragment(); currentSegments.forEach(seg => { const start = seg.segment[0]; const end = seg.segment[1]; const marker = document.createElement('div'); marker.className = 'sponsor-segment-marker'; marker.style.cssText = ` position: absolute; top: 0; height: 100%; left: ${(start / duration) * 100}%; width: ${((end - start) / duration) * 100}%; background-color: ${CATEGORY_COLORS[seg.category] || "#fff"}; z-index: 5; opacity: 0.6; pointer-events: none; `; fragment.appendChild(marker); }); progressBars.forEach(bar => bar.appendChild(fragment.cloneNode(true))); } function onTimeUpdate() { if (activeSkipZones.length === 0 || skipLockTimer) return; let currentTime = videoElement.currentTime; // 判断用户是否正在进行大跨度的拖拽 (前后相差超过 1 秒) let isSeeking = Math.abs(currentTime - lastTimeUpdate) > 1.0; lastTimeUpdate = currentTime; for (let i = 0; i < activeSkipZones.length; i++) { let zone = activeSkipZones[i]; // 如果用户是手动拖拽,并且正好拖拽到了广告区间内 if (isSeeking && currentTime >= zone.start && currentTime < zone.end) { ignoredZones.add(zone.uuid); // 暂时忽略这个片段,允许用户观看 continue; } // 如果当前进度完全离开了这个片段,解除忽略状态 if (currentTime < zone.start || currentTime > zone.end) { ignoredZones.delete(zone.uuid); } // 核心逻辑:正常播放进入了跳过区间,并且该区间目前没有被用户手动操作忽略 if (!isSeeking && currentTime >= zone.start && currentTime < (zone.end - 0.5) && !ignoredZones.has(zone.uuid)) { skipLockTimer = setTimeout(() => { skipLockTimer = null; }, 1500); videoElement.currentTime = zone.end; lastTimeUpdate = zone.end; // 同步 lastTimeUpdate,防止触发下一帧的 Seeking 判断 showToast(`已为您跳过: ${zone.catName}`); // 防重复请求机制:如果当前视频此片段未发送过统计,才发送 if (!recordedViews.has(zone.uuid)) { recordView(zone.uuid); recordedViews.add(zone.uuid); } break; } } } function monitorVideoChange() { setInterval(() => { const { bvid, cid } = getVideoInfo(); const currentKey = `${bvid}_${cid}`; if (bvid && currentKey !== lastVideoKey) { lastVideoKey = currentKey; currentSegments = []; activeSkipZones = []; fetchSegments(bvid, cid); } let videoNodes = document.querySelectorAll("video, bwp-video"); let activeVideo = Array.from(videoNodes).find(v => v.offsetWidth > 0) || videoNodes[0]; if (activeVideo && activeVideo !== videoElement) { if (videoElement) { videoElement.removeEventListener("timeupdate", onTimeUpdate); videoElement.removeEventListener("loadeddata", renderProgressMarkers); } videoElement = activeVideo; lastTimeUpdate = videoElement.currentTime; // 初始化进度追踪 videoElement.addEventListener("timeupdate", onTimeUpdate); videoElement.addEventListener("loadeddata", renderProgressMarkers); } if (currentSegments.length > 0 && document.querySelectorAll('.sponsor-segment-marker').length === 0) { renderProgressMarkers(); } }, 1500); } // ================== UI 与弹窗界面 ================== function injectStyles() { if(document.getElementById("skip-styles")) return; const style = document.createElement("style"); style.id = "skip-styles"; style.innerHTML = ` .skip-modal-mask { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); z-index: 999999; display: flex; align-items: center; justify-content: center; font-family: sans-serif; } .skip-modal-content { background: #fff; width: 380px; border-radius: 8px; padding: 20px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); color: #333;} .skip-modal-content h3 { margin: 0 0 15px 0; font-size: 18px; border-bottom: 1px solid #eee; padding-bottom: 10px; } .skip-setting-item { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; font-size: 14px; } .skip-input-group { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; } .skip-input-group label { width: 70px; font-size: 14px;} .skip-input-group input, .skip-input-group select { flex: 1; padding: 6px; border: 1px solid #ccc; border-radius: 4px; } .skip-btn-small { padding: 5px 8px; font-size: 12px; border: 1px solid #00aeec; background: #fff; color: #00aeec; border-radius: 4px; cursor: pointer; } .skip-modal-actions { margin-top: 20px; display: flex; justify-content: flex-end; gap: 10px; } .skip-btn { padding: 6px 15px; border: none; border-radius: 4px; cursor: pointer; } .skip-btn-save { background: #00aeec; color: #fff; } .skip-btn-cancel { background: #e5e9ef; color: #555; } #skip-toast { position: fixed; bottom: 100px; left: 20px; background: rgba(0,0,0,0.8); color: #fff; padding: 10px 18px; border-radius: 4px; z-index: 999999; font-size: 14px; pointer-events: none; transition: opacity 0.3s; opacity: 0; } .skip-stats-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 10px; text-align: center; } .skip-stat-box { background: #f4f5f7; padding: 10px; border-radius: 6px;} .skip-stat-num { font-size: 20px; font-weight: bold; color: #00aeec; margin-top: 5px;} `; document.head.appendChild(style); const toast = document.createElement("div"); toast.id = "skip-toast"; document.body.appendChild(toast); } let toastTimeout; function showToast(text) { const toast = document.getElementById("skip-toast"); if (!toast) return; toast.innerText = text; toast.style.opacity = "1"; clearTimeout(toastTimeout); toastTimeout = setTimeout(() => toast.style.opacity = "0", 3500); } function createModal(id, innerHTML, onMount) { if (document.getElementById(id)) return; const modal = document.createElement("div"); modal.id = id; modal.className = "skip-modal-mask"; modal.innerHTML = innerHTML; document.body.appendChild(modal); if(onMount) onMount(modal); } function openSettingsUI() { let checkboxesHtml = Object.keys(CATEGORIES).filter(k => k !== 'poi_highlight').map(key => ` `).join(""); createModal("skip-settings-modal", `
`, (modal) => { document.getElementById("set-cancel").onclick = () => modal.remove(); document.getElementById("set-save").onclick = () => { Object.keys(CATEGORIES).filter(k => k !== 'poi_highlight').forEach(k => { let chk = document.getElementById(`chk-${k}`); userSettings[k] = chk ? chk.checked : false; }); GM_setValue("skipSettings", userSettings); compileActiveZones(); modal.remove(); showToast("设置已保存!"); }; }); } function openReportUI() { let optionsHtml = Object.keys(CATEGORIES).map(key => ``).join(""); createModal("skip-report-modal", ` `, (modal) => { document.getElementById("btn-start").onclick = () => videoElement && (document.getElementById("rep-start").value = videoElement.currentTime.toFixed(3)); document.getElementById("btn-end").onclick = () => videoElement && (document.getElementById("rep-end").value = videoElement.currentTime.toFixed(3)); document.getElementById("rep-cancel").onclick = () => modal.remove(); document.getElementById("rep-save").onclick = () => { const start = document.getElementById("rep-start").value; const end = document.getElementById("rep-end").value; if (!start || !end) return showToast("请完整填写起止时间"); submitSegment(start, end, document.getElementById("rep-cat").value); }; }); } function openStatsUI() { showToast("正在查询你的贡献数据..."); const uid = getOrGenerateUserID(); GM_xmlhttpRequest({ method: "GET", url: `${API_BASE}/userInfo?userID=${uid}`, headers: HEADERS, onload: res => { let data = { minutesSaved: 0, segmentCount: 0, viewCount: 0 }; if (res.status === 200) data = JSON.parse(res.responseText); createModal("skip-stats-modal", ` `, (modal) => { document.getElementById("stat-close").onclick = () => modal.remove(); }); } }); } function init() { injectStyles(); GM_registerMenuCommand("设置跳过类型", openSettingsUI); GM_registerMenuCommand("上报跳过片段", openReportUI); GM_registerMenuCommand("查看我的贡献", openStatsUI); monitorVideoChange(); console.log("启动"); } if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init); else init(); })();