在 B 站收藏夹页提取本页所有视频标题 + 链接到浮动窗口,支持一键复制与导出 TXT
// ==UserScript==
// @name B站收藏夹批量提取视频链接/Bilibili Favlist Extractor
// @namespace ChatGPT
// @version 0.3
// @author GPT
// @description 在 B 站收藏夹页提取本页所有视频标题 + 链接到浮动窗口,支持一键复制与导出 TXT
// @match https://space.bilibili.com/*/favlist*
// @icon https://www.bilibili.com/favicon.ico
// @grant GM_setClipboard
// @grant GM_download
// @run-at document-idle
// @license MIT
// ==/UserScript==
(function () {
'use strict';
/** 计算当前页码(URL 参数 page= 或 pn=,默认 1) */
function getPageNumber() {
const u = new URL(location.href);
return parseInt(u.searchParams.get('page') || u.searchParams.get('pn') || '1', 10);
}
/** 导出为 TXT 文件 */
function exportTxt(content) {
const now = new Date();
const y = now.getFullYear();
const m = String(now.getMonth() + 1).padStart(2, '0');
const d = String(now.getDate()).padStart(2, '0');
const name = `${y}${m}${d}_第${getPageNumber()}页.txt`;
GM_download({
url: URL.createObjectURL(new Blob([content], { type: 'text/plain;charset=utf-8' })),
name,
saveAs: true
});
}
/** 收集当前 DOM 中已渲染的视频标题+链接 */
function collectVideos() {
const anchors = [...document.querySelectorAll('a[href*="/video/"]')];
const map = new Map(); // 去重(以 BV 号为键)
anchors.forEach(a => {
const url = new URL(a.href, location.origin);
const m = url.pathname.match(/\/video\/(BV\w+)/);
if (!m) return;
const bv = m[1];
const title = (a.innerText || '').trim();
map.set(bv, `${title} ${url.origin}${url.pathname}`);
});
return [...map.values()];
}
/** 创建浮窗 */
function createPanel() {
const panel = document.createElement('div');
panel.id = 'fav-extractor-panel';
panel.innerHTML = `
<div id="fav-extractor-header">
<span>🎬 收藏夹视频列表</span>
<button id="fav-extractor-refresh">刷新</button>
<button id="fav-extractor-copy">复制全部</button>
<button id="fav-extractor-export">导出TXT</button>
<button id="fav-extractor-close">✕</button>
</div>
<textarea id="fav-extractor-output" readonly></textarea>
<style>
#fav-extractor-panel{
position:fixed; right:24px; bottom:24px; z-index:999999;
width:360px; height:150px; max-height:90vh;
background:#fff; border:1px solid #888;
box-shadow:0 6px 12px rgba(0,0,0,.2); border-radius:8px; font-size:14px;
display:flex; flex-direction:column; resize:both; overflow:hidden;
}
#fav-extractor-header{
display:flex; align-items:center; justify-content:space-between;
background:#00AEEC; color:#fff; padding:6px 8px; cursor:move; user-select:none;
}
#fav-extractor-header button{
margin-left:6px; border:none; border-radius:4px; padding:2px 6px;
background:#fff; color:#00AEEC; cursor:pointer; font-size:12px;
}
#fav-extractor-header button:hover{opacity:.8;}
#fav-extractor-output{
flex:1; width:100%; border:none; padding:8px; box-sizing:border-box;
font-family:monospace; white-space:pre; overflow:auto;
}
</style>
`;
document.body.appendChild(panel);
/** 拖动支持 */
(function () {
const header = panel.querySelector('#fav-extractor-header');
let sx, sy, sl, st, dragging = false;
header.addEventListener('mousedown', e => {
dragging = true;
sx = e.clientX; sy = e.clientY;
const r = panel.getBoundingClientRect();
sl = r.left; st = r.top;
e.preventDefault();
});
document.addEventListener('mousemove', e => {
if (!dragging) return;
panel.style.left = sl + (e.clientX - sx) + 'px';
panel.style.top = st + (e.clientY - sy) + 'px';
panel.style.right = 'auto'; panel.style.bottom = 'auto';
});
document.addEventListener('mouseup', () => dragging = false);
})();
/** 填充文本框 */
function fillTextarea() {
const output = panel.querySelector('#fav-extractor-output');
output.value = collectVideos().join('\n');
}
/** 按钮事件 */
panel.querySelector('#fav-extractor-refresh').onclick = fillTextarea;
panel.querySelector('#fav-extractor-copy').onclick = () => {
const txt = panel.querySelector('#fav-extractor-output').value;
GM_setClipboard(txt, { type: 'text', mimetype: 'text/plain' });
alert(`已复制 ${txt.split('\\n').length} 条链接到剪贴板!`);
};
panel.querySelector('#fav-extractor-export').onclick = () => {
const txt = panel.querySelector('#fav-extractor-output').value;
exportTxt(txt);
};
panel.querySelector('#fav-extractor-close').onclick = () => panel.remove();
/** 初始填充 & 动态监听 */
fillTextarea();
const root = document.querySelector('.fav-video-list,.be-pager');
if (root) {
new MutationObserver(fillTextarea).observe(root, { childList: true, subtree: true });
}
}
/* 等 DOM 就绪后创建浮窗 */
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', createPanel);
} else {
createPanel();
}
})();