// ==UserScript==
// @name 哔哩哔哩动态图片下载
// @namespace https://space.bilibili.com/11768481
// @version 1.2
// @description 为方便下载bilibili图片而开发。
// @author 伊墨墨
// @match https://www.bilibili.com/opus/*
// @match https://t.bilibili.com/*
// @match https://space.bilibili.com/*/dynamic*
// @match https://www.bilibili.com/v/topic/*
// @grant GM_download
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @license MIT
// @icon https://www.bilibili.com/favicon.ico
// @supportURL https://greasyfork.org/zh-CN/scripts/531888/feedback
// @homepageURL https://greasyfork.org/zh-CN/scripts/531888
// @downloadURL none
// ==/UserScript==
/*
* CHANGELOG:
* v1.2:
* - 新增:设置面板及自定义文件名功能。
* - 新增:文件名模板增加 {date} 占位符,用于插入下载日期 (格式 YYYYMMDD)。
* - 新增:为“文件名格式”配置项增加一个独立的“恢复默认”按钮,方便用户单独重置。
* - 新增:增加“强制使用备用下载模式”选项。
* - 修复:b站新的修改。
*/
(function () {
'use strict';
// --- 样式定义 ---
GM_addStyle(`
/* --- 原有样式 --- */
#bili-download-images-button {
position: fixed; top: 50%; right: 20px; z-index: 1000; padding: 10px 20px;
background-color: #00a1d6; color: white; border: none; border-radius: 5px;
font-size: 16px; cursor: pointer; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}
#bili-download-images-button:hover { background-color: #007ead; }
.bili-toast-message {
position: fixed; top: 20px; left: 50%; transform: translateX(-50%);
background-color: rgba(51, 51, 51, 0.9); color: white; padding: 10px 20px;
border-radius: 5px; z-index: 9999; font-size: 14px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); pointer-events: none;
}
.download-images-option { cursor: pointer; }
.download-images-option:hover .bili-cascader-options__item-label,
.download-images-option.bili-dyn-more__menu__item:hover { color: #00a1d6; }
.side-toolbar__action.download {
cursor: pointer; text-align: center; color: #61666D;
transition: color 0.3s; margin-bottom: 16px;
}
.side-toolbar__action.download svg { width: 24px; height: 24px; margin-bottom: 2px; }
.side-toolbar__action.download .side-toolbar__action__text {
font-size: 12px; line-height: 14px; color: #9499A0;
transform: scale(0.875); transform-origin: center top;
}
.side-toolbar__action.download:hover { color: #00A1D6; }
/* --- 设置面板样式 --- */
.bili-dl-settings-overlay {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background-color: rgba(0, 0, 0, 0.5); z-index: 19998;
}
.bili-dl-settings-panel {
position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
background-color: #fff; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15);
width: 500px; max-width: 90vw; z-index: 19999; padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
.bili-dl-settings-panel h3 { margin: 0 0 15px; font-size: 18px; color: #333; text-align: center; }
.bili-dl-settings-panel .setting-item { margin-bottom: 15px; }
.bili-dl-settings-panel .setting-item label.checkbox-label { display: flex; align-items: center; cursor: pointer; font-weight: normal; }
.bili-dl-settings-panel label { display: block; font-weight: bold; margin-bottom: 5px; color: #555; }
.bili-dl-settings-panel input[type="text"] {
width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px;
box-sizing: border-box; font-size: 14px;
}
.bili-dl-settings-panel .instructions {
font-size: 12px; color: #666; background-color: #f7f7f7;
padding: 10px; border-radius: 4px; margin-top: 5px; line-height: 1.6;
}
.bili-dl-settings-panel .instructions code { background-color: #eee; padding: 2px 4px; border-radius: 3px; }
.bili-dl-settings-panel .actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px; }
.bili-dl-settings-panel button {
padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer;
font-size: 14px; transition: background-color 0.2s;
}
.bili-dl-settings-panel .btn-save { background-color: #00a1d6; color: white; }
.bili-dl-settings-panel .btn-save:hover { background-color: #007ead; }
.bili-dl-settings-panel .btn-reset, .bili-dl-settings-panel .btn-close {
background-color: #f1f2f3; color: #333;
}
.bili-dl-settings-panel .btn-reset:hover, .bili-dl-settings-panel .btn-close:hover {
background-color: #e0e1e2;
}
.bili-dl-settings-panel .setting-item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
}
.bili-dl-settings-panel .setting-item-header label {
margin-bottom: 0;
}
.bili-dl-settings-panel .btn-link-style {
background: none;
border: none;
color: #00a1d6;
font-size: 12px;
cursor: pointer;
padding: 0;
font-weight: normal;
}
.bili-dl-settings-panel .btn-link-style:hover {
text-decoration: underline;
}
`);
// --- 常量与配置 ---
const MAX_CONCURRENT_DOWNLOADS = 3; // 最大并发下载数
const CONFIG = {
FILENAME: {
STORAGE_KEY: 'bili_dl_filename_template',
DEFAULT_TEMPLATE: '{username}_{itemId}_{index}.{format}'
},
FALLBACK_MODE: {
STORAGE_KEY: 'bili_dl_force_fallback',
DEFAULT_VALUE: false
},
DETAIL_PAGE_ID_REGEX: /(?:\/opus\/|\/dynamic\/|\/)(\d{10,})/, // 用于从URL提取ID
IMAGE_CONTAINER_SELECTORS: [ // 详情页图片容器选择器 (优先级顺序)
'.horizontal-scroll-album__indicator',
'.bili-album__preview',
'.bili-dyn-item__images',
'.horizontal-scroll-album',
'.opus-module-content',
'.bili-dyn-gallery__track',
'.bili-dyn-card-video__cover',
'.bili-rich-text__content' // 新增:富文本内容区,用于提取 data-pics
],
DETAIL_PAGE_SELECTORS: {
USERNAME: ['.bili-dyn-title__text', '.opus-module-author__name'],
SIDE_TOOLBAR: ['.side-toolbar__box', '.sidebar-wrap'],
SIDE_TOOLBAR_TARGET: '.side-toolbar__box'
},
CARD_CONFIG: { // 卡片模式下的基础配置
CARD_SELECTOR: '.bili-dyn-list__item',
MORE_BUTTON_SELECTOR: '.bili-dyn-item__more .bili-dyn-more__btn',
CASCADER_SELECTOR: '.bili-dyn-more__cascader, .bili-popover',
OPTIONS_SELECTOR: '.bili-cascader-options, .bili-dyn-more__menu',
LIST_CONTAINER_SELECTOR: '.bili-dyn-list__items',
IMAGE_SELECTORS: [
'.bili-album__watch__track__list img[src]', '.bili-album__preview img[src]',
'.bili-dyn-gallery__track img[src]', '.bili-dyn-card-video__cover img[src]',
'.bili-dyn-card-live__cover img[src]'
],
RICH_TEXT_PICS_SELECTOR: 'span.bili-rich-text-viewpic[data-pics]',
// 新增:动态信息流配置
USERNAME_SELECTORS: ['.dyn-orig-author__name', '.bili-dyn-title__text'],
DYN_ID_SELECTOR:
'.bili-dyn-content__orig .dyn-card-opus[dyn-id], .bili-dyn-content__orig [data-origin-did], .bili-dyn-content__orig .bili-dyn-card-video[dyn-id], .bili-dyn-content__orig .bili-dyn-card-live[dyn-id]',
FALLBACK_LINK_SELECTOR: 'a[href*="/dynamic/"], a[href*="/opus/"]'
}
};
// --- 工具函数 ---
const utils = {
waitForElement: (selector, callback, timeout = 1000) => {
let element = document.querySelector(selector);
if (element) { callback(element); return; }
let observer = null, timeoutId = null;
const cleanup = () => { if (observer) observer.disconnect(); if (timeoutId) clearTimeout(timeoutId); observer = null; timeoutId = null; };
observer = new MutationObserver((mutations, obs) => {
element = document.querySelector(selector);
if (element) { cleanup(); callback(element); }
});
observer.observe(document.body, { childList: true, subtree: true });
if (timeout > 0) {
timeoutId = setTimeout(() => {
if (observer) { console.warn(`[B站图片下载] 等待元素 "${selector}" 超时 (${timeout}ms).`); cleanup(); }
}, timeout);
}
},
/** 文件名消毒:移除或替换非法字符,限制长度 */
sanitizeFilename: (name) => {
if (!name) return 'unknown_file';
return name.replace(/[\\][:*?"<>|]/g, '_').replace(/\s+/g, ' ').trim().slice(0, 250);
},
/** 显示自动消失的提示消息 */
showToast: (message, duration = 3000) => {
const toast = document.createElement('div');
toast.className = 'bili-toast-message';
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), duration);
},
/** 处理图片 URL:确保是 https,移除 @ 参数 */
processImageUrl: (rawUrl) => {
if (!rawUrl || typeof rawUrl !== 'string') return null;
let cleanUrl = rawUrl.startsWith('//') ? 'https:' + rawUrl : rawUrl;
if (!cleanUrl.startsWith('http')) return null;
return cleanUrl.split('@')[0];
},
/**文件名配置处理**/
formatFilename: (template, data) => {
let filename = template;
for (const key in data) {
// 只做替换,不做任何清理
filename = filename.replaceAll(`{${key}}`, data[key]);
}
return filename;
}
};
// --- 设置面板管理器 ---
const settingsManager = {
panel: null,
overlay: null,
init: function () {
GM_registerMenuCommand('⚙️ 下载设置', this.openPanel.bind(this));
},
createPanel: function () {
if (this.panel) return;
this.overlay = document.createElement('div');
this.overlay.className = 'bili-dl-settings-overlay';
this.overlay.style.display = 'none';
this.overlay.addEventListener('click', this.closePanel.bind(this));
this.panel = document.createElement('div');
this.panel.className = 'bili-dl-settings-panel';
this.panel.style.display = 'none';
this.panel.innerHTML = `
哔哩哔哩动态图片下载 - 设置
`;
document.body.appendChild(this.overlay);
document.body.appendChild(this.panel);
this.panel.querySelector('#reset-filename-btn').addEventListener('click', () => {
const defaultTemplate = CONFIG.FILENAME.DEFAULT_TEMPLATE;
this.panel.querySelector('#filename-template-input').value = defaultTemplate;
utils.showToast('文件名格式已重置,点击“保存设置”生效', 2500);
});
this.panel.querySelector('#settings-btn-save').addEventListener('click', this.saveSettings.bind(this));
this.panel.querySelector('#settings-btn-close').addEventListener('click', this.closePanel.bind(this));
},
openPanel: function () {
this.createPanel();
const currentTemplate = GM_getValue(CONFIG.FILENAME.STORAGE_KEY, CONFIG.FILENAME.DEFAULT_TEMPLATE);
this.panel.querySelector('#filename-template-input').value = currentTemplate;
const forceFallback = GM_getValue(CONFIG.FALLBACK_MODE.STORAGE_KEY, CONFIG.FALLBACK_MODE.DEFAULT_VALUE);
this.panel.querySelector('#force-fallback-checkbox').checked = forceFallback;
this.overlay.style.display = 'block';
this.panel.style.display = 'block';
},
closePanel: function () {
if (this.panel) {
this.overlay.style.display = 'none';
this.panel.style.display = 'none';
}
},
saveSettings: function () {
const newTemplate = this.panel.querySelector('#filename-template-input').value.trim();
if (newTemplate) {
GM_setValue(CONFIG.FILENAME.STORAGE_KEY, newTemplate);
} else {
utils.showToast('文件名格式不能为空!', 3000);
return;
}
const forceFallback = this.panel.querySelector('#force-fallback-checkbox').checked;
GM_setValue(CONFIG.FALLBACK_MODE.STORAGE_KEY, forceFallback);
utils.showToast('设置已保存!', 2000);
this.closePanel();
},
resetAllSettings: function () {
const defaultTemplate = CONFIG.FILENAME.DEFAULT_TEMPLATE;
this.panel.querySelector('#filename-template-input').value = defaultTemplate;
GM_setValue(CONFIG.FILENAME.STORAGE_KEY, defaultTemplate);
const defaultFallback = CONFIG.FALLBACK_MODE.DEFAULT_VALUE;
this.panel.querySelector('#force-fallback-checkbox').checked = defaultFallback;
GM_setValue(CONFIG.FALLBACK_MODE.STORAGE_KEY, defaultFallback);
utils.showToast('全部设置已恢复为默认值', 2000);
}
};
// --- 核心下载功能模块 ---
const core = {
//用户名
getUsernameFromPage: () => {
for (const selector of CONFIG.DETAIL_PAGE_SELECTORS.USERNAME) {
const elem = document.querySelector(selector);
if (elem) return elem.innerText.trim();
}
return '未知用户';
},
//动态id
getItemIdFromPage: () => {
const urlMatch = window.location.href.match(CONFIG.DETAIL_PAGE_ID_REGEX);
if (urlMatch?.[1]) return urlMatch[1];
console.warn("[B站图片下载] 无法从URL获取ID,使用时间戳作为后备。");
return Date.now().toString().slice(-10);
},
//图片容器
findImageContainerOnPage: () => {
for (const selector of CONFIG.IMAGE_CONTAINER_SELECTORS) {
const container = document.querySelector(selector);
if (container && (container.querySelector('img[src]') || container.matches(CONFIG.CARD_CONFIG.RICH_TEXT_PICS_SELECTOR) || container.querySelector(CONFIG.CARD_CONFIG.RICH_TEXT_PICS_SELECTOR))) {
return container;
}
}
return null;
},
//从指定的容器中提取所有有效图片信息
extractImagesFromContainer: (container) => {
if (!container) return [];
const images = [];
const seenUrls = new Set();
Array.from(container.querySelectorAll('img[src]')).forEach(img => {
const cleanUrl = utils.processImageUrl(img.src);
if (!cleanUrl || seenUrls.has(cleanUrl)) return;
seenUrls.add(cleanUrl);
const formatMatch = cleanUrl.match(/\.(jpg|jpeg|png|gif|webp|avif)(?:[?#]|$)/i);
images.push({ url: cleanUrl, format: formatMatch ? formatMatch[1].toLowerCase() : 'jpg' });
});
const richTextSpans = [];
if (container.matches && container.matches(CONFIG.CARD_CONFIG.RICH_TEXT_PICS_SELECTOR)) richTextSpans.push(container);
container.querySelectorAll(CONFIG.CARD_CONFIG.RICH_TEXT_PICS_SELECTOR).forEach(span => richTextSpans.push(span));
richTextSpans.forEach(span => {
try {
const picsData = JSON.parse(span.dataset.pics);
if (Array.isArray(picsData)) {
picsData.forEach(picInfo => {
if (picInfo && picInfo.src) {
const cleanUrl = utils.processImageUrl(picInfo.src);
if (!cleanUrl || seenUrls.has(cleanUrl)) return;
seenUrls.add(cleanUrl);
const formatMatch = cleanUrl.match(/\.(jpg|jpeg|png|gif|webp|avif)(?:[?#]|$)/i);
images.push({ url: cleanUrl, format: formatMatch ? formatMatch[1].toLowerCase() : 'jpg' });
}
});
}
} catch (e) { console.warn('[B站图片下载] 解析 data-pics JSON 失败:', e, span.dataset.pics); }
});
return images;
},
//监听 B站相册展开后的图片加载,因为阿b新样式废弃
monitorAlbumExpansion: (username, itemId) => {
utils.waitForElement('.bili-album__watch__track__list', (trackContainer) => {
if (trackContainer.children.length > 0) {
const images = core.extractImagesFromContainer(trackContainer);
if (images.length) core.downloadImages(images, username, itemId);
else utils.showToast('相册展开后未找到图片。');
} else {
utils.showToast('相册轨道为空。');
}
}, 10000);
},
//下载功能
downloadImages: (images, username = '未知用户', itemId = '未知ID') => {
if (!images || images.length === 0) {
utils.showToast('未找到可下载的图片!');
return;
}
const totalImages = images.length;
let submittedCount = 0;
utils.showToast(`开始处理 ${totalImages} 张图片...`);
const filenameTemplate = GM_getValue(CONFIG.FILENAME.STORAGE_KEY, CONFIG.FILENAME.DEFAULT_TEMPLATE);
const forceFallback = GM_getValue(CONFIG.FALLBACK_MODE.STORAGE_KEY, CONFIG.FALLBACK_MODE.DEFAULT_VALUE);
const now = new Date();
const yyyymmdd = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}`;
// --- 数据处理中心 ---
// 在这里对所有从外部获取的数据进行唯一一次的、彻底的清理
const cleanUsername = username.replace(/[/\\:*?"<>|]/g, '_').replace(/\s+/g, ' ').trim();
const cleanItemId = itemId;
// --- 清理结束 ---
const downloadBatch = (startIndex) => {
const endIndex = Math.min(startIndex + MAX_CONCURRENT_DOWNLOADS, totalImages);
for (let i = startIndex; i < endIndex; i++) {
const img = images[i];
// 准备用于模板的数据包,所有数据都是已经清理干净的
const templateData = {
username: cleanUsername,
itemId: cleanItemId,
date: yyyymmdd,
index: String(i + 1).padStart(2, '0'),
format: img.format
};
const filename = utils.formatFilename(filenameTemplate, templateData);
if (forceFallback) {
if (startIndex === 0 && i === 0) {
utils.showToast('已启用备用下载模式', 2000);
}
core.fetchAndDownload(img.url, filename);
submittedCount++;
continue;
}
try {
GM_download({
url: img.url,
name: filename,
headers: { "Referer": location.href },
onerror: (error, details) => {
console.error(`[GM_download Error] ${filename}:`, error, details);
utils.showToast(`下载失败,自动尝试备用方法: ${filename.split('/').pop()}`, 4000);
core.fetchAndDownload(img.url, filename);
},
ontimeout: () => {
console.warn(`[GM_download Timeout] ${filename}`);
utils.showToast(`下载超时,自动尝试备用方法: ${filename.split('/').pop()}`, 4000);
core.fetchAndDownload(img.url, filename);
}
});
submittedCount++;
} catch (e) {
console.error("[GM_download Exception] GM_download不可用:", e);
utils.showToast('GM_download不可用,使用备用下载...', 3000);
core.fetchAndDownload(img.url, filename);
submittedCount++;
}
}
if (endIndex < totalImages) {
setTimeout(() => downloadBatch(endIndex), 1000);
} else {
utils.showToast(`已提交 ${submittedCount} / ${totalImages} 个下载任务。`);
}
};
downloadBatch(0);
},
//备用下载方式
fetchAndDownload: async (url, filename) => {
try {
const response = await fetch(url, { headers: { "Referer": window.location.href } });
if (!response.ok) throw new Error(`HTTP error ${response.status}`);
const blob = await response.blob();
if (!blob.size) throw new Error("下载的文件为空");
const downloadUrl = window.URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.href = downloadUrl;
anchor.download = filename.split('/').pop();
document.body.appendChild(anchor);
anchor.click();
setTimeout(() => { document.body.removeChild(anchor); window.URL.revokeObjectURL(downloadUrl); }, 100);
} catch (err) {
console.error(`[Fetch Download Error] ${filename}:`, err);
utils.showToast(`备用下载失败: ${filename.split('/').pop()} (${err.message})`, 5000);
}
}
};
// --- 卡片处理模块 ---
const cardHandler = {
initCardDownloads: function (pageConfig) {
utils.waitForElement(pageConfig.LIST_CONTAINER_SELECTOR, (listContainer) => {
if (!listContainer) {
console.error(`[B站图片下载] 错误:未能找到列表容器: ${pageConfig.LIST_CONTAINER_SELECTOR}`);
return;
}
this.setupEventListeners(listContainer, pageConfig);
this.setupMutationObserver(listContainer, pageConfig);
}, 20000);
},
setupEventListeners: function (container, pageConfig) {
container.addEventListener('mouseenter', (event) => {
const moreButton = event.target.closest(pageConfig.MORE_BUTTON_SELECTOR);
if (moreButton && !moreButton.dataset.downloadHandlerAttached) {
moreButton.dataset.downloadHandlerAttached = 'true';
this.addDownloadOptionWhenMenuReady(moreButton, pageConfig);
}
}, true);
container.addEventListener('click', (event) => {
const moreButton = event.target.closest(pageConfig.MORE_BUTTON_SELECTOR);
if (moreButton && !moreButton.dataset.downloadOptionAdded) {
this.addDownloadOptionWhenMenuReady(moreButton, pageConfig);
}
}, true);
},
setupMutationObserver: function (container, pageConfig) {
const observer = new MutationObserver((mutations) => { });
observer.observe(container, { childList: true, subtree: false });
},
addDownloadOptionWhenMenuReady: function (moreButton, pageConfig) {
setTimeout(() => {
const parentMoreWrapper = moreButton.closest('.bili-dyn-item__more') || moreButton.closest('div[class*="more"]');
if (!parentMoreWrapper) return;
const cascader = parentMoreWrapper.querySelector(pageConfig.CASCADER_SELECTOR);
if (!cascader) return;
const optionsContainer = cascader.querySelector(pageConfig.OPTIONS_SELECTOR);
if (optionsContainer && !optionsContainer.querySelector('.download-images-option')) {
const downloadItem = document.createElement('div');
downloadItem.className = 'download-images-option';
if (optionsContainer.classList.contains('bili-cascader-options')) {
downloadItem.classList.add('bili-cascader-options__item');
downloadItem.innerHTML = ``;
} else if (optionsContainer.classList.contains('bili-dyn-more__menu')) {
downloadItem.classList.add('bili-dyn-more__menu__item');
downloadItem.style.cssText = 'height: 25px; line-height: 25px; padding: 0px 12px; text-align: left;';
downloadItem.textContent = '下载图片';
} else {
downloadItem.textContent = '下载图片';
downloadItem.style.padding = '5px 10px';
}
optionsContainer.prepend(downloadItem);
moreButton.dataset.downloadOptionAdded = 'true';
downloadItem.addEventListener('click', (e) => {
e.stopPropagation();
e.preventDefault();
const card = moreButton.closest(pageConfig.CARD_SELECTOR);
if (card) {
const { username, itemId, images } = pageConfig.getInfoFunction(card, pageConfig);
if (images && images.length > 0) {
core.downloadImages(images, username, itemId);
} else {
utils.showToast('在该动态中未找到可下载的图片');
}
} else {
utils.showToast('无法定位动态卡片');
}
});
} else if (optionsContainer && optionsContainer.querySelector('.download-images-option')) {
moreButton.dataset.downloadOptionAdded = 'true';
}
}, 150);
},
getInfoFromCard: function (card, pageConfig) {
let username = '未知UP主', itemId = `卡片_${Date.now()}`, images = [], seenImageKeys = new Set();
// 从配置中读取选择器获取用户名
for (const selector of pageConfig.USERNAME_SELECTORS) {
const elem = card.querySelector(selector);
if (elem && elem.textContent.trim()) {
username = elem.textContent.trim();
break;
}
}
// 从配置中读取选择器获取动态ID
const origCardElement = card.querySelector(pageConfig.DYN_ID_SELECTOR);
itemId = origCardElement?.getAttribute('data-origin-did') ||
origCardElement?.getAttribute('dyn-id') ||
card.getAttribute('data-did') ||
card.getAttribute('dyn-id') ||
itemId;
if (itemId.startsWith('卡片_')) {
// 从配置中读取选择器获取后备链接
const linkElement = card.querySelector(pageConfig.FALLBACK_LINK_SELECTOR);
if (linkElement) {
const idMatch = linkElement.href.match(CONFIG.DETAIL_PAGE_ID_REGEX);
if (idMatch?.[1]) itemId = idMatch[1];
}
}
const imgElements = card.querySelectorAll(pageConfig.IMAGE_SELECTORS.join(', '));
imgElements.forEach(img => {
const rawUrl = img.src || img.dataset.src;
const processedUrl = utils.processImageUrl(rawUrl);
if (processedUrl) {
let imageKey = processedUrl;
const bfsIndex = processedUrl.indexOf('/bfs/');
if (bfsIndex !== -1) imageKey = processedUrl.substring(bfsIndex);
if (!seenImageKeys.has(imageKey)) {
seenImageKeys.add(imageKey);
const formatMatch = processedUrl.match(/\.(jpg|jpeg|png|gif|webp|avif)(?:[?#]|$)/i);
images.push({ url: processedUrl, format: formatMatch ? formatMatch[1].toLowerCase() : 'jpg' });
}
}
});
if (pageConfig.RICH_TEXT_PICS_SELECTOR) {
card.querySelectorAll(pageConfig.RICH_TEXT_PICS_SELECTOR).forEach(span => {
try {
const picsData = JSON.parse(span.dataset.pics);
if (Array.isArray(picsData)) {
picsData.forEach(picInfo => {
if (picInfo && picInfo.src) {
const processedUrl = utils.processImageUrl(picInfo.src);
if (processedUrl) {
let imageKey = processedUrl;
const bfsIndex = processedUrl.indexOf('/bfs/');
if (bfsIndex !== -1) imageKey = processedUrl.substring(bfsIndex);
if (!seenImageKeys.has(imageKey)) {
seenImageKeys.add(imageKey);
const formatMatch = processedUrl.match(/\.(jpg|jpeg|png|gif|webp|avif)(?:[?#]|$)/i);
images.push({ url: processedUrl, format: formatMatch ? formatMatch[1].toLowerCase() : 'jpg' });
}
}
}
});
}
} catch (e) { console.warn('[B站图片下载] 解析卡片内 data-pics JSON 失败:', e, span.dataset.pics); }
});
}
return { username: username, itemId, images };
}
};
// --- 初始化 ---
(function init() {
settingsManager.init();
const hostname = location.hostname;
const pathname = location.pathname;
const isDynamicHome = hostname === 't.bilibili.com' && pathname === '/';
const isUserDynamicPage = hostname === 'space.bilibili.com' && pathname.includes('/dynamic');
const isTopicPage = hostname === 'www.bilibili.com' && pathname.includes('/v/topic');
const isDynamicDetailPage = hostname === 't.bilibili.com' && pathname.match(CONFIG.DETAIL_PAGE_ID_REGEX);
const isOpusPage = hostname === 'www.bilibili.com' && pathname.startsWith('/opus/');
let pageConfig = null;
if (isDynamicHome || isUserDynamicPage || isTopicPage) {
pageConfig = { ...CONFIG.CARD_CONFIG };
pageConfig.getInfoFunction = cardHandler.getInfoFromCard;
if (isDynamicHome) pageConfig.pageName = '动态首页';
else if (isUserDynamicPage) pageConfig.pageName = '用户空间动态';
else if (isTopicPage) {
pageConfig.pageName = '话题页';
pageConfig.CARD_SELECTOR = '.list__topic-card';
pageConfig.LIST_CONTAINER_SELECTOR = '.list-view.topic-list__flow-list';
}
if (pageConfig.LIST_CONTAINER_SELECTOR) {
cardHandler.initCardDownloads(pageConfig);
}
}
else if (isDynamicDetailPage || isOpusPage) {
function triggerDownloadForDetailPage() {
const username = core.getUsernameFromPage();
const itemId = core.getItemIdFromPage();
const container = core.findImageContainerOnPage();
if (!container) {
utils.showToast('未找到图片容器!');
return;
}
const requiresExpansionTrigger = container.querySelector('.bili-album__preview.more, .bili-album__preview--more, .total-mask, .bili-album-trigger--more');
const expandButton = container.querySelector('.bili-album__preview--more button, .bili-album__preview__picture:last-child, .bili-album-trigger--more');
if (requiresExpansionTrigger && expandButton) {
expandButton.click();
utils.showToast('正在展开相册...', 3000);
core.monitorAlbumExpansion(username, itemId);
} else {
const images = core.extractImagesFromContainer(container);
core.downloadImages(images, username, itemId);
}
}
const toolbarExists = CONFIG.DETAIL_PAGE_SELECTORS.SIDE_TOOLBAR.some(selector => document.querySelector(selector));
if (toolbarExists) {
utils.waitForElement(CONFIG.DETAIL_PAGE_SELECTORS.SIDE_TOOLBAR_TARGET, (actualToolbarBox) => {
if (actualToolbarBox.querySelector('#bili-custom-sidebar-download-button')) return;
const downloadAction = document.createElement('div'); downloadAction.id = 'bili-custom-sidebar-download-button'; downloadAction.className = 'side-toolbar__action download';
downloadAction.innerHTML = `下载
`;
downloadAction.addEventListener('click', triggerDownloadForDetailPage);
if (actualToolbarBox.firstChild) actualToolbarBox.insertBefore(downloadAction, actualToolbarBox.firstChild); else actualToolbarBox.appendChild(downloadAction);
}, 1000);
} else {
if (document.getElementById('bili-download-images-button')) return;
const downloadButton = document.createElement('button'); downloadButton.id = 'bili-download-images-button'; downloadButton.textContent = '下载图片';
downloadButton.addEventListener('click', triggerDownloadForDetailPage);
document.body.appendChild(downloadButton);
}
}
})();
})();