// ==UserScript== // @name B站一键复制全部分集URL // @namespace http://tampermonkey.net/ // @version 1.0 // @description 一键复制B站视频或番剧的全部分集URL,加入轻量级防卡顿和智能隐藏按钮功能。 // @author ShuiYun // @match *://www.bilibili.com/video/* // @match *://www.bilibili.com/bangumi/play/* // @grant GM_setClipboard // @grant unsafeWindow // @run-at document-idle // ==/UserScript== (function() { 'use strict'; // === 常量定义 === const BTN_ID = 'copy-all-episodes-btn'; const TOAST_CLASS = 'copy-toast'; const EXCLUDE_KEYWORDS = ['pv', '预告', '片头', '片尾', 'op', 'ed', '花絮', '彩蛋', '特报', '特别篇', '预告片', '宣传片']; // === 注入样式 === const style = document.createElement('style'); style.textContent = ` .copy-all-btn { position: fixed; z-index: 99999; bottom: 100px; right: 20px; background-color: #00a1d6; color: #fff; border-radius: 6px; padding: 10px 16px; font-size: 14px; font-weight: 500; cursor: pointer; user-select: none; white-space: nowrap; box-shadow: 0 4px 12px rgba(0,0,0,0.15); border: none; opacity: 0.85; transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; } .copy-all-btn:hover { opacity: 1; background-color: #fb7299; transform: scale(1.05) translateY(-2px); box-shadow: 0 6px 16px rgba(251,114,153,0.3); } .copy-all-btn:active { transform: scale(0.95); } .copy-all-btn.hidden { opacity: 0 !important; visibility: hidden !important; pointer-events: none !important; transform: translateX(50px); } .copy-toast { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(0.9); background: rgba(0,0,0,0.85); color: #fff; padding: 16px 30px; border-radius: 8px; z-index: 2147483647; font-size: 16px; pointer-events: none; opacity: 0; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); } .copy-toast.show { opacity: 1; transform: translate(-50%, -50%) scale(1); } `; document.head.appendChild(style); // === Toast 提示管理器 === class ToastManager { constructor() { this.toast = null; this.timer = null; } show(message, duration = 2500) { if (!this.toast) { this.toast = document.createElement('div'); this.toast.className = TOAST_CLASS; document.body.appendChild(this.toast); } if (this.timer) clearTimeout(this.timer); this.toast.textContent = message; this.toast.classList.add('show'); this.timer = setTimeout(() => { this.toast.classList.remove('show'); }, duration); } } const toastManager = new ToastManager(); // === 数据获取与提取核心 (恢复原版高成功率逻辑) === function getPageData() { try { const pageWindow = (typeof wrappedJSObject !== 'undefined') ? wrappedJSObject : (typeof unsafeWindow !== 'undefined' && unsafeWindow) ? unsafeWindow : window; return pageWindow.__INITIAL_STATE__ || null; } catch (e) { return null; } } // 番剧提取逻辑 function extractBangumiUrls() { let urls = []; const state = getPageData(); const currentEpIdMatch = location.href.match(/ep(\d+)/); const currentEpId = currentEpIdMatch ? currentEpIdMatch[1] : null; // 1. 尝试从页面变量提取 (仅当数据匹配当前视频时,防止SPA路由缓存) if (state && ((state.epInfo && state.epInfo.ep_id == currentEpId) || state.episodes)) { // 正片 episodes if (state.episodes && state.episodes.length > 0) { state.episodes.forEach(ep => { const id = ep.ep_id || ep.id || ep.episode_id; if (id) urls.push(`https://www.bilibili.com/bangumi/play/ep${id}`); }); } // 尝试 sections else if (state.sections) { state.sections.forEach(sec => { const secTitle = (sec.title || '').toLowerCase(); if (!EXCLUDE_KEYWORDS.some(k => secTitle.includes(k))) { (sec.episodes || []).forEach(ep => { const id = ep.ep_id || ep.id || ep.episode_id; if (id) urls.push(`https://www.bilibili.com/bangumi/play/ep${id}`); }); } }); } } // 2. 如果变量提取失败,启用原版宽泛的 DOM 扫描 if (urls.length === 0) { document.querySelectorAll('a[href*="/bangumi/play/ep"]').forEach(link => { const href = link.getAttribute('href'); if (!href) return; const text = (link.textContent || link.innerText || '').trim().toLowerCase(); if (!EXCLUDE_KEYWORDS.some(k => text.includes(k))) { const match = href.match(/ep(\d+)/); if (match) urls.push(`https://www.bilibili.com/bangumi/play/ep${match[1]}`); } }); } return urls; } // 普通视频提取逻辑 function extractVideoUrls() { let urls = []; const state = getPageData(); const currentBvidMatch = location.href.match(/BV[a-zA-Z0-9]+/); const currentBvid = currentBvidMatch ? currentBvidMatch[0] : null; // 1. 尝试从页面变量提取 (检查BVID以防止SPA路由缓存脏数据) if (state && state.videoData && state.bvid === currentBvid) { const pages = state.videoData.pages; const sections = state.videoData.sections || (state.videoData.ugc_season && state.videoData.ugc_season.sections); // 分P if (pages && pages.length > 0) { pages.forEach(p => urls.push(`https://www.bilibili.com/video/${state.bvid}?p=${p.page}`)); } // 合集 else if (sections && sections.length > 0) { sections.forEach(sec => { (sec.episodes || []).forEach(ep => { if (ep.bvid) urls.push(`https://www.bilibili.com/video/${ep.bvid}`); }); }); } } // 2. 如果变量提取失败,启用原版宽泛的 DOM 扫描 if (urls.length === 0) { const selectors = ['a[href*="bilibili.com/video/"][href*="BV"]', 'a[href*="/video/BV"]']; document.querySelectorAll(selectors.join(', ')).forEach(link => { const href = link.getAttribute('href'); const match = href ? href.match(/(BV[a-zA-Z0-9]+)/) : null; if (match) urls.push(`https://www.bilibili.com/video/${match[1]}`); }); } // 如果连DOM都没有,至少返回当前视频 if (urls.length === 0 && currentBvid) { urls.push(`https://www.bilibili.com/video/${currentBvid}`); } return urls; } // === 复制处理 === function copyUrls() { const isBangumi = location.pathname.includes('/bangumi/'); let result = isBangumi ? extractBangumiUrls() : extractVideoUrls(); // 去重 const uniqueUrls = [...new Set(result)]; if (uniqueUrls.length === 0) { toastManager.show('未检测到正片分集信息', 3000); return; } const textToCopy = uniqueUrls.join('\n'); try { if (typeof GM_setClipboard !== 'undefined') { GM_setClipboard(textToCopy); toastManager.show(`✅ 成功复制 ${uniqueUrls.length} 个正片URL!`); } else { throw new Error('GM_setClipboard 不可用'); } } catch (e) { if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(textToCopy) .then(() => toastManager.show(`✅ 成功复制 ${uniqueUrls.length} 个正片URL!`)) .catch(() => toastManager.show('❌ 复制失败,请检查浏览器权限', 3000)); } else { const textarea = document.createElement('textarea'); textarea.value = textToCopy; textarea.style.position = 'fixed'; textarea.style.opacity = '0'; document.body.appendChild(textarea); textarea.select(); try { document.execCommand('copy'); toastManager.show(`✅ 成功复制 ${uniqueUrls.length} 个正片URL!`); } catch (err) { toastManager.show('❌ 您的浏览器不支持自动复制,请手动操作', 3000); } finally { document.body.removeChild(textarea); } } } } // === UI 按钮管理器 === class ButtonManager { constructor() { this.btn = null; } ensureExists() { if (!document.getElementById(BTN_ID)) { const btn = document.createElement('div'); btn.id = BTN_ID; btn.className = 'copy-all-btn'; btn.textContent = '复制全部URL'; btn.addEventListener('click', copyUrls); document.body.appendChild(btn); this.btn = btn; } } updateVisibility() { if (!this.btn) return; const player = document.querySelector('.bpx-player-container, .bilibili-player'); let shouldHide = false; // 检测播放器屏幕状态 if (player) { const screenMode = player.getAttribute('data-screen') || 'normal'; if (['mini', 'full', 'web'].includes(screenMode)) { shouldHide = true; } } // 兼容部分画中画小窗 if (document.querySelector('.bpx-player-mini') || document.pictureInPictureElement) { shouldHide = true; } if (shouldHide) { this.btn.classList.add('hidden'); } else { this.btn.classList.remove('hidden'); } } } const buttonManager = new ButtonManager(); // === 极其轻量的循环监听 === function init() { let lastUrl = location.href; setInterval(() => { // URL 变化检测 (B站单页面跳转) if (location.href !== lastUrl) { lastUrl = location.href; } // 确保按钮存在并且更新显示隐藏状态 buttonManager.ensureExists(); buttonManager.updateVisibility(); }, 500); // 0.5秒轮询一次,对浏览器性能完全无影响 } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();