Last active
July 18, 2026 04:04
-
-
Save xebecnan/466f2d8ca0b25367d64ac43d3268772a to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name B站列表随机播放 | |
| // @namespace http://tampermonkey.net/ | |
| // @version 1.0.2 | |
| // @description 在B站视频页右下角提供一个可展开的随机播放按钮,视频结束后从当前页面可识别列表中随机跳转下一项 | |
| // @author ChatGPT | |
| // @match https://www.bilibili.com/video/* | |
| // @match https://www.bilibili.com/list/* | |
| // @grant none | |
| // @run-at document-idle | |
| // @license MIT | |
| // @downloadURL https://update.greasyfork.org/scripts/573854/B%E7%AB%99%E5%88%97%E8%A1%A8%E9%9A%8F%E6%9C%BA%E6%92%AD%E6%94%BE.user.js | |
| // @updateURL https://update.greasyfork.org/scripts/573854/B%E7%AB%99%E5%88%97%E8%A1%A8%E9%9A%8F%E6%9C%BA%E6%92%AD%E6%94%BE.meta.js | |
| // ==/UserScript== | |
| (function () { | |
| 'use strict'; | |
| const STORAGE_KEY_ENABLED = '__bili_random_playlist_enabled__'; | |
| const STORAGE_KEY_HISTORY = '__bili_random_playlist_history__'; | |
| const STORAGE_KEY_PANEL_OPEN = '__bili_random_playlist_panel_open__'; | |
| const STORAGE_KEY_WEB_FULLSCREEN = '__bili_random_playlist_web_fullscreen__'; | |
| const MAX_HISTORY = 50; | |
| const ROOT_ID = 'bili-random-play-root'; | |
| const STYLE_ID = 'bili-random-play-style'; | |
| function log(...args) { | |
| console.log('[BiliRandomSimple]', ...args); | |
| } | |
| function getEnabled() { | |
| return localStorage.getItem(STORAGE_KEY_ENABLED) === '1'; | |
| } | |
| function setEnabled(val) { | |
| localStorage.setItem(STORAGE_KEY_ENABLED, val ? '1' : '0'); | |
| } | |
| function getPanelOpen() { | |
| return localStorage.getItem(STORAGE_KEY_PANEL_OPEN) === '1'; | |
| } | |
| function setPanelOpen(val) { | |
| localStorage.setItem(STORAGE_KEY_PANEL_OPEN, val ? '1' : '0'); | |
| } | |
| function getWebFullscreenEnabled() { | |
| return localStorage.getItem(STORAGE_KEY_WEB_FULLSCREEN) === '1'; | |
| } | |
| function setWebFullscreenEnabled(val) { | |
| localStorage.setItem(STORAGE_KEY_WEB_FULLSCREEN, val ? '1' : '0'); | |
| } | |
| function getHistory() { | |
| try { | |
| return JSON.parse(localStorage.getItem(STORAGE_KEY_HISTORY) || '[]'); | |
| } catch { | |
| return []; | |
| } | |
| } | |
| function setHistory(arr) { | |
| localStorage.setItem(STORAGE_KEY_HISTORY, JSON.stringify(arr.slice(-MAX_HISTORY))); | |
| } | |
| function getCurrentVideoId() { | |
| const match = location.pathname.match(/\/video\/(BV[\w]+)/i); | |
| return match ? match[1] : null; | |
| } | |
| function normalizeUrl(url) { | |
| try { | |
| const u = new URL(url, location.origin); | |
| return u.origin + u.pathname + u.search; | |
| } catch { | |
| return url; | |
| } | |
| } | |
| function extractBvid(url) { | |
| const match = String(url).match(/\/video\/(BV[\w]+)/i); | |
| return match ? match[1] : null; | |
| } | |
| function getPlaylistAnchors() { | |
| const selectors = [ | |
| 'a[href*="/video/BV"]', | |
| '.video-pod__list a[href*="/video/BV"]', | |
| '.base-video-sections-v1 a[href*="/video/BV"]', | |
| '.next-play-list a[href*="/video/BV"]', | |
| '.video-section-list a[href*="/video/BV"]', | |
| '.pod-item a[href*="/video/BV"]', | |
| '.list-box a[href*="/video/BV"]' | |
| ]; | |
| const found = new Map(); | |
| for (const selector of selectors) { | |
| document.querySelectorAll(selector).forEach((a) => { | |
| const href = a.href || a.getAttribute('href'); | |
| if (!href || !href.includes('/video/')) return; | |
| const fullUrl = normalizeUrl(href); | |
| const bvid = extractBvid(fullUrl); | |
| if (!bvid) return; | |
| if (!found.has(fullUrl)) { | |
| found.set(fullUrl, { | |
| url: fullUrl, | |
| bvid, | |
| title: (a.textContent || '').trim() | |
| }); | |
| } | |
| }); | |
| } | |
| return Array.from(found.values()); | |
| } | |
| function pickRandomNext(items) { | |
| if (!items.length) return null; | |
| const currentBvid = getCurrentVideoId(); | |
| const history = getHistory(); | |
| let candidates = items.filter(item => item.bvid !== currentBvid); | |
| if (!candidates.length) return null; | |
| let fresh = candidates.filter(item => !history.includes(item.bvid)); | |
| if (!fresh.length) { | |
| setHistory(currentBvid ? [currentBvid] : []); | |
| fresh = candidates; | |
| } | |
| const randomIndex = Math.floor(Math.random() * fresh.length); | |
| return fresh[randomIndex]; | |
| } | |
| function jumpToVideo(item) { | |
| if (!item || !item.url) return; | |
| log('跳转到随机视频:', item); | |
| location.href = item.url; | |
| } | |
| function triggerWebFullscreen() { | |
| // 如果用户没开启此功能,直接返回 | |
| if (!getWebFullscreenEnabled()) return; | |
| // 查找 B站播放器的“网页全屏”按钮 | |
| // B站新版播放器通常使用 .bpx-player-ctrl-web-btn 类名 | |
| // 也可以通过 title="网页全屏" 或 aria-label 来查找作为后备 | |
| const webFsBtn = document.querySelector('.bpx-player-ctrl-web-btn') | |
| || document.querySelector('[title="网页全屏"]'); | |
| if (webFsBtn) { | |
| // 检查当前是否已经处于网页全屏状态 | |
| // B站播放器容器通常会有 .bpx-state-web-fullscreen 类 | |
| const playerContainer = document.querySelector('.bpx-player-container') || document.querySelector('#bilibili-player'); | |
| const isNowWebFs = playerContainer && playerContainer.classList.contains('bpx-state-web-fullscreen'); | |
| // 如果当前不是网页全屏,则点击 | |
| if (!isNowWebFs) { | |
| log('检测到开启指令,正在进入网页全屏...'); | |
| webFsBtn.click(); | |
| } else { | |
| log('当前已是网页全屏模式'); | |
| } | |
| } else { | |
| log('未找到网页全屏按钮,可能播放器未加载或结构变更'); | |
| } | |
| } | |
| function markCurrentPlayed() { | |
| const currentBvid = getCurrentVideoId(); | |
| if (!currentBvid) return; | |
| const history = getHistory(); | |
| if (!history.includes(currentBvid)) { | |
| history.push(currentBvid); | |
| setHistory(history); | |
| } | |
| } | |
| function updateToggleButtonUI() { | |
| const toggleBtn = document.getElementById('bili-random-toggle-btn'); | |
| if (!toggleBtn) return; | |
| const enabled = getEnabled(); | |
| toggleBtn.textContent = enabled ? '随机播放:开' : '随机播放:关'; | |
| toggleBtn.style.background = enabled ? '#00aeec' : '#666'; | |
| } | |
| function updatePanelUI() { | |
| const root = document.getElementById(ROOT_ID); | |
| const miniBtn = document.getElementById('bili-random-mini-btn'); | |
| const panel = document.getElementById('bili-random-panel'); | |
| if (!root || !miniBtn || !panel) return; | |
| const open = getPanelOpen(); | |
| panel.style.display = open ? 'flex' : 'none'; | |
| miniBtn.textContent = open ? '收起' : '随机'; | |
| updateToggleButtonUI(); | |
| } | |
| function handleVideoEnded() { | |
| if (!getEnabled()) return; | |
| markCurrentPlayed(); | |
| const items = getPlaylistAnchors(); | |
| log('当前识别到播放列表项目数:', items.length); | |
| if (items.length <= 1) { | |
| log('未识别到有效播放列表,无法随机切换'); | |
| return; | |
| } | |
| const next = pickRandomNext(items); | |
| if (!next) { | |
| log('没有可跳转的随机视频'); | |
| return; | |
| } | |
| setTimeout(() => { | |
| jumpToVideo(next); | |
| }, 800); | |
| } | |
| function bindVideoEnded(video) { | |
| if (!video || video.dataset.randomBound === '1') return; | |
| video.dataset.randomBound = '1'; | |
| video.addEventListener('ended', handleVideoEnded); | |
| log('已绑定 ended 事件'); | |
| } | |
| function observeVideo() { | |
| let lastVideo = null; | |
| const timer = setInterval(() => { | |
| const video = document.querySelector('video'); | |
| if (video && video !== lastVideo) { | |
| lastVideo = video; | |
| bindVideoEnded(video); | |
| // 【新增】当检测到 video 标签变化(意味着新视频加载),尝试触发全屏 | |
| triggerWebFullscreen(); | |
| } | |
| }, 1500); | |
| window.addEventListener('beforeunload', () => { | |
| clearInterval(timer); | |
| }); | |
| } | |
| function injectStyle() { | |
| if (document.getElementById(STYLE_ID)) return; | |
| const style = document.createElement('style'); | |
| style.id = STYLE_ID; | |
| style.textContent = ` | |
| #${ROOT_ID} { | |
| position: fixed; | |
| right: 20px; | |
| bottom: 20px; | |
| z-index: 999999; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: flex-end; | |
| gap: 10px; | |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; | |
| } | |
| #${ROOT_ID} #bili-random-mini-btn { | |
| width: 56px; | |
| height: 56px; | |
| border: none; | |
| border-radius: 50%; | |
| background: #00aeec; | |
| color: #fff; | |
| font-size: 14px; | |
| cursor: pointer; | |
| box-shadow: 0 4px 16px rgba(0,0,0,.2); | |
| } | |
| #${ROOT_ID} #bili-random-mini-btn:hover { | |
| opacity: 0.95; | |
| } | |
| #${ROOT_ID} #bili-random-panel { | |
| display: none; | |
| flex-direction: column; | |
| gap: 8px; | |
| min-width: 150px; | |
| padding: 12px; | |
| border-radius: 12px; | |
| background: rgba(255,255,255,0.96); | |
| box-shadow: 0 4px 20px rgba(0,0,0,.18); | |
| border: 1px solid rgba(0,0,0,.08); | |
| backdrop-filter: blur(6px); | |
| } | |
| #${ROOT_ID} .bili-random-btn { | |
| border: none; | |
| border-radius: 8px; | |
| padding: 10px 12px; | |
| font-size: 14px; | |
| cursor: pointer; | |
| color: #fff; | |
| } | |
| #${ROOT_ID} #bili-random-toggle-btn { | |
| background: #666; | |
| } | |
| #${ROOT_ID} #bili-random-reset-btn { | |
| background: #999; | |
| } | |
| #${ROOT_ID} .bili-random-btn:hover { | |
| opacity: 0.94; | |
| } | |
| #${ROOT_ID} #bili-random-web-fullscreen-btn { | |
| background: #FF9500; /* 用橙色区分,表示这是一个辅助功能 */ | |
| } | |
| `; | |
| document.head.appendChild(style); | |
| } | |
| function createFloatingPanel() { | |
| if (document.getElementById(ROOT_ID)) return; | |
| injectStyle(); | |
| const root = document.createElement('div'); | |
| root.id = ROOT_ID; | |
| const panel = document.createElement('div'); | |
| panel.id = 'bili-random-panel'; | |
| const toggleBtn = document.createElement('button'); | |
| toggleBtn.id = 'bili-random-toggle-btn'; | |
| toggleBtn.className = 'bili-random-btn'; | |
| toggleBtn.textContent = getEnabled() ? '随机播放:开' : '随机播放:关'; | |
| toggleBtn.addEventListener('click', () => { | |
| const nextState = !getEnabled(); | |
| setEnabled(nextState); | |
| if (nextState) { | |
| markCurrentPlayed(); | |
| } | |
| updateToggleButtonUI(); | |
| log('随机播放状态:', nextState ? '开启' : '关闭'); | |
| }); | |
| // 新增:自动网页全屏开关 | |
| const webFsBtn = document.createElement('button'); | |
| webFsBtn.id = 'bili-random-web-fullscreen-btn'; | |
| webFsBtn.className = 'bili-random-btn'; | |
| webFsBtn.textContent = getWebFullscreenEnabled() ? '自动网页全屏:开' : '自动网页全屏:关'; | |
| webFsBtn.addEventListener('click', () => { | |
| const nextState = !getWebFullscreenEnabled(); | |
| setWebFullscreenEnabled(nextState); | |
| webFsBtn.textContent = nextState ? '自动网页全屏:开' : '自动网页全屏:关'; | |
| webFsBtn.style.background = nextState ? '#FF9500' : '#999'; | |
| // 如果开启了,立即尝试执行一次(方便用户即时看到效果) | |
| if (nextState) { | |
| triggerWebFullscreen(); | |
| } | |
| log('自动网页全屏状态:', nextState ? '开启' : '关闭'); | |
| }); | |
| const resetBtn = document.createElement('button'); | |
| resetBtn.id = 'bili-random-reset-btn'; | |
| resetBtn.className = 'bili-random-btn'; | |
| resetBtn.textContent = '重置历史'; | |
| resetBtn.addEventListener('click', () => { | |
| const currentBvid = getCurrentVideoId(); | |
| setHistory(currentBvid ? [currentBvid] : []); | |
| log('播放历史已重置'); | |
| }); | |
| panel.appendChild(toggleBtn); | |
| panel.appendChild(webFsBtn); // 新增 | |
| panel.appendChild(resetBtn); | |
| const miniBtn = document.createElement('button'); | |
| miniBtn.id = 'bili-random-mini-btn'; | |
| miniBtn.textContent = getPanelOpen() ? '收起' : '随机'; | |
| miniBtn.addEventListener('click', () => { | |
| const nextOpen = !getPanelOpen(); | |
| setPanelOpen(nextOpen); | |
| updatePanelUI(); | |
| }); | |
| root.appendChild(panel); | |
| root.appendChild(miniBtn); | |
| document.body.appendChild(root); | |
| updatePanelUI(); | |
| } | |
| function init() { | |
| createFloatingPanel(); | |
| observeVideo(); | |
| log('脚本已启动'); | |
| } | |
| window.addEventListener('load', () => { | |
| setTimeout(init, 1200); | |
| }); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment