您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Bilibili SponsorBlock - 跳过B站恰饭片段
// ==UserScript== // @name BilibiliSponsorBlock // @namespace https://github.com/hanydd/BilibiliSponsorBlock // @version 0.8.5 // @description Bilibili SponsorBlock - 跳过B站恰饭片段 // @author hanydd (https://github.com/hanydd) // @license GPL-3.0 // @match https://*.bilibili.com/video/* // @icon https://www.bilibili.com/favicon.ico // @grant GM_xmlhttpRequest // @connect bsbsb.top // @run-at document-start // ==/UserScript== /** * BilibiliSponsorBlock Userscript * * This userscript provides sponsor segment skipping functionality for Bilibili videos * by implementing a cross-platform Tampermonkey script that works on Chrome, Firefox, Safari, and other browsers. * * Main Features: * - Automatically skips sponsor segments in Bilibili videos * - Supports multiple segment categories (sponsor, self-promo, interaction, etc.) * - Cross-platform compatibility through Tampermonkey * * Architecture: * - Uses GM_xmlhttpRequest for API communication * - Implements segment detection and automatic skipping * - Handles single-page application navigation */ (function () { "use strict"; // ============================================================================= // HTTP REQUEST WRAPPER // ============================================================================= /** * HTTP request wrapper using GM_xmlhttpRequest * Provides fetch-like interface for making API requests * @param {string} method - HTTP method (GET, POST, etc.) * @param {string} url - Request URL * @param {Object|string} data - Request data * @returns {Promise} Promise resolving to response object */ function makeRequest(method, url, data) { return new Promise((resolve, reject) => { const details = { method: method, url: url, onload: function (response) { // Create fetch-like response object resolve({ ok: response.status >= 200 && response.status < 300, status: response.status, text: () => Promise.resolve(response.responseText), json: () => Promise.resolve(JSON.parse(response.responseText)), }); }, onerror: function (error) { reject(error); }, }; if (method.toUpperCase() !== "GET" && data) { details.data = typeof data === "string" ? data : JSON.stringify(data); details.headers = { "Content-Type": "application/json", }; } GM_xmlhttpRequest(details); }); } // ============================================================================= // CONFIGURATION SYSTEM // ============================================================================= /** * Configuration management class * Handles userscript settings and category preferences */ class UserscriptConfig { constructor() { this.loadDefaults(); } /** * Load default configuration values * Sets up server address and category skip preferences */ loadDefaults() { // Configuration with server address and category skip settings this.config = { // Server configuration serverAddress: "https://bsbsb.top", // Category skip settings - determines which segment types to skip categorySelections: { sponsor: { skip: true }, // 赞助商片段 selfpromo: { skip: true }, // 自我推广 interaction: { skip: true }, // 互动提醒 intro: { skip: false }, // 开场 outro: { skip: false }, // 结尾 preview: { skip: false }, // 预览 music_offtopic: { skip: false }, // 音乐片段 filler: { skip: false }, // 填充内容 }, }; } /** * Get configuration value * @param {string} key - Configuration key * @returns {*} Configuration value */ get(key) { return this.config[key]; } } // Initialize global configuration instance const Config = new UserscriptConfig(); // ============================================================================= // PAGE DETECTION UTILITIES // ============================================================================= /** * Extract Bilibili video ID from current URL * @returns {string|null} Video ID (BVID) or null if not found */ function getBilibiliVideoID() { // Match different Bilibili URL patterns const patterns = [ /\/video\/([^/?#]+)/, // /video/BV1234567890 /\/BV([^/?#]+)/, // /BV1234567890 /bvid=([^&]+)/, // ?bvid=BV1234567890 ]; const url = window.location.href; for (const pattern of patterns) { const match = url.match(pattern); if (match) { let videoId = match[1]; // Ensure BV prefix if (!videoId.startsWith("BV")) { videoId = "BV" + videoId; } return videoId; } } return null; } // ============================================================================= // HASHING UTILITY // ============================================================================= /** * SHA-256 hash function using Web Crypto API * @param {string} value - String to hash * @returns {Promise<string>} Hash string in hexadecimal format */ async function sha256Hash(value) { try { // Check if crypto.subtle is available (required for hash functions) if (!crypto || !crypto.subtle) { console.error("[BSB] crypto.subtle is not available. Hash functions will not work."); return value; // Fallback to original value } const encoder = new TextEncoder(); const data = encoder.encode(value); const hashBuffer = await crypto.subtle.digest("SHA-256", data); const hashArray = Array.from(new Uint8Array(hashBuffer)); return hashArray.map((b) => b.toString(16).padStart(2, "0")).join(""); } catch (error) { console.error("[BSB] Error hashing value:", error); return value; // Fallback to original value } } /** * Get video ID hash for API requests * @param {string} videoID - Video ID to hash * @returns {Promise<string>} Hashed video ID */ async function getVideoIDHash(videoID) { return await sha256Hash(videoID); } // ============================================================================= // SEGMENT SKIPPING FUNCTIONALITY // ============================================================================= /** * Main class for handling segment detection and skipping * Manages video segment loading, detection, and automatic skipping */ class SegmentSkipper { constructor() { console.log("[BSB] SegmentSkipper instance created."); this.segments = []; // Array of skip segments for current video this.currentVideo = null; // Current video ID being processed this.videoElement = null; // Reference to HTML video element this.lastCheckedTime = 0; // Last time we checked for skipping (performance optimization) } /** * Load skip segments for a specific video from the API * @param {string} videoID - Bilibili video ID (BVID) */ async loadSegments(videoID) { if (!videoID) return; console.log(`[BSB] Loading segments for video: ${videoID}`); try { // Hash the video ID and get first 4 characters for API request const videoIDHash = await getVideoIDHash(videoID); const hashPrefix = videoIDHash.slice(0, 4); console.log(`[BSB] Using hash prefix: ${hashPrefix} for video: ${videoID}`); // Make API request to get skip segments using hash prefix const response = await makeRequest( "GET", `${Config.get("serverAddress")}/api/skipSegments/${hashPrefix}` ); if (response.ok) { const allSegments = await response.json(); console.log("[BSB] API response received:", allSegments); // Filter segments for this specific video ID this.segments = []; for (const segmentResponse of allSegments) { if (segmentResponse.videoID === videoID && segmentResponse.segments) { // Filter segments by enabled categories and action types const filteredSegments = segmentResponse.segments.filter((segment) => { const categorySettings = Config.get("categorySelections")[segment.category]; const enabledActionTypes = ["skip", "poi"]; // ActionType.Skip, ActionType.Poi return ( categorySettings && categorySettings.skip && enabledActionTypes.includes(segment.actionType) ); }); this.segments = filteredSegments; break; } } console.log("[BSB] Segments loaded and filtered:", this.segments); } else { console.error(`[BSB] Failed to load segments. Status: ${response.status}`); this.segments = []; } } catch (error) { console.error("[BSB] Error loading segments:", error); this.segments = []; } } /** * Check if current video time is within any skip segment * Called repeatedly during video playback via timeupdate event */ checkForSkip() { // Don't check if no video element or segments available if (!this.videoElement || this.segments.length === 0) return; const currentTime = this.videoElement.currentTime; // Performance optimization: only check every 0.5 seconds // This prevents excessive processing while maintaining accuracy if (Math.abs(currentTime - this.lastCheckedTime) < 0.5) return; this.lastCheckedTime = currentTime; // Check each segment to see if we should skip for (const segment of this.segments) { const [startTime, endTime] = segment.segment; const category = segment.category; // Check if user has enabled skipping for this category const categorySettings = Config.get("categorySelections")[category]; if (!categorySettings?.skip) continue; // Check if current playback time is within this segment if (currentTime >= startTime && currentTime < endTime) { console.log( `[BSB] Skipping ${category} segment: ${this.formatTime(startTime)} - ${this.formatTime( endTime )}` ); this.skipToTime(endTime); break; // Only skip one segment at a time } } } /** * Skip video to specified time * @param {number} time - Time in seconds to skip to */ skipToTime(time) { if (this.videoElement) { this.videoElement.currentTime = time; console.log(`[BSB] Skipped to: ${this.formatTime(time)}`); } } /** * Format time duration in MM:SS format * @param {number} seconds - Duration in seconds * @returns {string} Formatted time string */ formatTime(seconds) { const min = Math.floor(seconds / 60); const sec = Math.floor(seconds % 60); return `${min}:${sec.toString().padStart(2, "0")}`; } /** * Initialize the segment skipper * Sets up video element detection and event listeners */ init() { console.log("[BSB] Initializing segment skipper..."); // Function to check for video element periodically const checkForVideo = () => { // Try multiple video selectors to find the video element this.videoElement = document.querySelector("video") || document.querySelector(".bpx-player-video-area video") || document.querySelector(".bilibili-player video"); if (this.videoElement) { console.log("[BSB] Video element found, setting up event listeners."); // Remove existing event listeners to avoid duplicates this.videoElement.removeEventListener("timeupdate", this.checkForSkip); // Add timeupdate event listener to check for segments to skip this.videoElement.addEventListener("timeupdate", () => { this.checkForSkip(); }); // Load segments when video changes const videoID = getBilibiliVideoID(); if (videoID && videoID !== this.currentVideo) { this.currentVideo = videoID; this.loadSegments(videoID); } } else { // Video element not found, try again in 1 second setTimeout(checkForVideo, 1000); } }; // Start checking for video element checkForVideo(); } } // ============================================================================= // MAIN INITIALIZATION // ============================================================================= /** * Initialize the userscript * Main entry point that sets up all functionality */ function init() { console.log("[BSB] BilibiliSponsorBlock userscript starting..."); console.log("[BSB] Current URL:", window.location.href); console.log("[BSB] Configuration:", Config.config); // Extract video ID from current page const videoID = getBilibiliVideoID(); console.log(`[BSB] Video ID detected: ${videoID}`); // Test crypto availability if (!crypto || !crypto.subtle) { console.error("[BSB] WARNING: crypto.subtle not available. Userscript may not work correctly."); } else { console.log("[BSB] crypto.subtle available."); } // Initialize segment skipper const skipper = new SegmentSkipper(); skipper.init(); // Handle page navigation for Single Page Application (SPA) let currentURL = window.location.href; const handleNavigation = () => { if (window.location.href !== currentURL) { currentURL = window.location.href; console.log(`[BSB] Navigation detected. New URL: ${currentURL}`); // Reinitialize skipper after navigation setTimeout(() => { skipper.init(); }, 1000); } }; // Listen for popstate events (back/forward navigation) window.addEventListener("popstate", handleNavigation); // Check for URL changes periodically for SPA navigation setInterval(handleNavigation, 3000); } // ============================================================================= // USERSCRIPT STARTUP // ============================================================================= // Wait for DOM to be ready before initializing if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { // DOM is already ready, initialize immediately init(); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址