您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
一键(快捷键)下载Pixiv各页面的原图,支持多图下载,动图下载支持Zip压缩包/Gif动图格式切换。下载的图片将保存到以画师名命名的单独文件夹(需要调整tampermonkey“下载”设置为“浏览器API”)。保留已下载图片的记录。
当前为
// ==UserScript== // @name Pixiv Downloader // @namespace https://greasyfork.org/zh-CN/scripts/432150 // @version 0.3.5 // @description 一键(快捷键)下载Pixiv各页面的原图,支持多图下载,动图下载支持Zip压缩包/Gif动图格式切换。下载的图片将保存到以画师名命名的单独文件夹(需要调整tampermonkey“下载”设置为“浏览器API”)。保留已下载图片的记录。 // @author ruaruarua // @match https://www.pixiv.net/* // @icon https://www.google.com/s2/favicons?domain=pixiv.net // @grant GM_xmlhttpRequest // @grant GM_download // @grant GM_registerMenuCommand // @connect i.pximg.net // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/gif.js/0.2.0/gif.js // ==/UserScript== (async function () { const GIF_QUALITY = 10; //0-10。数值越小,GIF质量越高,转换时间越长 class PixivDownloader { constructor(pixivId) { this.pixivId = pixivId; this.artworksURL = 'https://www.pixiv.net/artworks/' + pixivId; this.gifOriginalSrc = 'https://www.pixiv.net/ajax/illust/' + pixivId + '/ugoira_meta'; this.author = ''; this.illustTitle = ''; this.illustType = -1; this.pageCount = 1; this.pageDownloaded = 0; this.imgList = []; this.ugoiraMeta = {}; } saveAsGif_(blob, self, savePath, resolve, reject) { const gifPromiseList = []; const imgList = []; zip.folder(`${self.pixivId}`) .loadAsync(blob) .then((zip) => { const gif = new GIF(gifConfig); let i = 0; zip.forEach((relativePath, file) => { gifPromiseList[i] = new Promise((loadResolve) => { i++; const image = new Image(); imgList.push(image); image.onload = () => { loadResolve(); }; file.async('blob') .then((blob) => { const objectURL = URL.createObjectURL(blob); image.src = objectURL; }); }); }); Promise.all(gifPromiseList) .then(() => { zip.remove(`${self.pixivId}`); imgList.forEach((e, i) => { gif.addFrame(e, { delay: self.ugoiraMeta.frames[i].delay }); }); gif.on('finished', (blob) => void self.saveImg_(blob, savePath, resolve, reject)); gif.render(); }); }); } saveImg_(blob, savePath, resolve, reject) { const imgUrl = URL.createObjectURL(blob); GM_download({ url: imgUrl, name: savePath, onerror: (error) => { if (reject) { console.log('save error.') reject(error); } }, onload: () => { URL.revokeObjectURL(imgUrl); if (resolve) { resolve(); } }, }); } fetchIllust_(savePath, imgSrc, onProgressCallback = null, onLoadCallback = null, resolve, reject) { const self = this; GM_xmlhttpRequest({ url: imgSrc, method: 'GET', headers: { referer: 'https://www.pixiv.net' }, responseType: 'blob', onprogress: (e) => { if(e.lengthComputable && e.loaded == e.total){ self.pageDownloaded++; } if (typeof onProgressCallback == 'function') { onProgressCallback(e, self.pageDownloaded, self.pageCount); } }, onload: (e) => { if (typeof onLoadCallback == 'function') { onLoadCallback(e, self.pageDownloaded, self.pageCount); } if (savePath.slice(-3) != 'gif') { self.saveImg_(e.response, savePath, resolve, reject); } else { self.saveAsGif_(e.response, self, savePath, resolve, reject); } }, onerror: (error) => { if (reject) { console.log('xmlhttpRequest failed.') reject(error); } }, }); } replaceInvalidChar_(string = '') { return string.trim() .replace(/^\.|\.$/g, '') .replace(/[\u200b-\u200f\uFEFF\u202a-\u202e\\/:*?"|]/g, "") .replace(/"/g, "'") .replace(/</g, '[') .replace(/>/g, ']'); } initProps_(preloadData, ugoiraMeta) { const illustInfo = preloadData.illust[this.pixivId]; this.author = this.replaceInvalidChar_(illustInfo.userName) || 'userId-' + illustInfo.userId; this.illustTitle = this.replaceInvalidChar_(illustInfo.illustTitle) || 'illustId-' + illustInfo.illustId; this.illustType = illustInfo.illustType; if (this.illustType == 0 || this.illustType == 1) { this.pageCount = illustInfo.pageCount; const firstImgSrc = illustInfo.urls.original; const srcPrefix = firstImgSrc.slice(0, firstImgSrc.indexOf('_') + 2); const srcSuffix = firstImgSrc.slice(-4); for (let i = 0; i < this.pageCount; i++) { const imgSrc = srcPrefix + i + srcSuffix; this.imgList.push(imgSrc); } } if (this.illustType == 2) { this.ugoiraMeta = ugoiraMeta; } } initial() { return fetch(this.artworksURL) .then((res) => { if (!res.ok) throw new Error('fetch artworksURL failed: ' + res.status); return res.text(); }) .then(async (htmlText) => { const preloadData = JSON.parse(htmlText.match(/"meta-preload-data" content='(.*)'>/)[1]); let ugoiraMeta = {}; if (!preloadData.illust) throw new Error('Fail to parse meta preload data.'); if (preloadData.illust[this.pixivId].illustType == 2) { ugoiraMeta = await fetch(this.gifOriginalSrc) .then((res) => { if (!res.ok) throw new Error('fetch ugoira meta failed: ' + res.status); return res.json(); }) .then((res) => res.body); if (!/img-zip-ugoira/.test(ugoiraMeta.originalSrc)) throw new Error('Fail to parse originalSrc.'); } this.initProps_(preloadData, ugoiraMeta); return this; }); } download(onProgressCallback, onLoadCallback) { let baseSavePath = this.author + '/' + this.author + '_' + this.illustTitle + '_' + this.pixivId; if (this.illustType == 0 || this.illustType == 1) { const promiseList = []; if (this.imgList.length > 0) { this.imgList.forEach((imgSrc, i) => { promiseList[i] = new Promise((resolve, reject) => { const savePath = baseSavePath + '_p' + i + imgSrc.slice(-4); this.fetchIllust_(savePath, imgSrc, onProgressCallback, onLoadCallback, resolve, reject); }); }) } return Promise.all(promiseList); } if (this.illustType == 2) { return new Promise((resolve, reject) => { if (GIF_FLAG) { baseSavePath += '.gif'; } else { baseSavePath += '.zip'; } this.fetchIllust_(baseSavePath, this.ugoiraMeta.originalSrc, onProgressCallback, onLoadCallback, resolve, reject); }); } } } const getPixivId = (thumbnail = null) => { if (thumbnail.childElementCount === 0) return false; const artworkReg = /artworks\/([0-9]+)$/; const isHrefMatch = artworkReg.exec(thumbnail.href); if (isHrefMatch) { if (thumbnail.getAttribute('data-gtm-value') || thumbnail.classList.contains('gtm-illust-recommend-thumbnail-thumbnail') || thumbnail.classList.contains('gtm-discover-user-recommend-thumbnail') || thumbnail.classList.contains('work')) { return isHrefMatch[1]; } if (window.location.href.indexOf('bookmark_new_illust') != -1 && thumbnail.getAttribute('class')) { return isHrefMatch[1]; } } else { const activityReg = /illust_id=([0-9]+)/; const isActivityMatch = activityReg.exec(thumbnail.href); if (isActivityMatch && thumbnail.classList.contains('work')) { return isActivityMatch[1]; } } return false; } const createPdlBtn = (isMain = false, pixivId = '') => { const pdlBtn = document.createElement('a'); pdlBtn.setAttribute('href', 'javascript:void(0)'); pdlBtn.setAttribute('pdl-id', pixivId); pdlBtn.classList.add('pdl-btn'); if (!isMain) { pdlBtn.classList.add('pdl-btn-sub'); } else { pdlBtn.classList.add('pdl-btn-main'); } if (pixivStorage.has(pixivId)) { pdlBtn.classList.add('pdl-complete'); } return pdlBtn; } const handleDownload = (pdlBtn, pixivId) => { const onProgressCallback = (e, pageDownloaded = 0, pageCount = 1) => { if (e.lengthComputable) { let progress = 0; if (pageCount == 1) { progress = Math.floor((e.loaded / e.total) * 100); } if (pageCount > 1) { progress = Math.floor((pageDownloaded / pageCount) * 100); } pdlBtn.textContent = progress; pdlBtn.style.setProperty('--pdl-progress', progress + '%'); } }; const onLoadCallback = (e) => { if (GIF_FLAG && 'zip' == e.finalUrl.slice(-3)) { pdlBtn.innerHTML = ''; } } pdlBtn.classList.add('pdl-progress'); new PixivDownloader(pixivId) .initial() .then((downloader) => downloader.download(onProgressCallback, onLoadCallback)) .then(() => { pixivStorage.add(pixivId); localStorage.setItem(`pdlTemp-${pixivId}`, '') pdlBtn.classList.remove('pdl-error'); pdlBtn.classList.add('pdl-complete'); }) .catch((err) => { if (err) console.log(err); pdlBtn.classList.remove('pdl-complete'); pdlBtn.classList.add('pdl-error'); }) .finally(() => { pdlBtn.innerHTML = ''; pdlBtn.style.removeProperty('--pdl-progress'); pdlBtn.classList.remove('pdl-progress'); }); } const observerCallback = () => { const reg = /artworks\/([0-9]+)$/; const isArtworksPage = reg.exec(window.location.href); if (isArtworksPage && !body.querySelector('.pdl-btn-main')) { const addFavousBtn = body.querySelector('.gtm-main-bookmark'); if (addFavousBtn) { const handleBar = addFavousBtn.parentElement.parentElement; const pdlBtnWrap = addFavousBtn.parentElement.cloneNode(); pdlBtnWrap.appendChild(createPdlBtn(true, isArtworksPage[1])); handleBar.appendChild(pdlBtnWrap); body.addEventListener('keydown', (e) => { if (e.ctrlKey && e.key == 'q') { const pdlMainBtn = body.querySelector('.pdl-btn-main'); if (pdlMainBtn) { e.preventDefault(); if (!e.repeat) { pdlMainBtn.dispatchEvent(new MouseEvent('click', { "bubbles": true })); } } } }); } } const picLists = body.querySelectorAll('a'); picLists.forEach((e) => { if (!e.querySelector('.pdl-btn-sub')) { const pixivId = getPixivId(e); if (pixivId) { e.appendChild(createPdlBtn(false, pixivId)); } } }); } const updateHistory = () => { Object.keys(localStorage).forEach((key) => { const matchResult = /pdlTemp-(\d+)/.exec(key); if (matchResult) { pixivStorage.add(matchResult[1]); localStorage.removeItem(matchResult[0]); } }); localStorage.pixivDownloader = JSON.stringify([...pixivStorage]); } localStorage.isPdlGif = localStorage.isPdlGif || 'false'; localStorage.pixivDownloader = localStorage.pixivDownloader || '[]'; let GIF_FLAG = JSON.parse(localStorage.isPdlGif); GM_registerMenuCommand('Gif', () => { GIF_FLAG = true; localStorage.isPdlGif = GIF_FLAG; }, 'g'); GM_registerMenuCommand('Zip', () => { GIF_FLAG = false; localStorage.isPdlGif = GIF_FLAG; }, 'z'); GM_registerMenuCommand('Clear history', () => { updateHistory(); localStorage.pixivDownloader = '[]'; }, 'c'); let pixivStorage = new Set(JSON.parse(localStorage.pixivDownloader)); updateHistory(); const pdlStyle = document.createElement('style'); pdlStyle.innerHTML = ` @property --pdl-progress { syntax: '<percentage>'; inherits: true; initial-value: 0%; } @keyframes pdl_loading { 100% { transform: translate(-50%, -50%) rotate(360deg); } } .pdl-btn { position: relative; border-top-right-radius: 8px; background: no-repeat center/85%; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E %3Cpath fill='%233C3C3C' d='M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm-32-316v116h-67c-10.7 0-16 12.9-8.5 20.5l99 99c4.7 4.7 12.3 4.7 17 0l99-99c7.6-7.6 2.2-20.5-8.5-20.5h-67V140c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12z'%3E%3C/path%3E %3C/svg%3E"); color: #01b468; display: inline-block; font-size: 13px; font-weight: bold; height: 32px; line-height: 32px; margin: 0; overflow: hidden; padding: 0; text-decoration: none!important; text-align: center; text-overflow: ellipsis; user-select: none; white-space: nowrap; width: 32px; z-index: 1; } .pdl-btn-main { margin: 0 0 0 10px; } .pdl-btn-sub { bottom: 0; background-color: rgba(255, 255, 255, .5); left: 0; position: absolute; } .pdl-error { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E %3Cpath fill='%23EA0000' d='M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm101.8-262.2L295.6 256l62.2 62.2c4.7 4.7 4.7 12.3 0 17l-22.6 22.6c-4.7 4.7-12.3 4.7-17 0L256 295.6l-62.2 62.2c-4.7 4.7-12.3 4.7-17 0l-22.6-22.6c-4.7-4.7-4.7-12.3 0-17l62.2-62.2-62.2-62.2c-4.7-4.7-4.7-12.3 0-17l22.6-22.6c4.7-4.7 12.3-4.7 17 0l62.2 62.2 62.2-62.2c4.7-4.7 12.3-4.7 17 0l22.6 22.6c4.7 4.7 4.7 12.3 0 17z'%3E%3C/path%3E %3C/svg%3E"); } .pdl-complete { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E %3Cpath fill='%2301B468' d='M256 8C119.033 8 8 119.033 8 256s111.033 248 248 248 248-111.033 248-248S392.967 8 256 8zm0 48c110.532 0 200 89.451 200 200 0 110.532-89.451 200-200 200-110.532 0-200-89.451-200-200 0-110.532 89.451-200 200-200m140.204 130.267l-22.536-22.718c-4.667-4.705-12.265-4.736-16.97-.068L215.346 303.697l-59.792-60.277c-4.667-4.705-12.265-4.736-16.97-.069l-22.719 22.536c-4.705 4.667-4.736 12.265-.068 16.971l90.781 91.516c4.667 4.705 12.265 4.736 16.97.068l172.589-171.204c4.704-4.668 4.734-12.266.067-16.971z'%3E%3C/path%3E %3C/svg%3E"); } .pdl-progress { background-image: none; cursor: default; } .pdl-progress:after{ content: ''; display: inline-block; position: absolute; top: 50%; left: 50%; width: 27px; height: 27px; transform: translate(-50%, -50%); -webkit-mask: radial-gradient(transparent, transparent 54%, #000 57%, #000); mask: radial-gradient(transparent, transparent 54%, #000 57%, #000); border-radius: 50%; } .pdl-progress:not(:empty):after { background: conic-gradient(#01B468 0, #01B468 var(--pdl-progress), transparent var(--pdl-progress), transparent); transition: --pdl-progress .2s ease; } .pdl-progress:empty:after { background: conic-gradient(#01B468 0, #01B468 25%, #01B46833 25%, #01B46833); animation: 1.5s infinite linear pdl_loading; }`; document.head.appendChild(pdlStyle); const body = document.body; const config = { attributes: false, childList: true, subtree: true, }; const rootObserver = new MutationObserver(observerCallback); rootObserver.observe(body, config); const zip = new JSZip(); const workerStr = await fetch('https://raw.githubusercontent.com/jnordberg/gif.js/master/dist/gif.worker.js').then((res) => res.blob()); const gifConfig = { workers: 3, quality: GIF_QUALITY, workerScript: URL.createObjectURL(workerStr), }; body.addEventListener('click', (e) => { if (e.target.hasAttribute('pdl-id')) { e.stopPropagation(); if (!e.target.classList.contains('pdl-progress')) { const pixivId = e.target.getAttribute('pdl-id'); handleDownload(e.target, pixivId); } } }); })();