// ==UserScript==
// @name B站视频封面高级提取助手
// @namespace http://tampermonkey.net/
// @version 3.3
// @description 可视化选择、折叠重复封面、显示分辨率格式、以“标题+BV/AV号”命名并一键打包ZIP下载,支持一键刷新抓取。
// @author Assistant
// @match https://*.bilibili.com/*
// @icon https://www.bilibili.com/favicon.ico
// @grant none
// @require https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js
// ==/UserScript==
(function() {
'use strict';
// 1. 创建右下角悬浮控制按钮
const floatBtn = document.createElement('div');
floatBtn.innerHTML = '📥 提取封面';
floatBtn.style.cssText = `
position: fixed;
bottom: 30px;
right: 30px;
z-index: 99999;
background-color: #00aeec;
color: white;
padding: 10px 16px;
border-radius: 20px;
cursor: pointer;
font-family: Arial, sans-serif;
font-size: 13px;
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
user-select: none;
transition: background 0.2s;
`;
floatBtn.onmouseover = () => floatBtn.style.backgroundColor = '#00b5e5';
floatBtn.onmouseout = () => floatBtn.style.backgroundColor = '#00aeec';
document.body.appendChild(floatBtn);
// 2. 创建可视化交互弹窗面板
const panel = document.createElement('div');
panel.style.cssText = `
position: fixed;
bottom: 80px;
right: 30px;
width: 380px;
max-height: 520px;
background: #f4f5f7;
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0 6px 20px rgba(0,0,0,0.2);
z-index: 100000;
display: none;
flex-direction: column;
font-family: Arial, sans-serif;
padding: 12px;
box-sizing: border-box;
`;
panel.innerHTML = `
B站封面高级提取工具
准备就绪
`;
document.body.appendChild(panel);
let mainGroups = [];
// 点击悬浮按钮开关面板并触发抓取
floatBtn.onclick = () => {
if (panel.style.display === 'flex') {
panel.style.display = 'none';
} else {
panel.style.display = 'flex';
fetchAndRenderCovers();
}
};
// 绑定刷新按钮事件
document.getElementById('tm-refresh').onclick = () => {
fetchAndRenderCovers();
};
// 抓取页面视频封面
async function fetchAndRenderCovers() {
const statusDiv = document.getElementById('tm-status');
const coverListDiv = document.getElementById('tm-cover-list');
statusDiv.textContent = '正在重新获取页面视频与封面...';
coverListDiv.innerHTML = '正在重新扫描页面卡片...
';
const selectors = [
'.bili-video-card',
'.feed-card',
'.video-card-common',
'.video-page-card-small',
'.related-card',
'.card-box',
'.rank-item'
];
const cards = document.querySelectorAll(selectors.join(','));
const rawCovers = [];
cards.forEach(card => {
const imgElement = card.querySelector('img');
const titleElement = card.querySelector('.bili-video-card__info--tit, .bili-video-card__title, .title, .video-name');
const linkElement = card.querySelector('a');
if (imgElement) {
let url = imgElement.getAttribute('data-src') || imgElement.getAttribute('src') || imgElement.src;
let title = 'bilibili_video';
if (titleElement) {
title = titleElement.getAttribute('title') || titleElement.innerText.trim();
} else if (imgElement.alt) {
title = imgElement.alt;
}
let bvid = '';
let href = linkElement ? linkElement.href : '';
const bvMatch = href.match(/(BV[1-9a-zA-Z]{10})/);
const avMatch = href.match(/(av\d+)/i);
if (bvMatch) {
bvid = bvMatch[1];
} else if (avMatch) {
bvid = avMatch[1];
}
if (url && !url.startsWith('data:image')) {
rawCovers.push({ url, title, bvid });
}
}
});
if (rawCovers.length === 0) {
coverListDiv.innerHTML = '未在当前页面找到视频封面
';
statusDiv.textContent = '未找到封面';
return;
}
statusDiv.textContent = '正在分析图片分辨率与格式...';
const urlMap = new Map();
for (const item of rawCovers) {
let cleanUrl = item.url;
if (cleanUrl.startsWith('//')) cleanUrl = 'https:' + cleanUrl;
cleanUrl = cleanUrl.split('@')[0];
const meta = await getImageMeta(cleanUrl);
const fullItem = { ...item, cleanUrl, ...meta };
if (!urlMap.has(cleanUrl)) {
urlMap.set(cleanUrl, { primary: fullItem, duplicates: [] });
} else {
urlMap.get(cleanUrl).duplicates.push(fullItem);
}
}
mainGroups = Array.from(urlMap.values());
renderList(mainGroups);
statusDiv.textContent = `刷新完成:发现 ${mainGroups.length} 个独立封面`;
}
// 异步加载图片获取分辨率和格式
function getImageMeta(url) {
return new Promise((resolve) => {
const img = new Image();
img.crossOrigin = "anonymous";
img.onload = () => {
let format = 'JPG';
if (url.includes('.webp')) format = 'WEBP';
else if (url.includes('.png')) format = 'PNG';
resolve({ width: img.naturalWidth, height: img.naturalHeight, format });
};
img.onerror = () => resolve({ width: 0, height: 0, format: 'Unknown' });
img.src = url;
});
}
// 渲染列表结构
function renderList(groups) {
const coverListDiv = document.getElementById('tm-cover-list');
coverListDiv.innerHTML = '';
groups.forEach((group, groupIndex) => {
const item = group.primary;
const groupDiv = document.createElement('div');
groupDiv.className = 'cover-group';
groupDiv.style.cssText = 'border-bottom: 1px solid #eee; padding: 4px 0;';
const primaryItemDiv = document.createElement('div');
primaryItemDiv.className = 'cover-item';
primaryItemDiv.style.cssText = 'display: flex; align-items: center; gap: 8px; padding: 4px; font-size: 11px;';
primaryItemDiv.innerHTML = `
${item.title}
${item.bvid ? `${item.bvid}` : ''}
${item.width}x${item.height} | ${item.format}
`;
groupDiv.appendChild(primaryItemDiv);
if (group.duplicates.length > 0) {
const toggleDiv = document.createElement('div');
toggleDiv.style.cssText = 'font-size: 10px; color: #666; background: #fafafa; padding: 4px 8px; cursor: pointer; border: 1px dashed #ddd; border-radius: 3px; margin: 2px 0 4px 28px; text-align: center; user-select: none;';
toggleDiv.textContent = `📂 还有 ${group.duplicates.length} 个相似/重复来源(点击展开折叠)`;
const containerDiv = document.createElement('div');
containerDiv.style.cssText = 'display: none; padding-left: 20px; background: #fdfdfd;';
group.duplicates.forEach((dup, dupIndex) => {
const dupItemDiv = document.createElement('div');
dupItemDiv.style.cssText = 'display: flex; align-items: center; gap: 8px; padding: 4px; font-size: 11px; border-top: 1px solid #f5f5f5;';
dupItemDiv.innerHTML = `
${dup.title}
${dup.width}x${dup.height} | ${dup.format}
`;
containerDiv.appendChild(dupItemDiv);
});
toggleDiv.addEventListener('click', () => {
if (containerDiv.style.display === 'block') {
containerDiv.style.display = 'none';
toggleDiv.textContent = toggleDiv.textContent.replace('点击收起', '点击展开折叠');
} else {
containerDiv.style.display = 'block';
toggleDiv.textContent = toggleDiv.textContent.replace('点击展开折叠', '点击收起');
}
});
groupDiv.appendChild(toggleDiv);
groupDiv.appendChild(containerDiv);
}
coverListDiv.appendChild(groupDiv);
});
}
// 按钮交互事件
document.getElementById('tm-selectAll').onclick = () => {
document.querySelectorAll('#tm-cover-list input[type="checkbox"]').forEach(chk => chk.checked = true);
};
document.getElementById('tm-deselectAll').onclick = () => {
document.querySelectorAll('#tm-cover-list input[type="checkbox"]').forEach(chk => chk.checked = false);
};
// 打包下载 ZIP
document.getElementById('tm-download').onclick = async () => {
const checkboxes = document.querySelectorAll('#tm-cover-list input[type="checkbox"]:checked');
if (checkboxes.length === 0) {
alert('请至少选择一个封面!');
return;
}
const statusDiv = document.getElementById('tm-status');
statusDiv.textContent = `正在打包 ${checkboxes.length} 张图片,请稍候...`;
const zip = new JSZip();
const folder = zip.folder("bilibili_covers");
for (const chk of checkboxes) {
const gIndex = chk.getAttribute('data-group');
const type = chk.getAttribute('data-type');
let targetItem;
if (type === 'primary') {
targetItem = mainGroups[gIndex].primary;
} else {
const dIndex = chk.getAttribute('data-dupindex');
targetItem = mainGroups[gIndex].duplicates[dIndex];
}
let idStr = targetItem.bvid ? `_${targetItem.bvid}` : '';
let cleanTitle = targetItem.title.replace(/[/\\?%*:|"<>]/g, '').slice(0, 30);
let fileName = `${cleanTitle}${idStr}.${targetItem.format.toLowerCase()}`;
try {
const response = await fetch(targetItem.cleanUrl);
const blob = await response.blob();
folder.file(fileName, blob);
} catch (e) {
console.error('下载图片失败:', targetItem.cleanUrl);
}
}
const content = await zip.generateAsync({ type: "blob" });
const url = URL.createObjectURL(content);
const a = document.createElement('a');
a.href = url;
a.download = `bili_covers_${Date.now()}.zip`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
statusDiv.textContent = '打包下载成功!';
};
})();