// ============================================================= // Bilibili 合集链接提取面板(带按钮 / 列表 / 复制 / 下载) // 安装方式(任选其一): // 1) 控制台:打开合集播放页 → F12 → Console → 粘贴本脚本 → 回车 // 2) 油猴:直接把本文件当 UserScript 安装(已带 ==UserScript== 头) // 功能:右下角出现「📋 合集链接」按钮 → 点击提取合集全部 // 视频名称 + 链接 → 面板列表展示 → 一键复制 / 下载 TXT / 下载 MD // ============================================================= // ==UserScript== // @name Bilibili 合集链接提取面板 // @description Bilibili 合集链接提取面板(带按钮 / 列表 / 复制 / 下载) // @version 1.0 // @match https://www.bilibili.com/video/* // @grant none // ==/UserScript== (function () { 'use strict'; const PREFIX = 'biliHeji'; const cleanUrl = (bvid) => `https://www.bilibili.com/video/${bvid}/`; let LAST_SEASON = ''; // ---------- 样式 ---------- const STYLE = ` .${PREFIX}-btn{ position:fixed;right:18px;bottom:90px;z-index:99999; background:#fb7299;color:#fff;border:none;border-radius:24px; padding:10px 16px;font-size:14px;cursor:pointer;box-shadow:0 4px 14px rgba(251,114,153,.5); font-family:-apple-system,"PingFang SC",Microsoft YaHei,sans-serif; } .${PREFIX}-btn:hover{background:#fc8bab;} .${PREFIX}-panel{ position:fixed;right:18px;bottom:140px;z-index:99999;width:380px;max-height:70vh; background:#fff;border:1px solid #eee;border-radius:12px;box-shadow:0 10px 40px rgba(0,0,0,.18); display:none;flex-direction:column;overflow:hidden; font-family:-apple-system,"PingFang SC",Microsoft YaHei,sans-serif;font-size:13px;color:#222; } .${PREFIX}-head{ display:flex;align-items:center;justify-content:space-between; padding:12px 14px;background:#fb7299;color:#fff;font-weight:600; } .${PREFIX}-head .close{cursor:pointer;font-size:18px;line-height:1;} .${PREFIX}-list{flex:1;overflow-y:auto;padding:8px 10px;margin:0;} .${PREFIX}-item{ display:flex;gap:8px;padding:8px 6px;border-bottom:1px solid #f2f2f2;align-items:flex-start; } .${PREFIX}-idx{color:#fb7299;font-weight:700;min-width:22px;text-align:right;} .${PREFIX}-meta{flex:1;min-width:0;} .${PREFIX}-title{display:block;color:#222;line-height:1.4;word-break:break-all;} .${PREFIX}-url{display:block;color:#888;font-size:11px;margin-top:2px;word-break:break-all;} .${PREFIX}-copyone{color:#fb7299;cursor:pointer;font-size:11px;white-space:nowrap;} .${PREFIX}-foot{display:flex;gap:8px;padding:10px 12px;border-top:1px solid #eee;} .${PREFIX}-foot button{ flex:1;border:none;border-radius:8px;padding:9px 0;font-size:13px;cursor:pointer;font-weight:600; } .${PREFIX}-copy{background:#fb7299;color:#fff;} .${PREFIX}-dl{background:#00a1d6;color:#fff;} .${PREFIX}-dlmd{background:#fff;color:#00a1d6;border:1px solid #00a1d6 !important;} .${PREFIX}-foot button:hover{filter:brightness(1.05);} .${PREFIX}-empty{padding:24px;text-align:center;color:#999;} `; function injectStyle() { if (document.getElementById(`${PREFIX}-style`)) return; const s = document.createElement('style'); s.id = `${PREFIX}-style`; s.textContent = STYLE; document.head.appendChild(s); } // ---------- 提取逻辑(异步:接口优先 → 初始数据 → DOM 兜底)---------- function getCurrentBvid() { const m = location.pathname.match(/\/(BV[0-9A-Za-z]+)/); if (m) return m[1]; try { const st = window.__INITIAL_STATE__; if (st && st.bvid) return st.bvid; if (st && st.videoData && st.videoData.bvid) return st.videoData.bvid; } catch (_) {} return null; } // 遍历含 Shadow DOM 的所有 DOM 节点,收集 /video/BV 链接 function domExtract() { const seen = new Set(); const out = []; const walk = (root) => { if (!root) return; let nodes = []; try { nodes = Array.from(root.querySelectorAll('a[href*="/video/BV"]')); } catch (_) {} nodes.forEach((a) => { const m = (a.href || '').match(/\/video\/(BV[0-9A-Za-z]+)/); if (!m) return; const bvid = m[1]; if (seen.has(bvid)) return; seen.add(bvid); let title = a.getAttribute('title') || ''; if (!title) { const t = a.querySelector('[class*="title"], .name'); if (t) title = t.textContent.trim(); } if (!title) title = a.textContent.replace(/\s+/g, ' ').trim().slice(0, 80); out.push({ bvid, title: title || bvid, url: cleanUrl(bvid) }); }); // 递归进入 shadow root try { root.querySelectorAll('*').forEach((el) => { if (el.shadowRoot) walk(el.shadowRoot); }); } catch (_) {} }; const container = document.querySelector('.video-pod') || document.querySelector('#multi_page') || document.querySelector('[class*="ugc"]') || document; walk(container); return out; } async function extract() { const bvid = getCurrentBvid(); let items = []; // 0) 初始数据(同步最快,部分老页面仍有效) try { const st = window.__INITIAL_STATE__; if (st && st.ugcSeason && Array.isArray(st.ugcSeason.eps)) { LAST_SEASON = st.ugcSeason.title || LAST_SEASON; items = st.ugcSeason.eps.map((ep) => ({ bvid: ep.bvid, title: ep.title || ep.bvid, url: cleanUrl(ep.bvid), })); } if (!items.length && st && st.videoList && Array.isArray(st.videoList)) { items = st.videoList.map((v) => ({ bvid: v.bvid, title: v.title || v.bvid, url: cleanUrl(v.bvid), })); } } catch (_) {} // 1) 主路径:调用官方接口(带登录态,最稳,覆盖异步合集) if (!items.length && bvid) { try { const r = await fetch( `https://api.bilibili.com/x/web-interface/view?bvid=${bvid}`, { credentials: 'include', headers: { Referer: 'https://www.bilibili.com/' } } ); const j = await r.json(); if (j && j.data) { // 优先判断单视频多 P:页面显示的分P列表(同一 BV 多个分P) // 必须放在合集判断之前,否则视频所属大合集(如 90 集)会抢先 if (Array.isArray(j.data.pages) && j.data.pages.length > 1) { LAST_SEASON = j.data.title || LAST_SEASON; items = j.data.pages.map((p) => ({ bvid, page: p.page, title: `P${p.page} ${p.part || ''}`.trim(), url: `https://www.bilibili.com/video/${bvid}/?p=${p.page}`, })); } else { const season = j.data.ugc_season; const eps = []; if (season && Array.isArray(season.episodes) && season.episodes.length) { eps.push(...season.episodes); } // 新版合集:全集在 sections[].episodes[] 里(episodes 顶层通常为空) if (season && Array.isArray(season.sections)) { season.sections.forEach((sec) => { const se = sec.episodes || sec.archives || []; if (Array.isArray(se)) eps.push(...se); }); } if (eps.length) { LAST_SEASON = (season && season.title) || LAST_SEASON; items = eps .map((ep) => ({ bvid: ep.bvid || (ep.arc && ep.arc.bvid), title: ep.title || (ep.arc && ep.arc.title) || ep.bvid || '', url: cleanUrl(ep.bvid || (ep.arc && ep.arc.bvid)), })) .filter((x) => x.bvid); } } } } catch (_) {} } // 2) DOM 兜底(含 Shadow DOM) if (!items.length) items = domExtract(); // 去重保序(按完整 url 去重:多 P 的 24 条共用同一 bvid,按 bvid 会错折叠成 1 条) const map = new Map(); items.forEach((it) => { if (it.url && !map.has(it.url)) map.set(it.url, it); }); return [...map.values()]; } // ---------- 复制 / 下载 ---------- async function copyText(text) { try { await navigator.clipboard.writeText(text); return true; } catch (_) { try { const ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.select(); const ok = document.execCommand('copy'); document.body.removeChild(ta); return ok; } catch (__) { return false; } } } function download(filename, content, mime) { const blob = new Blob([content], { type: mime || 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(() => URL.revokeObjectURL(url), 1000); } // ---------- 构建 UI ---------- function buildUI() { injectStyle(); const btn = document.createElement('button'); btn.id = `${PREFIX}-btn`; btn.className = `${PREFIX}-btn`; btn.textContent = '📋 合集链接'; document.body.appendChild(btn); const panel = document.createElement('div'); panel.className = `${PREFIX}-panel`; panel.innerHTML = `
合集链接提取 ✕
`; document.body.appendChild(panel); const listEl = panel.querySelector(`.${PREFIX}-list`); const titleEl = panel.querySelector(`.${PREFIX}-htitle`); let current = []; function render() { if (!current.length) { listEl.innerHTML = `
未识别到合集,请确认已打开合集播放页。
`; return; } titleEl.textContent = LAST_SEASON ? `${LAST_SEASON}(${current.length})` : `合集链接提取(${current.length})`; listEl.innerHTML = current .map( (it, i) => `
  • ${i + 1} ${it.title} ${it.url} 复制
  • ` ) .join(''); } async function refresh() { titleEl.textContent = '合集链接提取(提取中…)'; listEl.innerHTML = `
    正在从 B 站接口提取…
    `; current = await extract(); render(); } btn.addEventListener('click', () => { panel.style.display = panel.style.display === 'flex' ? 'none' : 'flex'; if (panel.style.display === 'flex') refresh(); }); panel.querySelector(`.${PREFIX}-close`).addEventListener('click', () => { panel.style.display = 'none'; }); panel.querySelector(`.${PREFIX}-copy`).addEventListener('click', async () => { const urls = current.map((it) => it.url).join('\n'); const ok = await copyText(urls); alert(ok ? `已复制 ${current.length} 个链接到剪贴板` : '复制失败,请手动选择'); }); panel.querySelector(`.${PREFIX}-dl`).addEventListener('click', () => { const txt = current.map((it, i) => `${i + 1}. ${it.title}\n ${it.url}`).join('\n\n'); download('bilibili_heji.txt', txt); }); panel.querySelector(`.${PREFIX}-dlmd`).addEventListener('click', () => { const md = `# Bilibili 合集链接(${current.length})\n\n` + current.map((it, i) => `${i + 1}. [${it.title}](${it.url})`).join('\n'); download('bilibili_heji.md', md, 'text/markdown;charset=utf-8'); }); listEl.addEventListener('click', async (e) => { const t = e.target.closest('.copyone'); if (!t) return; const ok = await copyText(t.dataset.url); if (ok) { const old = t.textContent; t.textContent = '已复制'; setTimeout(() => (t.textContent = old), 1200); } }); } // 等待页面就绪后注入(适配 SPA / 延迟渲染) function init() { if (document.getElementById(`${PREFIX}-btn`)) return; if (document.body) buildUI(); else window.addEventListener('DOMContentLoaded', buildUI); } if (document.readyState === 'loading') { window.addEventListener('DOMContentLoaded', init); } else { init(); } })();