// ==UserScript==
// @name WordPress網站圖片原檔下載器
// @namespace https://greasyfork.org/scripts/544379
// @version 1.1
// @description WordPress網站圖片原檔批量下載器 - 優先原始圖片,CRC32去重,自動檢測網站是否為WordPress網站
// @author fmnijk
// @license MIT
// @match https://*/*
// @match http://*/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @connect *
// @require https://cdn.jsdelivr.net/npm/[email protected]/umd/index.js
// @require https://cdnjs.cloudflare.com/ajax/libs/crc-32/1.2.2/crc32.min.js
// @icon https://s0.wp.com/i/favicon.ico
// ==/UserScript==
(function() {
'use strict';
let isDownloading = false;
let shouldStop = false;
// 全域開關變數(不顯示在UI上)
const useAPIDownload = true;
const useImgElements = true;
// 跳過的附檔名
const skipExtensions = ['.svg'];
// 改進版的WordPress檢測函數
async function isWordPressSite() {
let score = 0;
const checks = [];
// 1. 檢查generator標籤 (權重: 3)
const generatorMeta = document.querySelector('meta[name="generator"][content*="WordPress"]');
if (generatorMeta) {
score += 3;
checks.push('generator標籤');
}
// 2. 檢查WordPress特有的CSS/JS檔案 (權重: 2)
const wpAssets = document.querySelectorAll('link[href*="wp-includes"], script[src*="wp-includes"]');
if (wpAssets.length > 0) {
score += 2;
checks.push('wp-includes資源');
}
// 3. 檢查WordPress特有的CSS類名 (權重: 1)
const wpClasses = ['wp-admin-bar', 'wp-block', 'wp-content', 'wp-caption'];
const foundClasses = wpClasses.filter(cls => document.querySelector(`.${cls}`));
if (foundClasses.length > 0) {
score += Math.min(foundClasses.length, 2);
checks.push(`WordPress類名: ${foundClasses.join(', ')}`);
}
// 4. 檢查WordPress特有的HTML註釋 (權重: 1)
const htmlContent = document.documentElement.outerHTML;
const wpComments = [
/<!--.*?WordPress.*?-->/i,
/<!--.*?wp-.*?-->/i,
/<!--.*?WP_.*?-->/i
];
if (wpComments.some(regex => regex.test(htmlContent))) {
score += 1;
checks.push('WordPress註釋');
}
// 5. 檢查WordPress REST API (權重: 3) - 最可靠的方法
try {
const apiResponse = await fetch(`${location.origin}/wp-json/wp/v2/types`, {
method: 'HEAD',
timeout: 5000
});
if (apiResponse.ok) {
score += 3;
checks.push('REST API可用');
}
} catch (e) {
// API不可用,不加分也不扣分
}
// 6. 檢查wp-content路徑(降低權重,因為可能被其他CMS使用)
if (/\/wp-content\//i.test(htmlContent)) {
score += 1;
checks.push('wp-content路徑');
}
// 7. 檢查WordPress特有的函數或變數 (權重: 2)
if (window.wp || window.wpApiSettings || window.wc_add_to_cart_params) {
score += 2;
checks.push('WordPress JS物件');
}
// 8. 檢查WordPress特有的body class (權重: 1)
const bodyClasses = document.body.className;
if (/\b(wordpress|wp-|page-id-|postid-)\b/i.test(bodyClasses)) {
score += 1;
checks.push('WordPress body類名');
}
console.log(`[WP檢測] 得分: ${score}, 檢測到: ${checks.join(', ')}`);
// 設定閾值:得分>=4才認為是WordPress網站
return score >= 4;
}
async function testAPI() {
try {
const response = await fetch(`${location.origin}/wp-json/wp/v2/media?per_page=1`);
return response.ok;
} catch { return false; }
}
async function checkImageExists(url) {
try {
const response = await fetch(url, { method: 'HEAD' });
return response.ok;
} catch { return false; }
}
// 檢查是否應該跳過此檔案
function shouldSkipFile(url) {
const lowerUrl = url.toLowerCase();
return skipExtensions.some(ext => lowerUrl.includes(ext));
}
// 簡化樣式
GM_addStyle(`
#wp-dl {
position: fixed; top: 20px; right: 20px;
background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%);
border: 1px solid #404040; border-radius: 12px; padding: 16px;
box-shadow: 0 8px 32px rgba(0,0,0,0.4); z-index: 10000;
font: 13px/1.4 -apple-system, BlinkMacSystemFont, sans-serif;
min-width: 280px; color: #e0e0e0; backdrop-filter: blur(10px);
}
#wp-dl .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
#wp-dl h3 { margin: 0; color: #64b5f6; font-size: 15px; font-weight: 600; }
#wp-dl .close {
background: #f44336; color: white; border: none; width: 24px; height: 24px;
border-radius: 50%; cursor: pointer; font-size: 12px; font-weight: bold;
}
#wp-dl .close:hover { background: #d32f2f; }
#wp-dl button:not(.close) {
background: linear-gradient(135deg, #2196f3 0%, #1976d2 100%);
color: white; border: none; padding: 10px 16px; border-radius: 8px;
cursor: pointer; margin: 4px 0; width: 100%; font-weight: 500; transition: all 0.2s ease;
}
#wp-dl button:hover:not(:disabled) { background: linear-gradient(135deg, #1976d2 0%, #1565c0 100%); }
#wp-dl button:disabled { background: #424242 !important; opacity: 0.6; cursor: not-allowed; }
#wp-dl .info {
background: rgba(255,255,255,0.05); padding: 10px; border-radius: 6px;
margin: 6px 0; border-left: 3px solid #64b5f6;
}
#wp-dl .details { font-size: 11px; color: #b0b0b0; margin-top: 4px; }
`);
// 簡化UI
function createUI() {
const ui = document.createElement('div');
ui.id = 'wp-dl';
ui.innerHTML = `
<div class="header">
<h3>🖼️ WP圖片下載器</h3>
<button class="close">×</button>
</div>
<button id="start">開始下載(CRC32去重)</button>
<button id="stop" disabled>停止</button>
<div class="info" id="status">準備就緒</div>
<div class="details" id="details">優先original,CRC32去重,跳過SVG</div>
`;
document.body.appendChild(ui);
ui.querySelector('.close').onclick = () => ui.remove();
ui.querySelector('#start').onclick = startDownload;
ui.querySelector('#stop').onclick = () => {
shouldStop = true;
};
}
function updateStatus(msg, detail = '') {
const status = document.getElementById('status');
const details = document.getElementById('details');
if (status) status.textContent = msg;
if (details && detail) details.textContent = detail;
console.log(`[WP-DL] ${msg}${detail ? ' | ' + detail : ''}`);
}
// 解碼URL檔名
function decodeFilename(filename) {
try {
return decodeURIComponent(filename);
} catch {
return filename;
}
}
function cleanFileName(title, id) {
const cleanTitle = title || `image_${id}`;
return cleanTitle
.replace(/[<>:"/\\|?*\x00-\x1f]/g, '_')
.replace(/\s+/g, '_')
.replace(/_+/g, '_')
.trim();
}
// 下載圖片並返回數據和CRC32
function downloadImage(url, filename) {
return new Promise(resolve => {
GM_xmlhttpRequest({
method: 'GET',
url: url,
responseType: 'arraybuffer',
timeout: 30000,
onload: res => {
if (res.status === 200) {
const data = new Uint8Array(res.response);
const hash = CRC32.buf(data).toString(16);
resolve({ data, filename: decodeFilename(filename), hash, url });
} else {
resolve(null);
}
},
onerror: () => resolve(null),
ontimeout: () => resolve(null)
});
});
}
// 處理單個媒體項目
function processMediaItem(item) {
const base = cleanFileName(item.title?.rendered, item.id);
const domain = location.origin;
// 優先使用 original_image
if (item.media_details?.original_image) {
const filePath = item.media_details.file;
const originalName = item.media_details.original_image;
const pathParts = filePath.split('/');
pathParts[pathParts.length - 1] = originalName;
const url = `${domain}/wp-content/uploads/${pathParts.join('/')}`;
const ext = originalName.split('.').pop();
return {
url,
filename: `${base}.${ext}`,
baseKey: base,
isOriginal: true
};
} else {
const url = item.media_details?.sizes?.full?.source_url || item.source_url;
const ext = url.split('.').pop().split('?')[0];
return {
url,
filename: `${base}.${ext}`,
baseKey: base,
isOriginal: false
};
}
}
// 獲取API圖片
async function getAPIImages() {
const images = [];
let page = 1;
while (!shouldStop) {
try {
const res = await fetch(`${location.origin}/wp-json/wp/v2/media?per_page=100&page=${page}`);
if (!res.ok) break;
const data = await res.json();
if (!data?.length) break;
const imageItems = data
.filter(item => item.media_type === 'image')
.map(processMediaItem)
.filter(item => !shouldSkipFile(item.url)); // 過濾掉要跳過的檔案
images.push(...imageItems);
updateStatus(`掃描API第${page}頁`, `找到${images.length}張圖片`);
page++;
await new Promise(r => setTimeout(r, 100));
} catch (e) {
console.log(`獲取第${page}頁失敗:`, e);
break;
}
}
return images;
}
// 處理單個圖片元素
async function processImageElement(img, index) {
const src = img.src;
// 跳過不相關的圖片和要跳過的檔案類型
if (!src || src.startsWith('data:') || src.includes('base64') || shouldSkipFile(src)) {
return null;
}
console.log(`${index + 1}. ${src}`);
// 嘗試移除scale後綴獲得原始圖片URL
const originalSrc = src.replace(/-scale.*\./g, '.');
const isScaled = src !== originalSrc;
let finalUrl = src;
let isOriginal = !isScaled;
let filename = decodeFilename(src.split('/').pop().split('?')[0]);
// 如果是scaled版本,檢查原始版本是否存在
if (isScaled) {
const originalExists = await checkImageExists(originalSrc);
if (originalExists) {
finalUrl = originalSrc;
isOriginal = true;
filename = decodeFilename(originalSrc.split('/').pop().split('?')[0]);
console.log(` → 使用原始版本: ${originalSrc}`);
} else {
console.log(` → 原始版本不存在,使用scaled版本`);
}
}
const baseKey = filename.replace(/\.[^.]+$/, '');
return {
url: finalUrl,
filename,
baseKey,
isOriginal
};
}
// 使用您建議的方法獲取頁面圖片
async function getPageImages() {
const images = [];
const imgElements = Array.from(document.querySelectorAll('img'));
updateStatus(`🔍 掃描頁面圖片...`, `發現${imgElements.length}個img標籤`);
for (let i = 0; i < imgElements.length && !shouldStop; i++) {
const img = imgElements[i];
const result = await processImageElement(img, i);
if (result) {
images.push(result);
}
// 更新進度
if (i % 10 === 0) {
updateStatus(`🔍 掃描頁面圖片 ${i + 1}/${imgElements.length}`, `已處理${images.length}張`);
}
}
return images;
}
// CRC32去重函數
function deduplicateByCRC32(downloadedImages) {
const hashMap = new Map();
downloadedImages.forEach(img => {
if (!hashMap.has(img.hash)) {
hashMap.set(img.hash, []);
}
hashMap.get(img.hash).push(img);
});
const uniqueImages = {};
let removedCount = 0;
hashMap.forEach(duplicates => {
// 按檔名排序,取第一個
duplicates.sort((a, b) => a.filename.localeCompare(b.filename));
const chosen = duplicates[0];
uniqueImages[chosen.filename] = chosen.data;
// 記錄去重數量
if (duplicates.length > 1) {
removedCount += duplicates.length - 1;
console.log(`[去重] ${chosen.filename} 有 ${duplicates.length} 個重複檔案`);
}
});
console.log(`[去重完成] 移除了 ${removedCount} 個重複檔案`);
return uniqueImages;
}
// 分片壓縮函數
async function createZipInChunks(files) {
return new Promise(resolve => {
setTimeout(() => {
const zipData = fflate.zipSync(files, { level: 0 }); // 無壓縮,最快速度
resolve(zipData);
}, 0);
});
}
// 主下載函數
async function startDownload() {
if (isDownloading) return;
isDownloading = true;
shouldStop = false;
const startBtn = document.getElementById('start');
const stopBtn = document.getElementById('stop');
startBtn.disabled = true;
stopBtn.disabled = false;
try {
const allImages = [];
if (useAPIDownload) {
updateStatus('🔍 掃描API圖片...');
const apiImages = await getAPIImages();
allImages.push(...apiImages);
console.log(`[API] 找到 ${apiImages.length} 張圖片`);
}
if (useImgElements) {
updateStatus('🔍 掃描頁面圖片...');
const pageImages = await getPageImages();
allImages.push(...pageImages);
console.log(`[頁面] 找到 ${pageImages.length} 張圖片`);
}
if (!allImages.length) {
updateStatus('❌ 未找到圖片');
return;
}
// 基礎去重(相同baseKey優先保留原始)
const imageMap = new Map();
allImages.forEach(info => {
const existing = imageMap.get(info.baseKey);
if (!existing || (info.isOriginal && !existing.isOriginal)) {
imageMap.set(info.baseKey, info);
}
});
const finalImages = Array.from(imageMap.values());
updateStatus(`📦 開始下載${finalImages.length}張`, '準備CRC32去重');
// 批量下載
const downloadedImages = [];
let success = 0;
for (let i = 0; i < finalImages.length && !shouldStop; i += 8) {
const batch = finalImages.slice(i, i + 8);
const results = await Promise.all(
batch.map(info => downloadImage(info.url, info.filename))
);
// 使用 filter 替代 forEach
const validResults = results.filter(result => result !== null);
downloadedImages.push(...validResults);
success += validResults.length;
updateStatus(`⬇️ 已下載 ${success}/${finalImages.length}`, 'CRC32計算中');
}
if (!downloadedImages.length) {
updateStatus('❌ 下載失敗');
return;
}
// CRC32去重
updateStatus('🔄 CRC32去重中...', `${downloadedImages.length}張圖片`);
const uniqueFiles = deduplicateByCRC32(downloadedImages);
const uniqueCount = Object.keys(uniqueFiles).length;
const duplicateCount = downloadedImages.length - uniqueCount;
// 分片壓縮
updateStatus('📦 打包中...', `${uniqueCount}張 (去重${duplicateCount}張)`);
const zipData = await createZipInChunks(uniqueFiles);
const blob = new Blob([zipData], { type: 'application/zip' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `wp_images_${Date.now()}.zip`;
a.click();
URL.revokeObjectURL(url);
updateStatus('✅ 完成!', `下載${uniqueCount}張 (去重${duplicateCount}張)`);
} catch (e) {
updateStatus('❌ 錯誤', e.message);
console.error('[WP-DL] 錯誤:', e);
} finally {
isDownloading = false;
startBtn.disabled = false;
stopBtn.disabled = true;
}
}
// 初始化
async function init() {
const isWP = await isWordPressSite();
if (!isWP) {
console.log('[WP-DL] 非WordPress網站,腳本不會載入');
return;
}
if (useAPIDownload && !await testAPI()) {
console.log('[WP-DL] API不可用,僅使用頁面掃描模式');
}
createUI();
console.log('[WP-DL] 已載入 - WordPress網站檢測通過');
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();