NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Pixiv Downloader (Illustrations/Manga/Ugoira) // @name:vi Trình tải xuống Pixiv (Minh họa/Truyện tranh/Ugoira) // @name:zh-cn Pixiv 下载器(插图/漫画/动图) // @name:zh-tw Pixiv 下載器(插圖/漫畫/動圖) // @name:ja ピクシブダウンローダー(イラスト/マンガ/うごイラ) // @namespace http://tampermonkey.net/ // @version 1.3.1 // @description Download illustrations, manga, and ugoira from Pixiv with optimized speed, ZIP support for multi-page works, and GIF conversion for ugoira // @description:vi Tải xuống hình minh họa, truyện tranh và ugoira từ Pixiv với tốc độ tối ưu, có hỗ trợ ZIP cho các tác phẩm nhiều trang và chuyển đổi GIF cho ugoira // @description:zh-cn 以优化的速度从 Pixiv 下载插图、漫画和动图,ZIP 支持多页作品,动图转换为 GIF // @description:zh-tw 以最佳化的速度從 Pixiv 下載插圖、漫畫和動圖,ZIP 支援多頁作品,動圖轉換為 GIF // @description:ja 最適化された速度でPixivからイラスト、漫画、うごイラをダウンロード、複数ページの作品にはZIPをサポート、うごイラはGIFに変換 // @match https://www.pixiv.net/en/artworks/* // @author Jann // @license GPL-3.0-only // @icon https://t0.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=https://www.pixiv.net&size=64 // @grant GM_xmlhttpRequest // @grant GM_download // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_notification // @grant GM_setClipboard // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/gif.js/0.2.0/gif.js // ==/UserScript== (function () { 'use strict'; const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours const MAX_CONCURRENT_DOWNLOADS = 3; const NOTIFICATION_DURATION = 3000; // 3 seconds const cache = new Map(); const styles = ` #pixiv-downloader-status { position: fixed; bottom: 20px; right: 20px; z-index: 9999; padding: 10px; background-color: rgba(0, 0, 0, 0.7); color: white; border-radius: 5px; font-size: 14px; display: none; } `; GM_addStyle(styles); const getCachedData = (key) => { const cachedData = cache.get(key); if (cachedData && (Date.now() - cachedData.timestamp < CACHE_DURATION)) { return cachedData.data; } return null; }; const setCachedData = (key, data) => { cache.set(key, { data, timestamp: Date.now() }); }; const fetchWithCache = (url, options = {}) => { return new Promise((resolve, reject) => { const cachedData = getCachedData(url); if (cachedData) { resolve(cachedData); return; } GM_xmlhttpRequest({ method: options.method || "GET", url: url, responseType: options.responseType || "json", headers: { "Referer": "https://www.pixiv.net/" }, onload: (response) => { if (response.status === 200) { const data = options.responseType === "blob" ? response.response : JSON.parse(response.responseText); setCachedData(url, data); resolve(data); } else { reject(`Failed to fetch: ${response.status}`); } }, onerror: (error) => reject(`Error fetching: ${error}`) }); }); }; const getIllustData = async () => { const illustId = window.location.pathname.split('/').pop(); const url = `https://www.pixiv.net/ajax/illust/${illustId}`; const data = await fetchWithCache(url); return data.body; }; const createStatusElement = () => { const status = document.createElement('div'); status.id = 'pixiv-downloader-status'; document.body.appendChild(status); return status; }; const updateStatus = (message) => { const status = document.getElementById('pixiv-downloader-status'); status.textContent = message; status.style.display = 'block'; }; const hideStatus = () => { const status = document.getElementById('pixiv-downloader-status'); status.style.display = 'none'; }; const showNotification = (message, type = 'info') => { GM_notification({ text: message, title: 'Pixiv Downloader', timeout: NOTIFICATION_DURATION, image: type === 'success' ? 'https://i.imgur.com/oUiCS7Y.png' : type === 'error' ? 'https://i.imgur.com/LxPNwvr.png' : null }); }; const toggleUseOriginalFilename = () => { const currentValue = GM_getValue('useOriginalFilename', false); GM_setValue('useOriginalFilename', !currentValue); updateMenuCommands(); showNotification(`Use Original Filename: ${!currentValue ? 'On' : 'Off'}`); }; const getFilename = (illustData, index = 0) => { const useOriginalFilename = GM_getValue('useOriginalFilename', false); if (useOriginalFilename) { return `${illustData.id}${index > 0 ? `_p${index}` : ''}.${illustData.urls.original.split('.').pop()}`; } else { const artistName = illustData.userName.replace(/[^\w\s-]/gi, ''); const artworkTitle = illustData.title.replace(/[^\w\s-]/gi, ''); return `${artistName} - ${artworkTitle} (${illustData.id})${index > 0 ? `_p${index}` : ''}.${illustData.urls.original.split('.').pop()}`; } }; const downloadSinglePage = async (illustData) => { updateStatus('Downloading single image...'); const url = illustData.urls.original; const filename = getFilename(illustData); try { const response = await fetchWithCache(url, { responseType: "blob" }); const blob = new Blob([response], { type: 'image/jpeg' }); const downloadUrl = URL.createObjectURL(blob); GM_download({ url: downloadUrl, name: filename, saveAs: true, onload: () => { URL.revokeObjectURL(downloadUrl); showNotification('Download complete!', 'success'); hideStatus(); }, onerror: (error) => { URL.revokeObjectURL(downloadUrl); showNotification(`Download failed: ${error.message}`, 'error'); hideStatus(); } }); } catch (error) { console.error('Error downloading image:', error); showNotification(`Download failed: ${error.message}`, 'error'); hideStatus(); } }; const downloadMultiplePages = async (illustData) => { updateStatus('Downloading multiple pages...'); const zip = new JSZip(); const downloadPromises = []; let completedDownloads = 0; for (let i = 0; i < illustData.pageCount; i++) { const url = illustData.urls.original.replace('_p0', `_p${i}`); downloadPromises.push( fetchWithCache(url, { responseType: "blob" }) .then(blob => { completedDownloads++; updateStatus(`Downloaded page ${completedDownloads}/${illustData.pageCount}`); return zip.file(getFilename(illustData, i), blob); }) .catch(error => { console.error(`Failed to download page ${i+1}:`, error); showNotification(`Failed to download page ${i+1}: ${error.message}`, 'error'); }) ); if (downloadPromises.length === MAX_CONCURRENT_DOWNLOADS) { await Promise.all(downloadPromises); downloadPromises.length = 0; } } await Promise.all(downloadPromises); if (completedDownloads === illustData.pageCount) { updateStatus('Generating ZIP file...'); const content = await zip.generateAsync({ type: "blob" }); const zipUrl = URL.createObjectURL(content); const zipFilename = getFilename(illustData).replace(/\.\w+$/, '.zip'); GM_download({ url: zipUrl, name: zipFilename, saveAs: true, onload: () => { URL.revokeObjectURL(zipUrl); showNotification('Download complete!', 'success'); hideStatus(); }, onerror: (error) => { URL.revokeObjectURL(zipUrl); showNotification(`Download failed: ${error.message}`, 'error'); hideStatus(); } }); } else { showNotification(`Download incomplete. Only ${completedDownloads}/${illustData.pageCount} pages were downloaded.`, 'error'); hideStatus(); } }; const downloadUgoira = async (illustData) => { updateStatus('Downloading Ugoira...'); const ugoiraMetaUrl = `https://www.pixiv.net/ajax/illust/${illustData.id}/ugoira_meta`; const ugoiraData = await fetchWithCache(ugoiraMetaUrl); const zipUrl = ugoiraData.body.originalSrc; try { const zipBlob = await fetchWithCache(zipUrl, { responseType: "blob" }); const zip = await JSZip.loadAsync(zipBlob); const frames = []; const frameDelays = ugoiraData.body.frames.map(frame => frame.delay); for (const [filename, file] of Object.entries(zip.files)) { if (!file.dir) { const blob = await file.async("blob"); const imageBitmap = await createImageBitmap(blob); frames.push(imageBitmap); } } updateStatus('Converting Ugoira to GIF...'); const gif = new GIF({ workers: 2, quality: 10, width: frames[0].width, height: frames[0].height }); frames.forEach((frame, index) => { gif.addFrame(frame, { delay: frameDelays[index] }); }); gif.on('finished', (blob) => { const gifUrl = URL.createObjectURL(blob); const filename = getFilename(illustData).replace(/\.\w+$/, '.gif'); GM_download({ url: gifUrl, name: filename, saveAs: true, onload: () => { URL.revokeObjectURL(gifUrl); showNotification('Ugoira download and conversion complete!', 'success'); hideStatus(); }, onerror: (error) => { URL.revokeObjectURL(gifUrl); showNotification(`Ugoira download failed: ${error.message}`, 'error'); hideStatus(); } }); }); gif.render(); } catch (error) { console.error('Error processing Ugoira:', error); showNotification(`Ugoira download failed: ${error.message}`, 'error'); hideStatus(); } }; const copyArtworkInfo = async () => { try { const illustData = await getIllustData(); const info = `Title: ${illustData.title} Artist: ${illustData.userName} ID: ${illustData.id} Upload Date: ${new Date(illustData.uploadDate).toLocaleString()} Tags: ${illustData.tags.tags.map(tag => tag.tag).join(', ')} Description: ${illustData.description}`; GM_setClipboard(info); showNotification('Artwork info copied to clipboard!', 'success'); } catch (error) { console.error('Error copying artwork info:', error); showNotification('Error copying artwork info', 'error'); } }; const downloadArtwork = async () => { try { updateStatus('Fetching artwork data...'); const illustData = await getIllustData(); if (!illustData) { throw new Error('Failed to fetch illustration data.'); } if (illustData.illustType === 2) { await downloadUgoira(illustData); } else if (illustData.pageCount > 1) { await downloadMultiplePages(illustData); } else { await downloadSinglePage(illustData); } } catch (error) { updateStatus(`Error: ${error.message}`); console.error('Pixiv Downloader Error:', error); showNotification(`Error: ${error.message}`, 'error'); hideStatus(); } }; const menuItems = [{ name: 'Download Artwork', key: 'downloadArtwork', handler: downloadArtwork }, { name: 'Copy Artwork Info', key: 'copyArtworkInfo', handler: copyArtworkInfo }, { name: 'Use Original Filename', key: 'useOriginalFilename', type: 'toggle', handler: toggleUseOriginalFilename, getValue: () => GM_getValue('useOriginalFilename', false) }, { name: 'Set Download Directory', key: 'downloadDirectory', prompt: 'Enter download directory:', handler: (value) => GM_setValue('downloadDirectory', value) }, { name: 'Set Image Quality', key: 'imageQuality', prompt: 'Enter image quality (original/large/medium):', handler: (value) => GM_setValue('imageQuality', value) }, { name: 'Auto Download', key: 'autoDownload', type: 'toggle', handler: toggleAutoDownload, getValue: () => GM_getValue('autoDownload', false) }, ]; function registerMenuCommands() { menuItems.forEach(item => { let commandText = item.name; if (item.type === 'toggle') { const value = item.getValue(); commandText += `: ${value ? 'On' : 'Off'}`; } GM_registerMenuCommand(commandText, () => { if (item.type === 'toggle') { item.handler(); } else if (item.prompt) { const value = prompt(item.prompt); if (value !== null) { item.handler(value); } } else { item.handler(); } updateMenuCommands(); }); }); } function updateMenuCommands() { menuItems.forEach(item => { GM_unregisterMenuCommand(item.name); }); registerMenuCommands(); } function toggleAutoDownload() { const currentValue = GM_getValue('autoDownload', false); GM_setValue('autoDownload', !currentValue); showNotification(`Auto Download: ${!currentValue ? 'On' : 'Off'}`); } function initializeScript() { createStatusElement(); registerMenuCommands(); if (GM_getValue('autoDownload', false)) { downloadArtwork(); } } initializeScript(); })();