// ==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();