// ==UserScript== // @name Bilibili Article Image/GIF One-Click Downloader // @name:zh-TW Bilibili 專欄圖片/GIF 一鍵下載器 // @name:zh-CN Bilibili 专栏图片/GIF 一键下载器 // @namespace http://tampermonkey.net/ // @version 1.8 // @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! Uses GM_download for downloading and packaging. // @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 none // ==/UserScript== (function () { 'use strict'; // 等待頁面加載完成後延遲 200ms 執行新增下載按鈕 window.addEventListener('load', () => { setTimeout(addDownloadButton, 200); }); // 新增「逐張圖片下載」按鈕 function addDownloadButton() { // 若按鈕已存在則不重複新增 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', top: '100px', right: '20px', zIndex: 9999, padding: '10px 15px', backgroundColor: '#00a1d6', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer', fontSize: '14px', }); // 點擊按鈕時禁用按鈕,開始蒐集圖片 URL 並下載 button.addEventListener('click', () => { button.disabled = true; collectImageUrls(button); }); // 將按鈕加入網頁中 document.body.appendChild(button); } // 簡單延遲函式,回傳 Promise,方便 async/await 使用 function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // 從網址中取得貼文 ID (例:https://www.bilibili.com/opus/1045724253437034499) function getPostIdFromUrl() { const match = window.location.pathname.match(/\/opus\/(\d+)/); return match ? match[1] : 'unknown'; } // 蒐集貼文區內所有可下載圖片的 URL(排除頭像與留言圖) function collectImageUrls(button) { // 尋找貼文內容容器 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 => { // 取得圖片來源 URL,優先用 src,備用 data-src let url = img.src || img.getAttribute('data-src'); if (!url || url.startsWith('data:')) return; // 過濾 base64 編碼圖片 // 排除條件: const classList = img.className || ''; // 常見頭像 class 名稱 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 = url.replace(/@.*$/, ''); // 收錄可下載的圖片 URL urls.push(url); }); if (urls.length === 0) { alert("⚠️ 沒有找到可下載的圖片或 GIF。"); button.disabled = false; // 恢復按鈕 return; } // 取得貼文 ID 並開始下載 const postId = getPostIdFromUrl(); downloadMedia(urls, button, postId); } // 下載媒體檔案,逐張下載且等待下載完成後繼續下一張 async function downloadMedia(urls, button, postId) { let success = 0; for (let i = 0; i < urls.length; i++) { const url = urls[i]; // 判斷是否為 GIF 檔案 const isGif = url.includes('.gif') || url.includes('image/gif'); const ext = isGif ? 'gif' : 'png'; // 組合檔名,格式:貼文ID_序號 const filename = `${postId}_${String(i + 1).padStart(4, '0')}.${ext}`; // 透過 Promise 包裝 GM_download,等待單張下載完成再繼續 await new Promise((resolve) => { try { GM_download({ url, name: filename, saveAs: false, // 單張下載成功,更新成功數與按鈕文字,並解決 Promise onload: () => { success++; button.textContent = `下載中 (${success}/${urls.length})`; resolve(); }, // 下載失敗,印出警告,仍解決 Promise 以繼續下一張 onerror: (err) => { console.warn(`❌ 無法下載: ${url}`, err); resolve(); } }); } catch (e) { console.error(`❌ GM_download 錯誤: ${url}`, e); resolve(); } }); } // 全部下載完畢後,更新按鈕文字為完成提示 button.textContent = '✅ 下載完成'; // 30秒後恢復按鈕文字與啟用狀態 setTimeout(() => { button.disabled = false; button.textContent = '📥 逐張圖片下載'; }, 30000); } })();