// ==UserScript== // @name 哔哩哔哩-随机列表 // @namespace http://tampermonkey.net/ // @version 1.0 // @description 为哔哩哔哩视频选集添加随机播放按钮,支持随机选择上一个/下一个视频 // @author xujinkai // @license MIT // @match *://*.bilibili.com/video/* // @icon https://i0.hdslb.com/bfs/static/jinkela/long/images/favicon.ico // @grant none // @downloadURL none // ==/UserScript== ;(() => { //=========================================== // 配置区域 - 可根据需要修改 //=========================================== // 键盘快捷键配置 const KEY_CONFIG = { TOGGLE_RANDOM: "s", // 切换随机播放模式 PLAY_RANDOM: "n", // 手动触发随机播放 PREV_VIDEO: "[", // 上一个视频 NEXT_VIDEO: "]", // 下一个视频 } // 选择器配置 const SELECTORS = { // 播放器相关 VIDEO: [ ".bilibili-player-video video", // 标准播放器 ".bpx-player-video-wrap video", // 新版播放器 "video", // 通用选择器 ], // 控制区域 CONTROL_AREAS: [".bilibili-player-video-control", ".bpx-player-control-wrap"], // 播放器区域 PLAYER_AREAS: [".bilibili-player-video-wrap", ".bpx-player-video-area"], // 上一个/下一个按钮 PREV_NEXT_BUTTONS: [ // 旧版播放器 ".bilibili-player-video-btn-prev", ".bilibili-player-video-btn-next", // 新版播放器 ".bpx-player-ctrl-prev", ".bpx-player-ctrl-next", ], // 自动连播按钮 AUTO_PLAY: ".auto-play", // 播放列表项 PLAYLIST_ITEM: ".video-pod__item", // 活跃的播放列表项 ACTIVE_PLAYLIST_ITEM: ".video-pod__item.active", } // 样式配置 const STYLES = { BILIBILI_BLUE: "#00a1d6", BUTTON_MARGIN: "0 8px 0 0", // 按钮右侧间距 } // 时间配置(毫秒) const TIMING = { BUTTON_CHECK_INTERVAL: 1000, // 按钮检查间隔 INITIAL_DELAY: 1500, // 初始化延迟 PLAY_RANDOM_DELAY: 500, // 随机播放延迟 BUTTON_BIND_RETRY: 2000, // 按钮绑定重试延迟 } //=========================================== // 全局变量 //=========================================== let shuffleButtonAdded = false let buttonCheckInterval = null let isRandomPlayEnabled = false // 随机播放队列相关 let originalPlaylist = [] // 原始播放列表 let shuffledPlaylist = [] // 打乱后的播放列表 let currentPlayIndex = -1 // 当前播放索引 //=========================================== // 主要功能实现 //=========================================== /** * 初始化脚本 */ function init() { // 添加键盘快捷键监听 window.addEventListener("keydown", handleKeyDown) // 添加媒体按键监听 setupMediaKeysListener() // 使用间隔检查按钮是否存在 buttonCheckInterval = setInterval(checkAndAddButton, TIMING.BUTTON_CHECK_INTERVAL) // 初始尝试添加按钮 setTimeout(checkAndAddButton, TIMING.INITIAL_DELAY) } /** * 检查并添加随机播放按钮 */ function checkAndAddButton() { // 检查是否已经添加了随机按钮,避免重复添加 if (document.querySelector(".shuffle-btn")) { return } // 查找自动连播按钮容器 const autoPlayContainer = document.querySelector(SELECTORS.AUTO_PLAY) if (autoPlayContainer) { createShuffleButton(autoPlayContainer) shuffleButtonAdded = true console.log("Bilibili Video Playlist Shuffler button added") // 成功添加按钮后清除检查间隔 if (buttonCheckInterval) { clearInterval(buttonCheckInterval) buttonCheckInterval = null } // 设置视频播放结束监听 setupVideoEndListener() // 设置上一个/下一个按钮监听 setupPrevNextButtonsListener() // 初始化播放列表 initializePlaylist() } } /** * 创建随机播放按钮 * @param {HTMLElement} autoPlayContainer - 自动连播按钮容器 */ function createShuffleButton(autoPlayContainer) { // 克隆自动连播按钮作为模板 const shuffleContainer = autoPlayContainer.cloneNode(true) shuffleContainer.className = "shuffle-btn auto-play" // 保持相同的样式类 // 添加右侧间距 shuffleContainer.style.margin = STYLES.BUTTON_MARGIN // 修改文本和图标 const textElement = shuffleContainer.querySelector(".txt") if (textElement) { textElement.textContent = "随机播放" } // 替换图标为随机播放图标 const iconElement = shuffleContainer.querySelector("svg") if (iconElement) { // 清除原有的路径 while (iconElement.firstChild) { iconElement.removeChild(iconElement.firstChild) } // 添加随机图标的路径 const iconPath = document.createElementNS("http://www.w3.org/2000/svg", "path") iconPath.setAttribute( "d", "M10.59 9.17L5.41 4 4 5.41l5.17 5.17 1.42-1.41zM14.5 4l2.04 2.04L4 18.59 5.41 20 17.96 7.46 20 9.5V4h-5.5zm0.33 9.41l-1.41 1.41 3.13 3.13L14.5 20H20v-5.5l-2.04 2.04-3.13-3.13z", ) iconElement.appendChild(iconPath) } // 确保开关按钮的样式正确 const switchElement = shuffleContainer.querySelector(".switch-btn") if (switchElement) { // 确保开关按钮初始状态为off switchElement.className = "switch-btn off" } // 移除原有的点击事件 const newShuffleContainer = shuffleContainer.cloneNode(true) // 将随机按钮添加到自动连播按钮旁边 const rightContainer = autoPlayContainer.closest(".right") if (rightContainer) { rightContainer.insertBefore(newShuffleContainer, autoPlayContainer) // 添加点击事件 newShuffleContainer.addEventListener("click", toggleRandomPlay) } } /** * 初始化播放列表 */ function initializePlaylist() { // 获取所有播放列表项 const playlistItems = Array.from(document.querySelectorAll(SELECTORS.PLAYLIST_ITEM)) if (playlistItems.length <= 1) return // 保存原始播放列表 originalPlaylist = playlistItems // 初始化打乱的播放列表(初始为空,会在开启随机播放时生成) shuffledPlaylist = [] // 找到当前播放的视频索引 const activeItem = document.querySelector(SELECTORS.ACTIVE_PLAYLIST_ITEM) currentPlayIndex = activeItem ? playlistItems.indexOf(activeItem) : 0 console.log(`Playlist initialized with ${playlistItems.length} items, current index: ${currentPlayIndex}`) } /** * 生成随机播放队列 */ function generateShuffledPlaylist() { // 确保原始播放列表已初始化 if (originalPlaylist.length === 0) { initializePlaylist() if (originalPlaylist.length === 0) return } // 创建索引数组 const indices = Array.from({ length: originalPlaylist.length }, (_, i) => i) // 获取当前播放的视频索引 const activeItem = document.querySelector(SELECTORS.ACTIVE_PLAYLIST_ITEM) const currentIndex = activeItem ? originalPlaylist.indexOf(activeItem) : -1 // 从索引数组中移除当前播放的视频索引 if (currentIndex !== -1) { indices.splice(indices.indexOf(currentIndex), 1) } // Fisher-Yates 洗牌算法打乱剩余索引 for (let i = indices.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)) ;[indices[i], indices[j]] = [indices[j], indices[i]] } // 如果当前有播放的视频,将其放在队列最前面 if (currentIndex !== -1) { indices.unshift(currentIndex) } // 保存打��后的播放列表 shuffledPlaylist = indices.map((index) => originalPlaylist[index]) currentPlayIndex = 0 console.log("Shuffled playlist generated:", indices) } /** * 切换随机播放状态 */ function toggleRandomPlay() { isRandomPlayEnabled = !isRandomPlayEnabled // 更新按钮状态 const shuffleBtn = document.querySelector(".shuffle-btn") if (shuffleBtn) { // 更新开关按钮的状态 const switchElement = shuffleBtn.querySelector(".switch-btn") if (switchElement) { if (isRandomPlayEnabled) { switchElement.className = "switch-btn on" // 生成随机播放队列 generateShuffledPlaylist() } else { switchElement.className = "switch-btn off" // 清空随机播放队列 shuffledPlaylist = [] } } else { // 如果没有开关元素,使用颜色来表示状态 if (isRandomPlayEnabled) { shuffleBtn.style.color = STYLES.BILIBILI_BLUE // 生成随机播放队列 generateShuffledPlaylist() } else { shuffleBtn.style.color = "" // 清空随机播放队列 shuffledPlaylist = [] } } } // 不再显示切换提示 console.log(isRandomPlayEnabled ? "已开启随机播放模式" : "已关闭随机播放模式") } /** * 设置视频播放结束监听 */ function setupVideoEndListener() { // 监听视频元素变化 const observer = new MutationObserver(() => { const video = findVideoElement() if (video && !video._hasEndedListener) { video._hasEndedListener = true // 监听视频结束事件 video.addEventListener("ended", () => { console.log("Video ended, checking random play status") if (isRandomPlayEnabled) { setTimeout(playNextInQueue, TIMING.PLAY_RANDOM_DELAY) } }) console.log("Video end listener added") observer.disconnect() // 找到并设置监听器后停止观察 } }) // 开始观察播放器区域 const playerArea = findFirstElement(SELECTORS.PLAYER_AREAS) || document.body observer.observe(playerArea, { childList: true, subtree: true }) } /** * 设置媒体按键监听 */ function setupMediaKeysListener() { // 尝试使用 MediaSession API if ("mediaSession" in navigator) { navigator.mediaSession.setActionHandler("previoustrack", () => { console.log("Media key: previous track") if (isRandomPlayEnabled) { playPrevInQueue() return } }) navigator.mediaSession.setActionHandler("nexttrack", () => { console.log("Media key: next track") if (isRandomPlayEnabled) { playNextInQueue() return } }) console.log("Media session handlers registered") } // 监听键盘媒体按键事件 document.addEventListener( "keyup", (e) => { // 媒体按键通常会触发特殊的keyCode if (e.key === "MediaTrackPrevious" || e.key === "MediaTrackNext") { console.log(`Media key pressed: ${e.key}`) if (isRandomPlayEnabled) { e.preventDefault() e.stopPropagation() if (e.key === "MediaTrackPrevious") { playPrevInQueue() } else if (e.key === "MediaTrackNext") { playNextInQueue() } return false } } }, true, ) // 使用捕获阶段 } /** * 设置上一个/下一个按钮监听 */ function setupPrevNextButtonsListener() { // 监听按钮变化 const observer = new MutationObserver(() => { // 遍历所有可能的按钮选择器 SELECTORS.PREV_NEXT_BUTTONS.forEach((selector) => { const button = document.querySelector(selector) if (button && !button._hasRandomListener) { button._hasRandomListener = true // 保存原始的点击处理函数 const originalClickHandler = button.onclick // 替换为我们的处理函数 button.onclick = function (e) { if (isRandomPlayEnabled) { e.preventDefault() e.stopPropagation() // 根据按钮类型决定播放上一个还是下一个 if (selector.includes("prev")) { playPrevInQueue() } else { playNextInQueue() } return false } else if (originalClickHandler) { return originalClickHandler.call(this, e) } } // 如果是div元素,还需要监听click事件 if (button.tagName.toLowerCase() === "div") { button.addEventListener("click", (e) => { if (isRandomPlayEnabled) { e.preventDefault() e.stopPropagation() // 根据按钮类型决定播放上一个还是下一个 if (selector.includes("prev")) { playPrevInQueue() } else { playNextInQueue() } return false } }) } console.log(`Button listener added for ${selector}`) } }) }) // 观察播放器控制区域 const controlAreas = findElements(SELECTORS.CONTROL_AREAS) controlAreas.forEach((area) => { if (area) observer.observe(area, { childList: true, subtree: true }) }) // 如果没有找到控制区域,观察整个播放器 if (controlAreas.length === 0) { const playerArea = findFirstElement(SELECTORS.PLAYER_AREAS) || document.body observer.observe(playerArea, { childList: true, subtree: true }) } // 直接尝试一次绑定 setTimeout(() => { observer.disconnect() observer.observe(document.body, { childList: true, subtree: true }) }, TIMING.BUTTON_BIND_RETRY) } /** * 播放队列中的下一个视频 */ function playNextInQueue() { if (!isRandomPlayEnabled || shuffledPlaylist.length === 0) { // 如果随机播放未启用或队列为空,则随机选择一个视频播放 playRandomVideo() return } // 移动到下一个索引 currentPlayIndex = (currentPlayIndex + 1) % shuffledPlaylist.length // 播放当前索引的视频 const videoToPlay = shuffledPlaylist[currentPlayIndex] if (videoToPlay) { playVideo(videoToPlay) } else { // 如果出现问题,重新生成队列并播放 generateShuffledPlaylist() playNextInQueue() } } /** * 播放队列中的上一个视频 */ function playPrevInQueue() { if (!isRandomPlayEnabled || shuffledPlaylist.length === 0) { // 如果随机播放未启用或队列为空,则随机选择一个视频播放 playRandomVideo() return } // 移动到上一个索引 currentPlayIndex = (currentPlayIndex - 1 + shuffledPlaylist.length) % shuffledPlaylist.length // 播放当前索引的视频 const videoToPlay = shuffledPlaylist[currentPlayIndex] if (videoToPlay) { playVideo(videoToPlay) } else { // 如果出现问题,重新生成队列并播放 generateShuffledPlaylist() playPrevInQueue() } } /** * 随机播放一个视频(不使用队列,完全随机) */ function playRandomVideo() { const playlistItems = Array.from(document.querySelectorAll(SELECTORS.PLAYLIST_ITEM)) if (playlistItems.length <= 1) return // 获取当前活跃的视频 const activeItem = document.querySelector(SELECTORS.ACTIVE_PLAYLIST_ITEM) const currentIndex = activeItem ? playlistItems.indexOf(activeItem) : -1 // 随机选择一个不同的索引 let randomIndex do { randomIndex = Math.floor(Math.random() * playlistItems.length) } while (randomIndex === currentIndex && playlistItems.length > 1) console.log(`Playing random video: ${randomIndex + 1}/${playlistItems.length}`) // 播放随机选择的视频 playVideo(playlistItems[randomIndex]) } /** * 播放指定的视频 * @param {HTMLElement} videoElement - 要播放的视频元素 */ function playVideo(videoElement) { if (!videoElement) return // 尝试获取链接并导航 const link = videoElement.querySelector("a") if (link && link.href) { console.log(`Navigating to: ${link.href}`) window.location.href = link.href } else { // 如果无法获取链接,尝试模拟点击 console.log("Simulating click on playlist item") videoElement.click() } } /** * 播放上一个视频(如果随机模式开启则使用队列) */ function playPrevVideo() { if (isRandomPlayEnabled) { playPrevInQueue() } else { // 尝试点击上一个按钮 const prevButton = findFirstElement(SELECTORS.PREV_NEXT_BUTTONS.filter((s) => s.includes("prev"))) if (prevButton) { prevButton.click() } } } /** * 播放下一个视频(如果随机模式开启则使用队列) */ function playNextVideo() { if (isRandomPlayEnabled) { playNextInQueue() } else { // 尝试点击下一个按钮 const nextButton = findFirstElement(SELECTORS.PREV_NEXT_BUTTONS.filter((s) => s.includes("next"))) if (nextButton) { nextButton.click() } } } /** * 处理键盘快捷键 * @param {KeyboardEvent} event - 键盘事件 */ function handleKeyDown(event) { // 忽略带有修饰键的按键 if (event.ctrlKey || event.altKey || event.metaKey) { return } const key = event.key.toLowerCase() // 根据按键执行相应操作 switch (key) { case KEY_CONFIG.TOGGLE_RANDOM: // 切换随机播放模式 const shuffleBtn = document.querySelector(".shuffle-btn") if (shuffleBtn) { shuffleBtn.click() } break case KEY_CONFIG.PLAY_RANDOM: // 手动触发随机播放 if (isRandomPlayEnabled) { playNextInQueue() } break case KEY_CONFIG.PREV_VIDEO: // 播放上一个视频 playPrevVideo() break case KEY_CONFIG.NEXT_VIDEO: // 播放下一个视频 playNextVideo() break } } //=========================================== // 辅助函数 //=========================================== /** * 查找视频元素 * @returns {HTMLElement|null} 找到的视频元素或null */ function findVideoElement() { return findFirstElement(SELECTORS.VIDEO) } /** * 从选择器数组中查找第一个匹配的元素 * @param {string[]} selectors - 选择器数组 * @returns {HTMLElement|null} 找到的元素或null */ function findFirstElement(selectors) { for (const selector of selectors) { const element = document.querySelector(selector) if (element) return element } return null } /** * 从选择器数组中查找所有匹配的元素 * @param {string[]} selectors - 选择器数组 * @returns {HTMLElement[]} 找到的元素数组 */ function findElements(selectors) { const elements = [] for (const selector of selectors) { const found = document.querySelector(selector) if (found) elements.push(found) } return elements } /** * 显示提示信息 * @param {string} message - 要显示的消息 */ function showNotification(message) { // 检查是否已存在通知,如果存在则移除 const existingNotification = document.querySelector(".shuffle-notification") if (existingNotification) { document.body.removeChild(existingNotification) } const notification = document.createElement("div") notification.className = "shuffle-notification" notification.textContent = message notification.style.position = "fixed" notification.style.top = "50%" notification.style.left = "50%" notification.style.transform = "translate(-50%, -50%)" notification.style.padding = "10px 20px" notification.style.backgroundColor = "rgba(0, 0, 0, 0.7)" notification.style.color = "white" notification.style.borderRadius = "4px" notification.style.zIndex = "9999" document.body.appendChild(notification) // 淡出动画 setTimeout(() => { notification.style.transition = "opacity 1s ease" notification.style.opacity = "0" setTimeout(() => { if (document.body.contains(notification)) { document.body.removeChild(notification) } }, 1000) }, 1500) } //=========================================== // 初始化 //=========================================== // 页面加载完成后初始化 if (document.readyState === "complete") { init() } else { window.addEventListener("load", init) } })()