Pixiv Batch Downloader Ultimate

批量下载Pixiv作品图片,自动重命名(终极版)

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

You will need to install an extension such as Tampermonkey to install this script.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Pixiv Batch Downloader Ultimate
// @namespace    http://tampermonkey.net/
// @version      1.5
// @description  批量下载Pixiv作品图片,自动重命名(终极版)
// @author       YoTESPRIS
// @match        https://www.pixiv.net/artworks/*
// @match        https://www.pixiv.net/*/artworks/*
// @grant        GM_download
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // 添加样式
    GM_addStyle(`
        #pixiv-downloader-btn {
            position: fixed;
            top: 20px;
            right: 20px;
            z-index: 9999;
            background: linear-gradient(45deg, #0096fa, #0066cc);
            color: white;
            border: none;
            border-radius: 6px;
            padding: 12px 18px;
            cursor: pointer;
            font-size: 14px;
            font-weight: bold;
            box-shadow: 0 4px 15px rgba(0,150,250,0.3);
            transition: all 0.3s ease;
        }
        #pixiv-downloader-btn:hover {
            transform: translateY(-2px);
            box-shadow: 0 6px 20px rgba(0,150,250,0.4);
        }
        #pixiv-downloader-btn:active {
            transform: translateY(0);
        }
        #pixiv-downloader-btn.loading {
            background: #cccccc;
            cursor: not-allowed;
            transform: none;
            box-shadow: 0 2px 5px rgba(0,0,0,0.2);
        }
        #pixiv-downloader-progress {
            position: fixed;
            top: 80px;
            right: 20px;
            z-index: 9999;
            background: rgba(0,0,0,0.9);
            color: white;
            padding: 15px;
            border-radius: 8px;
            font-size: 13px;
            display: none;
            max-width: 320px;
            backdrop-filter: blur(10px);
            border: 1px solid rgba(255,255,255,0.1);
        }
        .pixiv-downloader-file-info {
            font-size: 12px;
            color: #aaa;
            margin-top: 5px;
            word-break: break-all;
        }
        .pixiv-downloader-success-text {
            color: #4caf50;
        }
        .pixiv-downloader-error-text {
            color: #f44336;
        }
        .pixiv-downloader-warning-text {
            color: #ff9800;
        }
    `);

    // 创建下载按钮
    function createDownloadButton() {
        // 移除可能存在的旧按钮
        const oldBtn = document.getElementById('pixiv-downloader-btn');
        if (oldBtn) oldBtn.remove();

        const oldProgress = document.getElementById('pixiv-downloader-progress');
        if (oldProgress) oldProgress.remove();

        // 创建新按钮
        const btn = document.createElement('button');
        btn.id = 'pixiv-downloader-btn';
        btn.innerHTML = '⚡ 一键下载';
        btn.addEventListener('click', downloadImages);
        document.body.appendChild(btn);

        const progress = document.createElement('div');
        progress.id = 'pixiv-downloader-progress';
        document.body.appendChild(progress);
    }

    // 获取作品信息
    async function getArtworkInfo() {
        const artworkId = window.location.pathname.split('/').pop();

        // 获取标题
        let title = `pixiv_${artworkId}`;
        const titleSelectors = ['h1', '[data-gtm-value]', 'meta[property="og:title"]'];

        for (let selector of titleSelectors) {
            if (selector.startsWith('meta')) {
                const element = document.querySelector(selector);
                if (element) {
                    title = element.getAttribute('content') || title;
                    break;
                }
            } else {
                const element = document.querySelector(selector);
                if (element) {
                    title = element.textContent.trim() || title;
                    break;
                }
            }
        }

        // 清理文件名
        title = title.replace(/[<>:"/\\|?*\x00-\x1F]/g, '_').trim();
        if (!title) title = `pixiv_${artworkId}`;

        // 获取图片URLs
        let imageUrls = [];

        try {
            // 方法1: 从预加载数据获取
            const preloadData = document.getElementById('meta-preload-data');
            if (preloadData) {
                const content = preloadData.getAttribute('content');
                if (content) {
                    const data = JSON.parse(content);
                    const illustData = data.illust?.[artworkId];
                    if (illustData) {
                        const urls = illustData.urls;
                        if (urls) {
                            if (urls.original) {
                                // 单图
                                imageUrls.push(urls.original);
                            } else {
                                // 多图
                                Object.keys(urls).forEach(key => {
                                    if (urls[key]?.original) {
                                        imageUrls.push(urls[key].original);
                                    } else if (typeof urls[key] === 'string' && urls[key].includes('img-original')) {
                                        imageUrls.push(urls[key]);
                                    }
                                });
                            }
                        }
                    }
                }
            }
        } catch (e) {
            console.log('预加载数据解析失败:', e);
        }

        // 方法2: 从页面JSON数据获取
        if (imageUrls.length === 0) {
            try {
                const scripts = document.querySelectorAll('script:not([src])');
                for (let script of scripts) {
                    const text = script.textContent;
                    if (text && text.includes('illust')) {
                        // 尝试提取JSON
                        const jsonMatch = text.match(/[^\"]*\"illust\"\s*:\s*(\{[^}]+\})/);
                        if (jsonMatch && jsonMatch[1]) {
                            try {
                                const jsonData = JSON.parse(`{${jsonMatch[1]}}`);
                                const illustData = jsonData[artworkId];
                                if (illustData?.urls?.original) {
                                    imageUrls.push(illustData.urls.original);
                                }
                                break;
                            } catch (e) {
                                continue;
                            }
                        }
                    }
                }
            } catch (e) {
                console.log('JSON数据解析失败:', e);
            }
        }

        // 方法3: 从图片元素获取
        if (imageUrls.length === 0) {
            const imgElements = document.querySelectorAll('img[src*="img-original"]');
            imgElements.forEach(img => {
                let url = img.src;
                // 清理URL
                url = url.replace(/\/c\/\d+x\d+.*?\//, '/');
                url = url.replace(/\?.*$/, '');
                if (url.includes('img-original')) {
                    imageUrls.push(url);
                }
            });
        }

        // 方法4: 从meta标签获取
        if (imageUrls.length === 0) {
            const metaTags = document.querySelectorAll('meta[property="og:image"]');
            metaTags.forEach(tag => {
                let url = tag.content;
                if (url && url.includes('img-original')) {
                    url = url.replace(/\/c\/\d+x\d+.*?\//, '/');
                    url = url.replace(/\?.*$/, '');
                    imageUrls.push(url);
                }
            });
        }

        // 去重和验证
        imageUrls = [...new Set(imageUrls)].filter(url =>
            url && typeof url === 'string' && (url.includes('img-original') || url.includes('i.pximg.net'))
        );

        console.log('最终获取到的图片URLs:', imageUrls);
        return { title, imageUrls, artworkId };
    }

    // 获取文件扩展名
    function getFileExtension(url) {
        const match = url.match(/\.(\w+)(?:\?|#|$)/);
        return match ? match[1].toLowerCase() : 'jpg';
    }

    // 使用GM_xmlhttpRequest下载并保存文件
    function downloadImageAsBlob(url, filename) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: url,
                responseType: 'blob',
                headers: {
                    'Referer': 'https://www.pixiv.net/',
                    'User-Agent': navigator.userAgent
                },
                onload: function(response) {
                    try {
                        const blob = response.response;
                        const blobUrl = URL.createObjectURL(blob);

                        // 创建下载链接
                        const a = document.createElement('a');
                        a.href = blobUrl;
                        a.download = filename;
                        document.body.appendChild(a);
                        a.click();
                        document.body.removeChild(a);

                        // 清理
                        setTimeout(() => URL.revokeObjectURL(blobUrl), 1000);

                        console.log(`✅ 下载成功: ${filename}`);
                        resolve(true);
                    } catch (e) {
                        console.error(`❌ Blob下载失败: ${filename}`, e);
                        reject(false);
                    }
                },
                onerror: function(error) {
                    console.error(`❌ 请求失败: ${filename}`, error);
                    reject(false);
                }
            });
        });
    }

    // 备用下载方法
    function fallbackDownload(url, filename) {
        return new Promise((resolve, reject) => {
            try {
                const a = document.createElement('a');
                a.href = url;
                a.download = filename;
                a.target = '_blank';
                document.body.appendChild(a);
                a.click();
                document.body.removeChild(a);
                setTimeout(() => resolve(true), 500);
            } catch (e) {
                console.error('备用下载方法失败:', e);
                reject(false);
            }
        });
    }

    // 下载图片主函数
    async function downloadImages() {
        const btn = document.getElementById('pixiv-downloader-btn');
        const progress = document.getElementById('pixiv-downloader-progress');

        if (btn.classList.contains('loading')) return;

        btn.classList.add('loading');
        btn.innerHTML = '🔍 分析中...';
        progress.style.display = 'block';
        progress.innerHTML = '正在分析作品信息...';

        try {
            console.log('开始获取作品信息...');
            const { title, imageUrls, artworkId } = await getArtworkInfo();

            console.log('作品信息获取完成:', { title, imageUrls, artworkId });

            if (imageUrls.length === 0) {
                progress.innerHTML = `<span class="pixiv-downloader-error-text">❌ 未找到可下载的图片</span><br><small>请确保已登录Pixiv并刷新页面</small>`;
                setTimeout(() => {
                    progress.style.display = 'none';
                    btn.classList.remove('loading');
                    btn.innerHTML = '⚡ 一键下载';
                }, 4000);
                return;
            }

            progress.innerHTML = `找到 <b>${imageUrls.length}</b> 张图片<br>开始下载...`;

            let successCount = 0;
            let failCount = 0;

            // 逐个下载图片
            for (let i = 0; i < imageUrls.length; i++) {
                const url = imageUrls[i];
                const fileExtension = getFileExtension(url);
                const filename = imageUrls.length === 1
                    ? `${title}.${fileExtension}`
                    : `${title}-${i + 1}.${fileExtension}`;

                progress.innerHTML = `
                    正在下载 (<b>${i + 1}/${imageUrls.length}</b>)<br>
                    <div class="pixiv-downloader-file-info">${filename}</div>
                `;

                btn.innerHTML = `📥 ${(i + 1)}/${imageUrls.length}`;

                try {
                    console.log(`开始下载第${i + 1}张图片:`, { url, filename });
                    await downloadImageAsBlob(url, filename);
                    successCount++;
                    console.log(`第${i + 1}张图片下载成功`);
                } catch (e) {
                    console.error(`第${i + 1}张图片下载失败,尝试备用方法:`, e);
                    try {
                        await fallbackDownload(url, filename);
                        successCount++;
                        console.log(`第${i + 1}张图片备用方法成功`);
                    } catch (e2) {
                        console.error(`第${i + 1}张图片彻底失败:`, e2);
                        failCount++;
                    }
                }

                // 添加延迟避免请求过快
                if (i < imageUrls.length - 1) {
                    await new Promise(resolve => setTimeout(resolve, 1000));
                }
            }

            // 显示最终结果
            if (failCount === 0) {
                progress.innerHTML = `<span class="pixiv-downloader-success-text">✅ 下载完成!</span><br><div class="pixiv-downloader-file-info">${successCount} 张图片已保存至下载目录</div>`;
            } else if (successCount > 0) {
                progress.innerHTML = `<span class="pixiv-downloader-warning-text">⚠️ 下载完成</span><br><div class="pixiv-downloader-file-info">成功: ${successCount} 张 | 失败: ${failCount} 张</div>`;
            } else {
                progress.innerHTML = `<span class="pixiv-downloader-error-text">❌ 下载失败</span><br><div class="pixiv-downloader-file-info">所有图片下载失败,请检查网络连接</div>`;
            }

            setTimeout(() => {
                progress.style.display = 'none';
                btn.classList.remove('loading');
                btn.innerHTML = '⚡ 一键下载';
            }, 4000);

        } catch (error) {
            console.error('下载过程中出现严重错误:', error);
            progress.innerHTML = `<span class="pixiv-downloader-error-text">❌ 程序错误</span><br><small>${error.message || '未知错误'}</small>`;
            setTimeout(() => {
                progress.style.display = 'none';
                btn.classList.remove('loading');
                btn.innerHTML = '⚡ 一键下载';
            }, 4000);
        }
    }

    // 初始化函数
    function init() {
        // 确保DOM完全加载
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', () => {
                setTimeout(createDownloadButton, 1000);
            });
        } else {
            setTimeout(createDownloadButton, 1000);
        }
    }

    // 启动脚本
    init();
})();