// ==UserScript== // @name Pixiv Downloader // @name:en Pixiv Downloader (Illustration/Manga) // @name:ja Pixiv Downloader (イラスト/漫画) // @name:zh-cn Pixiv Downloader (插画/漫画) // @name:vi Pixiv Downloader (Hình minh họa/Truyện tranh) // @namespace http://tampermonkey.net/ // @version 2.2.0 // @description Tải xuống hình ảnh và truyện tranh từ Pixiv // @description:en Download illustrations and manga from Pixiv // @description:ja Pixivからイラストと漫画をダウンロード // @description:zh-cn 从Pixiv下载插画和漫画 // @description:vi Tải xuống hình minh họa và truyện tranh từ Pixiv // @match https://www.pixiv.net/en/artworks/* // @match https://www.pixiv.net/users/* // @author RenjiYuusei // @license GPL-3.0-only // @icon https://www.google.com/s2/favicons?sz=64&domain=pixiv.net // @grant GM_xmlhttpRequest // @grant GM_download // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM_notification // @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js // @run-at document-end // @connect pixiv.net // @connect pximg.net // @noframes // @downloadURL none // ==/UserScript== (function () { 'use strict'; // Configuration const CONFIG = { CACHE_DURATION: 24 * 60 * 60 * 1000, MAX_CONCURRENT: 3, NOTIFY_DURATION: 3000, RETRY_ATTEMPTS: 3, RETRY_DELAY: 1000, CHUNK_SIZE: 5, // Number of images to download at once BATCH_SIZE: 20, // Number of artworks to download in batch mode }; // Cache and styles const cache = new Map(); GM_addStyle(` .pd-container { position: fixed; bottom: 20px; right: 20px; z-index: 9999; font-family: Arial, sans-serif; } .pd-status, .pd-progress { background: rgba(33, 33, 33, 0.9); color: white; padding: 12px; border-radius: 8px; margin-top: 10px; display: none; box-shadow: 0 2px 5px rgba(0,0,0,0.2); } .pd-progress { width: 250px; height: 24px; background: #444; padding: 3px; } .pd-progress .progress-bar { height: 100%; background: linear-gradient(90deg, #2196F3, #00BCD4); border-radius: 4px; transition: width 0.3s ease; } .pd-batch-dialog { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.3); z-index: 10000; width: 500px; } .pd-batch-dialog textarea { width: 100%; height: 200px; margin: 10px 0; padding: 8px; border: 1px solid #ddd; border-radius: 4px; } .pd-batch-dialog button { padding: 8px 16px; background: #2196F3; color: white; border: none; border-radius: 4px; cursor: pointer; margin-right: 10px; } .pd-batch-dialog button:hover { background: #1976D2; } .pd-batch-status { margin-top: 10px; color: #666; } `); // Utilities const utils = { sleep: ms => new Promise(resolve => setTimeout(resolve, ms)), retry: async (fn, attempts = CONFIG.RETRY_ATTEMPTS) => { for (let i = 0; i < attempts; i++) { try { return await fn(); } catch (err) { if (i === attempts - 1) throw err; await utils.sleep(CONFIG.RETRY_DELAY * (i + 1)); } } }, fetch: async (url, opts = {}) => { const cached = cache.get(url); if (cached?.timestamp > Date.now() - CONFIG.CACHE_DURATION) { return cached.data; } return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: opts.method || 'GET', url, responseType: opts.responseType || 'json', headers: { Referer: 'https://www.pixiv.net/', Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', }, withCredentials: false, onload: res => { if (res.status === 200) { const data = opts.responseType === 'blob' ? res.response : JSON.parse(res.responseText); cache.set(url, { data, timestamp: Date.now() }); resolve(data); } else reject(new Error(`HTTP ${res.status}: ${res.statusText}`)); }, onerror: reject, ontimeout: () => reject(new Error('Request timed out')), timeout: 30000, }); }); }, extractId: input => { // Handle both full URLs and direct IDs const match = input.match(/artworks\/(\d+)/) || input.match(/^(\d+)$/); return match ? match[1] : null; }, ui: { container: null, init: () => { utils.ui.container = document.createElement('div'); utils.ui.container.className = 'pd-container'; document.body.appendChild(utils.ui.container); utils.ui.status.init(); utils.ui.progress.init(); }, notify: (msg, type = 'info') => GM_notification({ text: msg, title: 'Pixiv Downloader', timeout: CONFIG.NOTIFY_DURATION, }), status: { el: null, init: () => { utils.ui.status.el = document.createElement('div'); utils.ui.status.el.className = 'pd-status'; utils.ui.container.appendChild(utils.ui.status.el); }, show: msg => { utils.ui.status.el.textContent = msg; utils.ui.status.el.style.display = 'block'; }, hide: () => (utils.ui.status.el.style.display = 'none'), }, progress: { el: null, bar: null, init: () => { const container = document.createElement('div'); container.className = 'pd-progress'; const bar = document.createElement('div'); bar.className = 'progress-bar'; container.appendChild(bar); utils.ui.container.appendChild(container); utils.ui.progress.el = container; utils.ui.progress.bar = bar; }, update: pct => { utils.ui.progress.el.style.display = 'block'; utils.ui.progress.bar.style.width = `${pct}%`; }, hide: () => (utils.ui.progress.el.style.display = 'none'), }, showBatchDialog: () => { const dialog = document.createElement('div'); dialog.className = 'pd-batch-dialog'; dialog.innerHTML = `
Enter the ID or URL of the artwork (one link per line):