本文于 2025年7月4日 1:57 更新,注意查看最新内容
======2025.7.4 13:44 更新======
因为除了用B站学习以外,可能还会浏览一些其他视频,但其他视频并不需要跳转片头片尾。
所以,迭代这个版本,增加功能如下:
1、对指定BV号视频跳过片头片尾
2、可自定义每个视频的跳过时间
如果没有这两个需求,可以继续使用V1.2的版本。
PS:两个版本均兼容B站空降助手。
// ==UserScript== // [url=home.php?mod=space&uid=170990]@name[/url] B站指定视频片头片尾跳过 // [url=home.php?mod=space&uid=467642]@namespace[/url] [url=http://tampermonkey.net/]http://tampermonkey.net/[/url] // [url=home.php?mod=space&uid=1248337]@version[/url] 1.4 // @description 仅对指定BV号视频跳过片头片尾,可自定义每个视频的跳过时间 // [url=home.php?mod=space&uid=686208]@AuThor[/url] l9zp6 // [url=home.php?mod=space&uid=195849]@match[/url] [url=https://www.bilibili.com/video/]https://www.bilibili.com/video/[/url]* // @match [url=https://www.bilibili.com/bangumi/play/]https://www.bilibili.com/bangumi/play/[/url]* // [url=home.php?mod=space&uid=609072]@grant[/url] GM_getValue // @grant GM_setValue // @grant GM_addStyle // ==/UserScript== (function() { 'use strict'; // ========================== 配置区域 ========================== // 默认跳过时间(当未指定视频的自定义时间时使用) const DEFAULT_INTRO_SKIP_TIME = 0; // 默认不跳过片头 const DEFAULT_OUTRO_SKIP_TIME = 0; // 默认不跳过片尾 // 指定视频的自定义跳过时间(支持多个BV号) // 格式: 'BV号': { intro: 片头时间, outro: 片尾时间 } const CUSTOM_SKIP_TIMES = { 'BV14yzCYgE9Y': { intro: 10, outro: 5 }, // 示例1:自定义此BV号的跳过时间 'BV14yzCYgE9S': { intro: 5, outro: 3 }, // 示例2:添加更多BV号 // 可继续添加更多BV号配置... }; // ============================================================= // 存储状态信息 let lastVideoKey = ''; // 视频唯一标识 let lastUrl = ''; // 上次的完整URL let isWaitingForNextVideo = false; // 是否等待下一个视频 let lastProcessTime = 0; // 上次处理时间 let skipCooldown = false; // 跳过冷却标志 let forceProcess = false; // 强制处理标志 // 添加样式 GM_addStyle(` .skip-hint { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: rgba(0, 0, 0, 0.7); color: white; padding: 10px 20px; border-radius: 5px; font-size: 18px; z-index: 10000; opacity: 0; transition: opacity 0.3s; } .skip-hint.show { opacity: 1; } `); // 创建提示元素 function createHintElement() { const hint = document.createElement('div'); hint.className = 'skip-hint'; document.body.appendChild(hint); return hint; } // 获取播放器元素 function getPlayer() { return document.querySelector('bwp-video, video'); } // 获取播放器容器 function getPlayerContainer() { return document.querySelector('#bilibili-player, .bpx-player-container'); } // 显示提示 function showHint(text) { let hint = document.querySelector('.skip-hint'); if (!hint) { hint = createHintElement(); } hint.textContent = text; hint.classList.add('show'); // 获取播放器容器 const playerContainer = getPlayerContainer(); // 检查是否全屏 const isFullscreen = document.fullscreenElement || document.webkitFullscreenElement; if (playerContainer && !isFullscreen) { // 非全屏状态,将提示添加到播放器容器内 playerContainer.appendChild(hint); // 重置位置样式 hint.style.top = '50%'; hint.style.left = '50%'; hint.style.transform = 'translate(-50%, -50%)'; } else { // 全屏状态,提示保持在body内 document.body.appendChild(hint); } setTimeout(() => { hint.classList.remove('show'); }, 2000); // 提示显示2秒 } // 获取当前视频的BV号 function getCurrentBVId() { const pathMatch = location.pathname.match(/\/video\/(BV\w+)/); return pathMatch ? pathMatch[1] : ''; } // 生成视频唯一标识(只考虑BV号和p参数) function generateVideoKey() { const bvid = getCurrentBVId(); if (!bvid) return ''; const pMatch = location.search.match(/[?&]p=(\d+)/); const pValue = pMatch ? `_p${pMatch[1]}` : ''; return `${bvid}${pValue}`; } // 获取当前视频的跳过时间配置 function getSkipTimes() { const bvid = getCurrentBVId(); // 如果是指定视频,使用自定义时间,否则使用默认时间 if (CUSTOM_SKIP_TIMES[bvid]) { console.log(`使用自定义跳过时间 for ${bvid}:`, CUSTOM_SKIP_TIMES[bvid]); return CUSTOM_SKIP_TIMES[bvid]; } return { intro: DEFAULT_INTRO_SKIP_TIME, outro: DEFAULT_OUTRO_SKIP_TIME }; } // 检查视频是否在指定列表中 function isVideoIncluded() { const bvid = getCurrentBVId(); return !!CUSTOM_SKIP_TIMES[bvid]; } // 获取视频标题 function getVideoTitle() { const titleElement = document.querySelector('h1.title, .media-info-title'); return titleElement ? titleElement.textContent.trim() : ''; } // 检查是否是新视频 function isNewVideo() { const currentVideoKey = generateVideoKey(); const currentUrl = location.href; let isNew = currentVideoKey !== lastVideoKey; // 如果URL相同但需要强制处理 if (!isNew && currentUrl === lastUrl && forceProcess) { console.log('强制处理相同URL的视频'); isNew = true; forceProcess = false; } if (isNew) { console.log('检测到新视频:', currentVideoKey); lastVideoKey = currentVideoKey; lastUrl = currentUrl; } else if (currentUrl !== lastUrl) { console.log('URL参数变化但视频未变:', currentUrl); lastUrl = currentUrl; } return isNew; } // 获取并更新记忆进度 function processMemoryProgress(player) { // 如果不是指定视频,直接返回 if (!isVideoIncluded()) return; const videoKey = generateVideoKey(); if (!videoKey) return; const memoryTime = GM_getValue(`bilibili_memory_${videoKey}`, 0); const { intro } = getSkipTimes(); // 如果记忆进度在片头时间之后,不做处理 if (memoryTime > intro) { console.log(`检测到记忆进度:${memoryTime}秒,超过片头时间${intro}秒,不跳过`); return; } // 如果是新视频且进度为0,跳转到片头跳过时间 if (player.currentTime <= intro) { console.log(`跳转到片头跳过时间:${intro}秒`); player.currentTime = intro; showHint(`已跳过${intro}秒片头`); } } // 检查片尾并跳过 function checkAndSkipOutro(player, duration) { // 如果不是指定视频,直接返回 if (!isVideoIncluded()) return; // 防止无限循环跳转 if (skipCooldown) return; const { outro } = getSkipTimes(); const currentTime = player.currentTime; // 检查是否在片尾区域 if (duration - currentTime <= outro) { // 防止在短时间内重复跳转 const now = Date.now(); if (now - lastProcessTime < 2000) { console.log('检测到重复跳转尝试,跳过此次操作'); return; } lastProcessTime = now; // 设置冷却期 skipCooldown = true; setTimeout(() => { skipCooldown = false; }, 2000); // 跳转到离片尾0.5秒处,确保能触发下一集 player.currentTime = duration - 0.5; console.log(`已跳过${outro}秒片尾,跳转到${duration - 0.5}秒`); showHint(`已跳过${outro}秒片尾`); // 标记为等待下一个视频,并设置强制处理 isWaitingForNextVideo = true; forceProcess = true; } } // 检查是否应该处理新视频 function shouldProcessNewVideo(player) { if (!player) return false; // 如果正在等待下一个视频且播放器已加载新视频 if (isWaitingForNextVideo && player.currentTime < 5) { console.log('检测到自动连播的新视频'); isWaitingForNextVideo = false; return true; } // 检查是否是新视频 return isNewVideo(); } // 主处理函数 function processVideo() { const player = getPlayer(); if (!player) return; // 检查是否应该处理新视频 if (shouldProcessNewVideo(player)) { console.log('检测到需要处理的新视频:', getVideoTitle()); // 处理记忆进度 processMemoryProgress(player); } // 保存进度 const videoKey = generateVideoKey(); if (videoKey) { GM_setValue(`bilibili_memory_${videoKey}`, player.currentTime); } // 检查片尾 if (player.duration && player.duration > 0) { checkAndSkipOutro(player, player.duration); } } // 初始化 function init() { console.log('B站片头片尾跳过脚本已加载'); // 监听播放器状态变化 const player = getPlayer(); if (player) { player.addEventListener('loadedmetadata', () => { console.log('播放器加载了新的元数据'); processVideo(); }); player.addEventListener('timeupdate', () => { // 播放器时间更新时检查 processVideo(); }); } // 监听URL变化 new MutationObserver(() => { console.log('检测到URL变化'); setTimeout(() => { processVideo(); }, 300); }).observe(document, { childList: true, subtree: true, attributes: true, attributeFilter: ['href'] }); // 定时检查 setInterval(() => { const player = getPlayer(); if (player && !player.paused) { processVideo(); } }, 1000); // 初始处理 setTimeout(processVideo, 1000); } // 等待页面加载完成 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();
======2025.7.4 原文======
前言
众所周知,目前B站已经成为了最大的学习乐园之一,拥有海量的学习资源。
但在学习的过程中,很多的学习视频会考虑宣传等目的,在片头或者片尾加上与视频内容实质无关的片段,极其影响学习。
因为太久没写Javascript,以及不喜欢重复造轮子的想法,在尝试完市面上大部分的脚本之后。
我选择使用AI编写了脚本的大部分代码,目前已稳定支持全自动跳过固定片头片尾。
代码
// ==UserScript== // @name B站片头片尾跳过 // @namespace http://tampermonkey.net/ // @version 1.2 // @description 自动跳过B站前10秒片头和后5秒片尾,支持任意URL参数 // @author l9zp6 // @match https://www.bilibili.com/video/* // @match https://www.bilibili.com/bangumi/play/* // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // ==/UserScript== (function() { 'use strict'; // 配置参数 const INTRO_SKIP_TIME = 10; // 片头跳过时间(秒) const OUTRO_SKIP_TIME = 5; // 片尾跳过时间(秒) const SHOW_TIME = 2; // 提示显示时间(秒) // 存储上一次的视频信息 let lastVideoKey = ''; // 视频唯一标识(忽略无关参数) let lastUrl = ''; // 上次的完整URL let isWaitingForNextVideo = false; // 是否正在等待下一个视频 let lastProcessTime = 0; // 上次处理时间 let skipCooldown = false; // 跳过冷却标志 let forceProcess = false; // 强制处理标志 // 添加样式 GM_addStyle(` .skip-hint { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: rgba(0, 0, 0, 0.7); color: white; padding: 10px 20px; border-radius: 5px; font-size: 18px; z-index: 10000; opacity: 0; transition: opacity 0.3s; } .skip-hint.show { opacity: 1; } `); // 创建提示元素 function createHintElement() { const hint = document.createElement('div'); hint.className = 'skip-hint'; document.body.appendChild(hint); return hint; } // 获取播放器元素 function getPlayer() { return document.querySelector('bwp-video, video'); } // 获取播放器容器 function getPlayerContainer() { return document.querySelector('#bilibili-player, .bpx-player-container'); } // 显示提示 function showHint(text) { let hint = document.querySelector('.skip-hint'); if (!hint) { hint = createHintElement(); } hint.textContent = text; hint.classList.add('show'); // 获取播放器容器 const playerContainer = getPlayerContainer(); // 检查是否全屏 const isFullscreen = document.fullscreenElement || document.webkitFullscreenElement; if (playerContainer && !isFullscreen) { // 非全屏状态,将提示添加到播放器容器内 playerContainer.appendChild(hint); // 重置位置样式 hint.style.top = '50%'; hint.style.left = '50%'; hint.style.transform = 'translate(-50%, -50%)'; } else { // 全屏状态,提示保持在body内 document.body.appendChild(hint); } setTimeout(() => { hint.classList.remove('show'); }, SHOW_TIME * 1000); } // 生成视频唯一标识(只考虑BV号和p参数) function generateVideoKey() { const pathMatch = location.pathname.match(/\/video\/(BV\w+)/); if (!pathMatch) return ''; const bvid = pathMatch[1]; const pMatch = location.search.match(/[?&]p=(\d+)/); const pValue = pMatch ? `_p${pMatch[1]}` : ''; return `${bvid}${pValue}`; } // 获取视频标题 function getVideoTitle() { const titleElement = document.querySelector('h1.title, .media-info-title'); return titleElement ? titleElement.textContent.trim() : ''; } // 检查是否是新视频 function isNewVideo() { const currentVideoKey = generateVideoKey(); const currentUrl = location.href; let isNew = currentVideoKey !== lastVideoKey; // 如果URL相同但需要强制处理 if (!isNew && currentUrl === lastUrl && forceProcess) { console.log('强制处理相同URL的视频'); isNew = true; forceProcess = false; } if (isNew) { console.log('检测到新视频:', currentVideoKey); lastVideoKey = currentVideoKey; lastUrl = currentUrl; } else if (currentUrl !== lastUrl) { console.log('URL参数变化但视频未变:', currentUrl); lastUrl = currentUrl; } return isNew; } // 获取并更新记忆进度 function processMemoryProgress(player) { const videoKey = generateVideoKey(); if (!videoKey) return; const memoryTime = GM_getValue(`bilibili_memory_${videoKey}`, 0); // 如果记忆进度在片头时间之后,不做处理 if (memoryTime > INTRO_SKIP_TIME) { console.log(`检测到记忆进度:${memoryTime}秒,超过片头时间,不跳过`); return; } // 如果是新视频且进度为0,跳转到片头跳过时间 if (player.currentTime <= INTRO_SKIP_TIME) { console.log(`跳转到片头跳过时间:${INTRO_SKIP_TIME}秒`); player.currentTime = INTRO_SKIP_TIME; showHint(`已跳过${INTRO_SKIP_TIME}秒片头`); } } // 检查片尾并跳过 function checkAndSkipOutro(player, duration) { // 防止无限循环跳转 if (skipCooldown) return; const currentTime = player.currentTime; // 检查是否在片尾区域 if (duration - currentTime <= OUTRO_SKIP_TIME) { // 防止在短时间内重复跳转 const now = Date.now(); if (now - lastProcessTime < 2000) { console.log('检测到重复跳转尝试,跳过此次操作'); return; } lastProcessTime = now; // 设置冷却期 skipCooldown = true; setTimeout(() => { skipCooldown = false; }, 2000); // 跳转到离片尾0.5秒处,确保能触发下一集 player.currentTime = duration - 0.5; console.log(`已跳过${OUTRO_SKIP_TIME}秒片尾,跳转到${duration - 0.5}秒`); showHint(`已跳过${OUTRO_SKIP_TIME}秒片尾`); // 标记为等待下一个视频,并设置强制处理 isWaitingForNextVideo = true; forceProcess = true; } } // 检查是否应该处理新视频 function shouldProcessNewVideo(player) { if (!player) return false; // 如果正在等待下一个视频且播放器已加载新视频 if (isWaitingForNextVideo && player.currentTime < 5) { console.log('检测到自动连播的新视频'); isWaitingForNextVideo = false; return true; } // 检查是否是新视频 return isNewVideo(); } // 主处理函数 function processVideo() { const player = getPlayer(); if (!player) return; // 检查是否应该处理新视频 if (shouldProcessNewVideo(player)) { console.log('检测到需要处理的新视频:', getVideoTitle()); // 处理记忆进度 processMemoryProgress(player); } // 保存进度 const videoKey = generateVideoKey(); if (videoKey) { GM_setValue(`bilibili_memory_${videoKey}`, player.currentTime); } // 检查片尾 if (player.duration && player.duration > OUTRO_SKIP_TIME) { checkAndSkipOutro(player, player.duration); } } // 初始化 function init() { console.log('B站片头片尾跳过脚本已加载'); // 监听播放器状态变化 const player = getPlayer(); if (player) { player.addEventListener('loadedmetadata', () => { console.log('播放器加载了新的元数据'); processVideo(); }); player.addEventListener('timeupdate', () => { // 播放器时间更新时检查 processVideo(); }); } // 监听URL变化 new MutationObserver(() => { console.log('检测到URL变化'); setTimeout(() => { processVideo(); }, 300); }).observe(document, { childList: true, subtree: true, attributes: true, attributeFilter: ['href'] }); // 定时检查 setInterval(() => { const player = getPlayer(); if (player && !player.paused) { processVideo(); } }, 1000); // 初始处理 setTimeout(processVideo, 1000); } // 等待页面加载完成 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();
说明
本脚本是专门为了学习而编写的脚本,也只有在学习时使用该脚本才有可能有相应的效果,严禁将此脚本内容用于任何商业或非法用途,对于因违反此说明而产生的任何法律后果,用户需自行承担全部责任。
Comments | NOTHING