您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
WordPress網站圖片原檔批量下載器 - 優先原始圖片,CRC32去重,自動檢測網站是否為WordPress網站
// ==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(); } })();