孔夫子旧书网无水印图片下载助手

一键批量下载孔夫子旧书网商品图片(无水印版本)

// ==UserScript==
// @name         孔夫子旧书网无水印图片下载助手
// @description  一键批量下载孔夫子旧书网商品图片(无水印版本)
// @version      1.0.5
// @author       骄阳哥
// @namespace    jyg
// @match        *://search.kongfz.com/product_result/*
// @match        *://book.kongfz.com/*
// @match        *://item.kongfz.com/book/*
// @grant        GM_addStyle
// @grant        GM_download
// @grant        GM_xmlhttpRequest
// @connect      www0.kfzimg.com
// @connect      *
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // 当前页面URL
    const currentUrl = window.location.href;

    // 在顶部添加常量定义
    const STORAGE_KEY = 'kfz_crop_height';
    const DEFAULT_CROP_HEIGHT = 110;

    // 获取裁剪高度设置
    function getCropHeight() {
        const saved = localStorage.getItem(STORAGE_KEY);
        return saved ? parseInt(saved) : DEFAULT_CROP_HEIGHT;
    }

    // 移除图片水印
    function removeImageWatermark(imgUrl) {
        return imgUrl.replace(/(_water|_n|_p|_b|_s)/g, '');
    }

    // 创建商品详情页下载按钮
    function createDetailPageButton(images) {
        const container = document.createElement('div');
        container.id = 'kfz-download-container';

        // 创建设置区域
        const settingDiv = document.createElement('div');
        settingDiv.className = 'crop-setting';

        const label = document.createElement('label');
        label.innerText = '裁剪高度(px):';

        const input = document.createElement('input');
        input.type = 'number';
        input.min = '0';
        input.value = getCropHeight();
        input.className = 'crop-height-input';

        input.addEventListener('change', (e) => {
            const value = parseInt(e.target.value);
            if(value >= 0) {
                localStorage.setItem(STORAGE_KEY, value);
            }
        });

        settingDiv.appendChild(label);
        settingDiv.appendChild(input);

        // 创建下载按钮
        const btn = document.createElement('button');
        btn.innerText = `📥 下载全部图片(${images.length}张)`;
        btn.id = 'kfz-download-btn';
        btn.style.backgroundColor = '#1890ff';
        btn.style.color = 'white';

        container.appendChild(settingDiv);
        container.appendChild(btn);
        document.body.appendChild(container);
        return btn;
    }

    // 创建索页面下载按钮
    function createSearchPageButton(doc, item) {
        const btn = doc.createElement('button');
        btn.innerText = '📥 下载图片';
        btn.className = 'kfz-search-download-btn';
        btn.style.backgroundColor = '#1890ff';
        const cartBtn = item.querySelector('div.add-cart-btn');
        cartBtn.parentNode.insertBefore(btn, cartBtn);
        return btn;
    }

    // 创建书籍列表页下载按钮
    function createListPageButton(doc, item) {
        const btn = doc.createElement('button');
        btn.innerText = '📥 下载图片';
        btn.className = 'kfz-list-download-btn';
        btn.style.backgroundColor = '#1890ff';
        const cartBtn = item.querySelector('a.con-btn-cart');
        cartBtn.parentNode.insertBefore(btn, cartBtn.nextSibling);
        return btn;
    }

    // 获取商品图片列表
    function getBookImages(doc) {
        const imgItems = doc.querySelectorAll('ul#figure-info-box > li');
        return Array.from(imgItems, item => {
            const img = item.querySelector('img');
            const imgSrc = img ? img.getAttribute('_viewsrc') : null;
            return removeImageWatermark(imgSrc);
        });
    }

    // 修改本地图片裁剪函数
    function cropLocalImage(imageUrl) {
        return new Promise((resolve, reject) => {
            const img = new Image();
            img.crossOrigin = 'anonymous'; // 允许跨域

            img.onload = () => {
                // 创建 canvas 进行裁剪
                const canvas = document.createElement('canvas');
                const ctx = canvas.getContext('2d');

                // 使用保存的裁剪高度
                const cropHeight = getCropHeight();
                canvas.width = img.width;
                canvas.height = img.height - cropHeight;

                // 绘制裁剪后的图片
                ctx.drawImage(img, 0, 0);

                // 转换为 blob
                canvas.toBlob(blob => {
                    resolve(blob);
                }, 'image/jpeg', 0.95);
            };

            img.onerror = reject;
            img.src = imageUrl;
        });
    }

    // 添加延时函数
    function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    // 下载图片
    function downloadImages(doc, btn) {
        const images = getBookImages(doc);
        if(images.length === 0) {
            console.warn('未找到可下载的图片');
            btn.innerText = '😅 暂无可下载的图片';
            btn.style.backgroundColor = '#999';
            btn.disabled = true;
            return;
        }

        console.log(`找到${images.length}张图片待下载:`, images);
        btn.innerText = '下载中...';
        btn.disabled = true;
        btn.style.backgroundColor = '#1890ff';

        let successCount = 0;
        let failCount = 0;

        // 获取书名和ISBN
        const bookName = doc.querySelector('h1')?.innerText || '未知书名';
        const isbnInfo = doc.querySelector('meta[name="description"]').getAttribute('content').match(/ISBN:([0-9]*)/);
        const isbn = isbnInfo?.[1] || '';

        console.log('书籍信息:', {
            bookName,
            isbn
        });

        // 串行下载图片,添加重试和延迟
        async function downloadWithRetry(url, retryCount = 1, hasWatermark = false) {
            const ext = url.split('.').pop()?.toLowerCase() || 'jpg';
            const watermarkText = hasWatermark ? '-裁剪' : '';
            const fileName = `${bookName.trim()}-${isbn.trim()}-${successCount + 1}${watermarkText}.${ext}`;

            try {
                if(!hasWatermark) {
                    // 尝试下载无水印版本
                    await new Promise((resolve, reject) => {
                        GM_download({
                            url,
                            name: fileName,
                            onload: resolve,
                            onerror: reject
                        });
                    });
                    successCount++;
                    console.log(`无水印图片下载成功:`, {url, fileName});
                } else {
                    // 下载并裁剪带水印版本
                    const blob = await cropLocalImage(url);
                    const downloadUrl = URL.createObjectURL(blob);

                    await new Promise((resolve, reject) => {
                        GM_download({
                            url: downloadUrl,
                            name: fileName,
                            onload: () => {
                                URL.revokeObjectURL(downloadUrl);
                                resolve();
                            },
                            onerror: (err) => {
                                URL.revokeObjectURL(downloadUrl);
                                reject(err);
                            }
                        });
                    });

                    successCount++;
                    console.log(`裁剪图片下载成功:`, {url, fileName});
                }
            } catch(err) {
                console.warn(`图片下载失败 (${retryCount}次重试机会):`, {url, error: err});

                if(retryCount > 0) {
                    const delay = 1000 + Math.random() * 2000;
                    await sleep(delay);

                    if(!hasWatermark) {
                        const watermarkUrl = url.replace(/\.([^.]*)$/, '_b.$1');
                        return downloadWithRetry(watermarkUrl, retryCount - 1, true);
                    } else {
                        const otherWatermarkUrl = url.replace(/_b\./, '_p.');
                        return downloadWithRetry(otherWatermarkUrl, retryCount - 1, true);
                    }
                } else {
                    failCount++;
                    console.error('图片下载失败:', {url, error: err});
                }
            }

            // 更新按钮状态
            btn.innerText = `下载中...(${successCount}/${images.length})`;
            if(successCount + failCount === images.length) {
                if(failCount === 0) {
                    btn.innerText = `✅ ${successCount}张图片已下载`;
                    btn.style.backgroundColor = '#52c41a';
                } else {
                    btn.innerText = `⚠️ ${successCount}张成功, ${failCount}张失败`;
                    btn.style.backgroundColor = '#faad14';
                }
                btn.disabled = false;
            }
        }

        // 串行下载所有图片
        (async () => {
            for(const imgUrl of images) {
                if(!imgUrl) {
                    failCount++;
                    continue;
                }
                await downloadWithRetry(imgUrl);
                // 添加随机延迟
                await sleep(500 + Math.random() * 1000);
            }
        })();
    }

    // 从URL获取并下载图片
    function downloadFromUrl(url, btn) {
        btn.addEventListener('click', () => {
            console.log('开始获取页面:', url);

            GM_xmlhttpRequest({
                method: 'GET',
                url: url,
                onload: response => {
                    console.log('页面获取成功:', {
                        url,
                        status: response.status
                    });

                    const parser = new DOMParser();
                    const doc = parser.parseFromString(response.responseText, 'text/html');
                    downloadImages(doc, btn);
                },
                onerror: err => {
                    console.error('页面获取失败:', {
                        url,
                        error: err
                    });
                    btn.innerText = '❌ 获取图片失败';
                }
            });
        });
    }

    // 处理搜索页面
    function handleSearchPage(item) {
        const link = item.querySelector('.item-info > .title > a');
        const btn = createSearchPageButton(document, item);
        downloadFromUrl(link.href, btn);
    }

    // 处理列表页面
    function handleListPage(item) {
        const link = item.querySelector('div.list-con-title > a');
        const btn = createListPageButton(document, item);
        downloadFromUrl(link.href, btn);
    }

    // 初始化页面
    let checkInterval;

    if(currentUrl.includes('book.kongfz.com')) {
        const btn = createDetailPageButton(getBookImages(document));
        btn.addEventListener('click', () => downloadImages(document, btn));
    }
    else if(currentUrl.includes('search.kongfz.com/product_result')) {
        checkInterval = setInterval(() => {
            const listBox = document.querySelector('#listBox');
            if(listBox) {
                clearInterval(checkInterval);
                document.querySelectorAll('#listBox .item')
                    .forEach(item => handleSearchPage(item));
            }
        }, 1000);
    }
    else if(currentUrl.includes('item.kongfz.com/book')) {
        checkInterval = setInterval(() => {
            const listBox = document.querySelector('ul.itemList');
            if(listBox) {
                clearInterval(checkInterval);
                document.querySelectorAll('ul.itemList > li')
                    .forEach(item => handleListPage(item));
            }
        }, 1000);
    }

    // 注入样式
    GM_addStyle(`
        #kfz-download-btn {
            padding: 12px 24px;
            border: none;
            border-radius: 6px;
            cursor: pointer;
            font-size: 14px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.15);
            transition: all 0.3s;
            width: 100%;
        }

        #kfz-download-btn:hover {
            transform: translateY(-2px);
            box-shadow: 0 4px 12px rgba(0,0,0,0.2);
        }

        .kfz-search-download-btn,
        .kfz-list-download-btn {
            padding: 4px 12px;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 12px;
            margin: 0 8px;
            transition: all 0.3s;
        }

        .kfz-search-download-btn:hover,
        .kfz-list-download-btn:hover {
            opacity: 0.8;
        }

        button:disabled {
            background-color: #999 !important;
            cursor: not-allowed;
            opacity: 0.7;
        }

        #kfz-download-container {
            position: fixed;
            bottom: 30px;
            right: 30px;
            display: flex;
            flex-direction: column;
            align-items: stretch;
            gap: 10px;
            z-index: 9999;
            min-width: 200px;
        }

        .crop-setting {
            background: white;
            padding: 8px 12px;
            border-radius: 6px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.15);
            display: flex;
            align-items: center;
            gap: 8px;
            width: 100%;
        }

        .crop-height-input {
            width: 60px;
            padding: 4px;
            border: 1px solid #d9d9d9;
            border-radius: 4px;
        }

        .crop-height-input:focus {
            border-color: #1890ff;
            outline: none;
        }
    `);

})();