您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Enhanced download of wnacg
// ==UserScript== // @name wnacgDownload // @namespace Yr // @version 3.0.3 // @description Enhanced download of wnacg // @author yanagiragi // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.2.0/jszip.js // @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.8/FileSaver.js // @match http*://*.wnacg.com/photos-index-page-*.html // @match http*://*.wnacg.org/photos-index-page-*.html // @match http*://*.wnacg.com/photos-index-aid-*.html // @match http*://*.wnacg.org/photos-index-aid-*.html // @grant GM_xmlhttpRequest // @grant GM_download // @grant GM_openInTab // ==/UserScript== 'use strict'; // Global Defines const waitingStr = `排隊中`; const downloadStr = `已下載`; const timeout = 1000; // time interval between retry const successCountLimit = 0; // How many continous success checks required to start download, set 0 for instant download const closeTabInterval = -1; // set to -1 to avoid auto close new opened tabs const closeWindowInterval = -1; // set to -1 to avoid auto close current window // ===================================================== // Utilities // ===================================================== // Modified from https://gist.github.com/WebReflection/df05641bd04954f6d366 // with predefined object specific, for HTML entities only function _Unescape (s) { var re = /&(?:amp|#38|lt|#60|gt|#62|apos|#39|quot|#34);/g; var unescaped = { '&': '&', '&': '&', '<': '<', '<': '<', '>': '>', '>': '>', ''': "'", ''': "'", '"': '"', '"': '"', '-': '-' }; return s.replace(re, function (m) { return escape(unescaped[m]); }); } async function _Fetch (url) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: url, headers: { // Without header it return 200 and seldom return 503 even if service is not availiable "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/112.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", "Accept-Language": "zh-TW,zh;q=0.8,en-US;q=0.5,en;q=0.3", "Upgrade-Insecure-Requests": "1", "Pragma": "no-cache", "Cache-Control": "no-cache" }, onload: function (response) { resolve(response.responseText) }, onerror: function (error) { reject(error) } }) }) } function _PromissAll (promises, progressCallback) { let count = 0; progressCallback(0); for (const p of promises) { p.then(() => { count++; progressCallback((count * 100) / promises.length); }); } return Promise.all(promises); } // ===================================================== // Direct Download Methods // ===================================================== async function ParseDownloadPageLink (url) { const result = await _Fetch(url); const match = result.match(/href=\"(\/download-index-aid-.*)"/); return `${location.protocol}//wnacg.org` + match[1]; } async function ParseDownloadLink (target) { const result = await _Fetch(target.replace('wnacg.org', location.hostname)); const matches = result.match(/down_btn ads\" href="(.*?)">/); const rawLink = `${location.protocol}//` + _Unescape(matches[1]); // fixs download re-naming of server behaviour return new URL(rawLink).href; } function GetCategory () { let raw = document.querySelector('.asTBcell.uwconn label').textContent raw = raw.replace(/分類:/, '').replace(/ /g, '').replace(/\//g, '') return encodeURIComponent(raw) } async function DirectDownload (event) { event.preventDefault(); const btn = document.querySelector('#YrDownloadBtn'); const block = document.querySelector('#YrDirectDownloadStatusBlock'); const status = document.querySelector('#YrStatus'); const retryCount = document.querySelector('#YrRetryCount'); const lastRetry = document.querySelector('#YrLastRetry'); const url = btn.href.replace(/&_ga=.*/, '') // remove ga since it sucks and broke server renaming function; let retries = 0; let successCount = 0; const Download = (onSuccessCallback, onFailCallback) => { // TODO: shoud refactor to call _Fetch later GM_xmlhttpRequest({ method: "HEAD", url: url, headers: { // Without header it return 200 and seldom return 503 even if service is not availiable "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:93.0) Gecko/20100101 Firefox/93.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", "Accept-Language": "zh-TW,zh;q=0.8,en-US;q=0.5,en;q=0.3", "Upgrade-Insecure-Requests": "1", "Pragma": "no-cache", "Cache-Control": "no-cache" }, timeout: 1000 * 10, onerror: function (error) { console.log('onerror') successCount = 0; onFailCallback(); }, onload: function (response) { console.log(`successCount = ${successCount}, code = ${response.status}`); if (response.status == 200 || response.status == 403) { // 403 means cloudflare middleware successCount += 1; if (successCount >= successCountLimit) { onSuccessCallback(); } else { onFailCallback(); } } else { successCount = 0; onFailCallback(); } } }); }; const OnSuccess = () => { status.textContent = downloadStr; const openTab = async () => { console.log(url) // open download link in new tab, use it with set auto download to specific types const tab = await GM_openInTab(url); // auto close tab if (closeTabInterval > 0) { await new Promise((resolve) => setTimeout(resolve, closeTabInterval)); tab.close(); } // auto close window, use it with brower config: allow script to close window // e.g. Firefox: dom.allow_scripts_to_close_windows if (closeWindowInterval > 0) { setTimeout(() => window.close(), closeWindowInterval); } } openTab(); } const OnFailed = () => { status.textContent = waitingStr; lastRetry.textContent = `${new Date().toLocaleTimeString()}`; retryCount.textContent = `${++retries}`; setTimeout(TryDownload, timeout); } const TryDownload = () => Download(OnSuccess, OnFailed); // btn.style.display = 'none'; block.style.display = 'block'; lastRetry.textContent = `${new Date().toLocaleTimeString()}`; retryCount.textContent = `${retries}`; status.textContent = waitingStr; TryDownload(); } // ===================================================== // Download Image Methods // ===================================================== function GetImageBase64 (index, url) { return new Promise((resolve, reject) => { const extension = url.substring(url.lastIndexOf('.') + 1) GM_xmlhttpRequest({ method: "GET", url: url, overrideMimeType: 'text/plain; charset=x-user-defined', onload: response => { let binary = ""; const responseText = response.responseText; const responseTextLen = responseText.length; for (let i = 0; i < responseTextLen; i++) { binary += String.fromCharCode(responseText.charCodeAt(i) & 255) } // Note there is no 'data:image/jpeg;base64,' Due to JSZip let src = btoa(binary) console.log(`Downloaded: ${index}.${extension}, src=${url}`) resolve({ 'index': index, 'base64': src, 'extension': extension }) } }) }) } async function Compress (title, pics, progressCallback = null) { console.log(`Start Compress`) const zip = new JSZip(); const folder = zip.folder(title); for (let i = 0; i < pics.length; ++i) { folder.file(`${pics[i].index}.${pics[i].extension}`, pics[i].base64, { base64: true }) } const content = await zip.generateAsync({ type: "blob", streamFiles: true }, metadata => { progressCallback?.(metadata) console.log(`Compress Progress = ${metadata.percent.toFixed(2)} %`) }) console.log(`All Done, Save to ${title}.zip`); return saveAs(content, `${title}.zip`); } async function FetchImageLinks (url) { const resp = await _Fetch(url) const dom = new DOMParser().parseFromString(resp, 'text/html') const blocks = dom.querySelectorAll('.gallary_item') const result = [] for (let i = 0; i < blocks.length; ++i) { const a = blocks[i].querySelector('a') result.push(a.href) } return result } async function FetchImageSrc (url) { const resp = await _Fetch(url) const dom = new DOMParser().parseFromString(resp, 'text/html') const img = dom.querySelector('#photo_body img') return img.src } function GetPageCount () { const paginators = [...document.querySelectorAll('.f_left.paginator a')] if (paginators.length == 0) { // cases: current book has only one page return 1 } const href = paginators.slice(-2, -1)[0].href return parseInt(href.substring(href.indexOf('photos-index-page-') + 'photos-index-page-'.length, href.indexOf('-aid-'))) } function GetPageId () { // two formats: // https://wnacg.org/photos-index-aid-xxxxx.html // https://wnacg.org/photos-index-page-1-aid-xxxxx.html const href = location.href return location.href.substring(location.href.indexOf('-aid-') + '-aid-'.length, location.href.indexOf('.html')) } async function DownloadImages (event) { event.preventDefault(); const block = document.querySelector('#YrDownloadImageStatusBlock'); const parsingPageId = block.querySelector('#YrParsingPageId'); const downloadImageStatus = block.querySelector('#YrDownloadImageStatus'); block.style.display = 'block'; const pageCount = GetPageCount() const pageId = GetPageId() downloadImageStatus.textContent = `解析頁面中 ...` const imageSrcs = [] for (let i = 1; i <= pageCount; ++i) { parsingPageId.textContent = `第 ${i} 頁` const url = `https://wnacg.com/photos-index-page-${i}-aid-${pageId}.html` const links = await FetchImageLinks(url) const tasks = links.map(x => FetchImageSrc(x)) const srcs = await Promise.all(tasks) imageSrcs.push(srcs) console.log(url, srcs) // for debug } parsingPageId.textContent = `已完成, 共 ${pageCount} 頁` const tasks = imageSrcs.flat().map((x, idx) => GetImageBase64(idx, x)) const images = await _PromissAll(tasks, progress => { downloadImageStatus.textContent = `解析 ${parseInt(0.01 * progress * tasks.length)} / ${tasks.length} 圖片中 ...` }) const title = document.querySelector('#bodywrap h2').textContent const results = await Compress(title, images, metadata => { downloadImageStatus.textContent = `壓縮中 (${metadata.percent.toFixed(2)} %)` }) return results } // ===================================================== // General Setups // ===================================================== async function SetupDirectDownloadButton () { const category = GetCategory(); const downloadPageLink = await ParseDownloadPageLink(location.href); let downloadLink = await ParseDownloadLink(downloadPageLink); downloadLink = downloadLink.replace(/\?n=/, `?n=[${category}]`) console.log(`downloadPageLink = ${downloadPageLink}`) // for debug! console.log(`downloadLink = ${downloadLink}`); // for debug! // setup DOMs const downloadZipBtnElement = `<a id="YrDownloadBtn" class="btn" style="width:130px;" target="_blank" rel="noreferrer noopener" href=${downloadLink}>直接下載 (原生壓縮)</a>`; const statusElement = ` <div id="YrDirectDownloadStatusBlock" style="display: none;"> <div>重試次數: <span id="YrRetryCount"></span></div> <div style="padding-bottom: 3px;">目前狀態: <span id="YrStatus" style="color: blueviolet; font-weight: bold; font-size: 1.5em;"></span></div> <div>最後重試時間: <span id="YrLastRetry"></span></div> </div>`; const root = document.querySelector('.asTBcell.uwthumb'); root.insertAdjacentHTML('beforeend', downloadZipBtnElement); root.insertAdjacentHTML('beforeend', statusElement); const downloadZipBtn = document.querySelector('#YrDownloadBtn'); downloadZipBtn.addEventListener('click', DirectDownload); } async function SetupDownloadImageButton () { // setup DOMs const downloadImageBtnElement = `<a id="YrDownloadImageBtn" class="btn" style="width:130px;" target="_blank" rel="noreferrer noopener" href=#>直接下載 (網站圖片)</a>`; const statusElement = ` <div id="YrDownloadImageStatusBlock" style="display: none;"> <div>解析頁面: <span id="YrParsingPageId" style="color: blueviolet; font-weight: bold;"></span></div> <div style="padding-bottom: 3px;">目前狀態: <span id="YrDownloadImageStatus" style="color: blueviolet; font-weight: bold;"></span></div> </div>`; const root = document.querySelector('.asTBcell.uwthumb'); root.insertAdjacentHTML('beforeend', downloadImageBtnElement); root.insertAdjacentHTML('beforeend', statusElement); const downloadImageBtn = document.querySelector('#YrDownloadImageBtn'); downloadImageBtn.addEventListener('click', DownloadImages); } async function Run () { await SetupDirectDownloadButton(); await SetupDownloadImageButton(); } Run();