// ==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 = `

哔哩哔哩动态图片下载 - 设置

可用占位符: {username}, {itemId}, {date}, {index}, {format}
默认模式支持文件夹。备用不支持。
可以用/来创建文件夹,如
{username}/{username}_{itemId}_{date}_{index}.{format}
{date}指的是当前下载的时间,不是图片/动态发布时间

当默认下载模式(GM_download)出现问题或无法下载时,脚本自动使用备用模式。一般是浏览器的问题。 直接当扩展用时使用该方法
`; 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); } } })(); })();