// ==UserScript== // @name Bilibili Article Image/GIF One-Click Downloader // @name:zh-TW Bilibili 專欄圖片/GIF 一鍵下載器 // @name:zh-CN Bilibili 专栏图片/GIF 一键下载器 // @namespace http://tampermonkey.net/ // @version 2.2 // @description One-click download of images/GIFs from Bilibili article posts, excluding avatars and comment images. Displays progress and completion notifications, with filenames including the post ID! Supports both fast download and sequential download modes, using GM_download for packaging and downloading. // @description:zh-TW 一鍵下載 Bilibili 專欄貼文圖片/GIF,排除頭像與留言圖,顯示進度與完成提示,檔名含貼文 ID!使用 GM_download 進行下載打包,快速下載、逐一下載兩種模式 // @description:zh-CN 一键下载 Bilibili 专栏贴文图片/GIF,排除头像与留言图,显示进度与完成提示,档名含贴文 ID!使用 GM_download 进行下载打包,快速下载、逐一下载两种模式 // @author ChatGPT // @match https://www.bilibili.com/opus/* // @grant GM_download // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/541929/Bilibili%20Article%20ImageGIF%20One-Click%20Downloader.user.js // @updateURL https://update.greasyfork.icu/scripts/541929/Bilibili%20Article%20ImageGIF%20One-Click%20Downloader.meta.js // ==/UserScript== (function () { 'use strict'; let fastMode = false; // 下載模式狀態(預設關) window.addEventListener('load', () => { setTimeout(addDownloadControls, 200); }); // 加入按鈕與快速模式開關 function addDownloadControls() { if (document.querySelector('#bili-download-button')) return; // 下載按鈕 const button = document.createElement('button'); button.textContent = '📥 逐張圖片下載'; button.id = 'bili-download-button'; Object.assign(button.style, { position: 'fixed', bottom: '60px', left: '20px', zIndex: 9999, padding: '10px 15px', backgroundColor: '#00a1d6', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer', fontSize: '14px', }); // 快速下載切換按鈕 const toggle = document.createElement('button'); toggle.textContent = `⚡ 快速下載模式:❌`; toggle.id = 'bili-download-toggle'; Object.assign(toggle.style, { position: 'fixed', bottom: '20px', left: '20px', zIndex: 9999, padding: '6px 12px', backgroundColor: '#00a1d6', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer', fontSize: '14px', }); // 點擊切換快速下載狀態和按鈕文字 toggle.addEventListener('click', () => { fastMode = !fastMode; toggle.textContent = `⚡ 快速下載模式:${fastMode ? '✅' : '❌'}`; }); // 按下載鈕開始下載,根據 fastMode 判斷模式 button.addEventListener('click', () => { button.disabled = true; collectImageUrls(button, fastMode); }); document.body.appendChild(button); document.body.appendChild(toggle); } // 延遲用 function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // 從網址取得貼文 ID function getPostIdFromUrl() { const match = window.location.pathname.match(/\/opus\/(\d+)/); return match ? match[1] : 'unknown'; } // 過濾圖片來源,排除頭像、留言圖等 function collectImageUrls(button, fastMode) { const contentContainer = document.querySelector('.article-content, .normal-post, .opus-detail'); if (!contentContainer) { alert("⚠️ 無法找到貼文內容區塊!"); button.disabled = false; return; } const images = Array.from(contentContainer.querySelectorAll('img')); const urls = []; images.forEach(img => { let url = img.src || img.getAttribute('data-src'); if (!url || url.startsWith('data:')) return; const classList = img.className || ''; const isAvatarClass = classList.includes('avatar') || classList.includes('user-face') || classList.includes('bili-avatar'); const isCommentAvatar = img.closest('.reaction-item__face'); const isAuthorAvatar = img.closest('.opus-module-author__avatar') || img.closest('.opus-module-author__decorate'); const isGlobalAvatar = img.closest('#user-avatar'); const isTooSmall = (img.naturalWidth && img.naturalWidth <= 60) && (img.naturalHeight && img.naturalHeight <= 60); if (isAvatarClass || isCommentAvatar || isAuthorAvatar || isGlobalAvatar || isTooSmall) return; url = url.replace(/@.*$/, ''); urls.push(url); }); if (urls.length === 0) { alert("⚠️ 沒有找到可下載的圖片或 GIF。"); button.disabled = false; return; } const postId = getPostIdFromUrl(); downloadMedia(urls, button, postId, fastMode); } // 主下載邏輯 async function downloadMedia(urls, button, postId, fastMode) { let success = 0; if (fastMode) { // ✅ 快速模式:不等待單張完成,但追蹤下載狀態 const downloadTasks = urls.map((url, i) => { return new Promise((resolve) => { const isGif = url.includes('.gif') || url.includes('image/gif'); const ext = isGif ? 'gif' : 'png'; const filename = `${postId}_${String(i + 1).padStart(4, '0')}.${ext}`; try { GM_download({ url, name: filename, saveAs: false, onload: () => { success++; button.textContent = `下載中 (${success}/${urls.length})`; resolve(); }, onerror: (err) => { console.warn(`❌ 無法下載: ${url}`, err); resolve(); } }); } catch (e) { console.error(`❌ GM_download 錯誤: ${url}`, e); resolve(); } }).then(() => sleep(100)); // 每個任務之間仍保留 100ms 間隔 }); await Promise.all(downloadTasks); // 顯示下載完成提示 button.textContent = '✅ 下載完成'; } else { // 🧱 逐一模式:等待每張下載完成才下一張 for (let i = 0; i < urls.length; i++) { const url = urls[i]; const isGif = url.includes('.gif') || url.includes('image/gif'); const ext = isGif ? 'gif' : 'png'; const filename = `${postId}_${String(i + 1).padStart(4, '0')}.${ext}`; await new Promise((resolve) => { try { GM_download({ url, name: filename, saveAs: false, onload: () => { success++; button.textContent = `下載中 (${success}/${urls.length})`; resolve(); }, onerror: (err) => { console.warn(`❌ 無法下載: ${url}`, err); resolve(); } }); } catch (e) { console.error(`❌ GM_download 錯誤: ${url}`, e); resolve(); } }); } button.textContent = '✅ 下載完成'; } // 恢復按鈕狀態 setTimeout(() => { button.disabled = false; button.textContent = '📥 逐張圖片下載'; }, 30000); } })();