// ==UserScript==
// @name NHentai Konnichiwa
// @author naiymu
// @version 1.1.11
// @license MIT; https://raw.githubusercontent.com/naiymu/nhentai-konnichiwa/main/LICENSE
// @namespace https://github.com/naiymu/nhentai-konnichiwa
// @homepage https://github.com/naiymu/nhentai-konnichiwa
// @supportURL https://github.com/naiymu/nhentai-konnichiwa/issues
// @description A simple usercript for downloading doujinshi from NHentai and mirrors
// @match https://nhentai.net/*
// @match https://nhentai.xxx/*
// @match https://nyahentai.red/*
// @match https://nhentai.to/*
// @match https://nhentai.website/*
// @exclude /https:\/\/n.*hentai.(red|net|xxx|to|website)\/g\/[0-9]*\/[0-9]+\/?$/
// @connect nhentai.xxx
// @connect cdn.nload.xyz
// @connect i3.nhentai.net
// @connect cdn.nhentai.xxx
// @connect nhentai.com
// @connect t.dogehls.xyz
// @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.0/jszip.min.js
// @require https://unpkg.com/comlink@4.3.1/dist/umd/comlink.min.js
// @grant GM_addStyle
// @grant GM_setClipboard
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @run-at document-end
// @icon https://raw.githubusercontent.com/naiymu/nhentai-konnichiwa/main/assets/icon.png
// ==/UserScript==
GM_addStyle (
`
.relative {
position: relative !important;
}
.download-check {
position: absolute;
top: 0;
left: 0;
cursor: pointer;
height: 20px;
width: 20px;
accent-color: #ed2553;
}
.download-check:focus,
.download-check:hover {
box-shadow: 0 0 10px 0 #ed2553;
}
.red-box {
position: relative;
background-color: #ed2553;
color: white;
border: none;
outline: none;
font-size: 16px;
width: 80px;
height: 35px;
border-radius: 5px;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
cursor: pointer;
}
.percent {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%,-50%);
}
.downloading-span,
.compressing-span {
display: inline-block;
position: absolute;
top: 0;
left: 0;
width: 50px;
height: 100%;
border-radius: 5px;
}
.downloading-span {
background-color: #03c03c;
}
.compressing-span {
background-color: #0047ab;
}
.red-box:hover {
background-color: #4d4d4d;
}
.red-box>i,
.red-box>.download-check {
margin-right: 5px;
}
.download-check-all {
width: 20px;
height: 20px;
cursor: pointer;
accent-color: #1f1f1f;
}
.red-box>.download-check:focus,
.red-box>.download-check:hover {
box-shadow: none;
}
.red-box:disabled {
background-color: #1f1f1f;
cursor: default;
}
.download-div {
position: fixed;
bottom: 0;
left: 0;
display: flex;
flex-direction: column;
gap: 5px;
}
.div-horizontal {
flex-direction: row !important;
}
.fa-spinner,
.fa-circle-notch {
animation: spin 1.5s infinite linear;
}
@keyframes spin {
100% {transform: rotate(359deg)};
}
.config-wrapper {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
z-index: 999999;
visibility: hidden;
}
.visible {
visibility: visible;
}
.config-div {
position: absolute;
top: 50%;
left: 50%;
width: 100%;
height: 100%;
max-width: 850px;
max-height: 550px;
padding: 50px;
transform: translate(-50%, -50%);
background-color: #1f1f1f;
border-radius: 5px;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
overflow: scroll;
}
.config-element-div {
width: 100%;
display: inline-grid;
grid: auto / 200px auto;
margin-bottom: 15px
}
.config-element-div>input {
color: #000 !important;
}
.config-element-div>label {
grid-column-start: 1;
}
.config-element-div>input[type='checkbox'] {
justify-self: left;
}
.config-element-div>*:not(label) {
grid-column-start: 2;
border-radius: 0;
border: none;
background-color: #d9d9d9 !important;
}
.config-btn-div {
display: flex;
flex-direction: row;
gap: 10px;
align-items: center;
}
.config-reset:hover {
cursor: pointer;
color: #ed2553;
}
.heading {
width: 100%;
border-bottom: solid 1px #d9d9d9;
text-align: left;
}
`
);
const netMediaUrl = "https://i3.nhentai.net/galleries/";
const btnStates = {
enabled: "<i class='fa fa-download'></i>",
fetching: "<i id='btn-spinner' class='fa fa-spinner'></i>",
downloading: "<i id='btn-spinner' class='fa fa-circle-notch'></i>",
config: "<i class='fa fa-cog'></i>",
};
const titleFormats = {
PR: "pretty",
EN: "english",
JP: "japanese",
ID: "id",
};
const saveJSONModes = {
NO: "Don't save",
FI: "Save as JSON file",
CB: "Copy to clipboard",
};
const fileNameSeps = {
SP: "Space",
HY: "Hyphen",
US: "Underscore",
};
const btnOrientations = {
VR: "Vertical",
HR: "Horizontal",
};
const CONFIG = {
simulN: {
label: 'Download batch size',
type: 'int',
min: 1,
max: 50,
default: 10,
},
compressionLevel: {
label: 'Compression level',
type: 'int',
min: 0,
max: 9,
default: 0,
},
titleFormat: {
label: 'Title format',
type: 'select',
options: [titleFormats.PR,titleFormats.EN,titleFormats.JP,
titleFormats.ID],
default: titleFormats.PR,
},
fileNamePrep: {
label: 'Filename to prepend',
type: 'text',
size: 30,
default: "",
},
fileNameSep: {
label: 'Filename separator',
type: 'select',
options: [fileNameSeps.SP,fileNameSeps.HY,fileNameSeps.US],
default: fileNameSeps.SP,
},
saveJSONMode: {
label: 'Save JSON',
type: 'select',
options: [saveJSONModes.NO,saveJSONModes.FI,saveJSONModes.CB],
default: saveJSONModes.NO,
},
includeGroups: {
label: 'Include groups in authors',
type: 'checkbox',
default: false,
},
btnOrientation: {
label: 'Button orientation',
type: 'select',
options: [btnOrientations.VR, btnOrientations.HR],
default: btnOrientations.VR,
},
openInNewTab: {
label: 'Open galleries in new tab',
type: 'checkbox',
default: true,
},
autorestart: {
label: 'Auto restart downloads',
type: 'checkbox',
default: true,
}
};
const WORKER_THREAD_NUM = ((navigator && navigator.hardwareConcurrency) || 2) - 1;
class JSZipWorkerPool {
constructor() {
this.pool = [];
this.WORKER_URL = URL.createObjectURL(
new Blob(
[
`importScripts(
'https://unpkg.com/comlink/dist/umd/comlink.min.js',
'https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js'
);
class JSZipWorker {
constructor() {
this.zip = new JSZip;
}
file(title, name, {data:data}) {
this.zip.folder(title).file(name, data);
}
async generateAsync(options, onUpdate) {
const data = await this.zip.generateAsync(options, onUpdate);
const url = URL.createObjectURL(data);
return Comlink.transfer({url:url});
}
}
Comlink.expose(JSZipWorker);`
],
{type: 'text/javascript'}
)
);
for(let id=0; id<WORKER_THREAD_NUM; id++) {
this.pool.push({
id,
JSZip: null,
idle: true,
});
}
}
createWorker() {
const worker = new Worker(this.WORKER_URL);
return Comlink.wrap(worker);
}
async generateAsync(files, options, onUpdate) {
const worker = this.pool.find(({idle}) => idle);
if(!worker) throw new Error('No available JSZip worker');
worker.idle = false;
if(!worker.JSZip) worker.JSZip = this.createWorker();
const zip = await new worker.JSZip();
for(const {title, name, data} of files) {
await zip.file(title, name, Comlink.transfer({data}, [data]));
}
return zip
.generateAsync(
options,
Comlink.proxy((data) => onUpdate({workerId: worker.id, ...data}))
)
.then(({url}) => {
worker.idle = true;
return url;
});
}
}
const jsZipPool = new JSZipWorkerPool();
class JSZip {
constructor() {
this.files = [];
}
file(title, name, data) {
this.files.push({title, name, data});
}
generateAsync(options, onUpdate) {
return jsZipPool.generateAsync(this.files, options, onUpdate);
}
}
// nhentai.net API URL
const netAPI = "https://nhentai.net/api/gallery/";
// nhentai.xxx page URL
const xxxPage = "https://nhentai.xxx/g/";
// If we are on nhentai.net
const onNET = location.hostname == 'nhentai.net';
// DOM parser for later
var parser = new DOMParser();
// Saved config options
var configOptions = JSON.parse(GM_getValue('configOptions') || '{}');
// Buttons and Divs
var downloadDiv,
downloadBtn,
downloadPercent,
downloadingSpan,
compressingSpan,
configWrapper,
configDiv;
// Download info
var downloading = false,
cancelled = false,
total = 0,
downloaded = 0,
currentDownloads = 0,
queue = [],
info = JSON.parse(sessionStorage.getItem('info') || '[]');
// Final zip
var zip;
function disableButton(btnState) {
downloadingSpan.style.width = 0;
compressingSpan.style.width = 0;
downloadBtn.disabled = true;
downloadPercent.innerHTML = btnState;
}
function enableButton() {
downloadingSpan.style.width = 0;
compressingSpan.style.width = 0;
downloadBtn.disabled = false;
downloadPercent.innerHTML = btnStates.enabled;
}
function createNode(element, classes=[]) {
var node = document.createElement(element);
for(let cls of classes) {
node.classList.add(cls);
}
return node;
}
function createLabel(id, text) {
var label = createNode('label');
label.innerHTML = text;
label.setAttribute('for', id);
return label;
}
function getExtension(type) {
switch(type) {
case 'j': return '.jpg';
case 'p': return '.png';
case 'g': return '.gif';
}
}
function saveConfig(reset=false) {
const oldOpenInNewTab = configOptions.openInNewTab;
for(const [key, value] of Object.entries(CONFIG)) {
var element = document.getElementById(`config-${key}`);
var configValue;
switch(value.type) {
case 'checkbox':
configValue = element.checked;
break;
case 'int':
configValue = element.value;
if(configValue > value.max) {
configValue = value.max;
}
if(configValue < value.min) {
configValue = value.min;
}
break;
default:
configValue = element.value;
}
configValue = reset ? value.default : configValue;
configOptions[key] = configValue;
element.value = configValue;
if(value.type == 'checkbox') element.checked = configValue;
}
if(configOptions.btnOrientation == btnOrientations.HR) {
downloadDiv.classList.add('div-horizontal');
}
else {
downloadDiv.classList.remove('div-horizontal');
}
GM_setValue('configOptions', JSON.stringify(configOptions));
if(configOptions.openInNewTab != oldOpenInNewTab) {
var gLinks = document.querySelectorAll('.gallery > a, .gallerythumb');
for(let a of gLinks) {
if(configOptions.openInNewTab) {
a.setAttribute('target', '_blank');
}
else {
a.removeAttribute('target');
}
}
}
}
function addConfigMenu() {
var saveConfigBtn, closeConfigBtn, cancelAnchor, resetConfigAnchor;
var heading = createNode('h3', ['heading']);
heading.innerHTML = "nhentai-konnichiwa";
configDiv.appendChild(heading);
for(const [key, value] of Object.entries(CONFIG)) {
var div = createNode('div', ['config-element-div']);
var element, id, label;
switch(value.type) {
case 'select':
element = createNode('select');
for(let i=0; i<value.options.length; i++) {
var optionElement = createNode('option');
var option = value.options[i];
optionElement.text = option;
optionElement.value = option;
element.appendChild(optionElement);
}
break;
case 'int':
element = createNode('input');
element.type = 'number';
if(value.min != undefined) {
element.min = value.min;
}
if(value.max != undefined) {
element.max = value.max;
}
break;
case 'checkbox':
element = createNode('input');
element.type = 'checkbox';
break;
case 'text':
element = createNode('input');
element.type = 'text';
element.maxLength = value.size;
element.defaultValue = value.default;
break;
}
id = `config-${key}`;
element.id = id;
var configValue = (configOptions.hasOwnProperty(key))
? configOptions[key]
: value.default;
if(value.type == 'checkbox') {
element.checked = configValue;
}
else {
element.value = configValue;
}
// If key does not exist in configOptions, add it
if(!configOptions[key]) {
configOptions[key] = configValue;
}
label = createLabel(id, value.label);
div.appendChild(label);
div.appendChild(element);
configDiv.appendChild(div);
}
var btnDiv = createNode('div', ['config-btn-div']);
saveConfigBtn = createNode('button', ['red-box']);
saveConfigBtn.innerHTML = "Save";
saveConfigBtn.addEventListener('click', () => {
saveConfig();
});
closeConfigBtn = createNode('button', ['red-box']);
closeConfigBtn.innerHTML = "Close";
closeConfigBtn.addEventListener('click', () => {
configWrapper.classList.remove('visible');
});
cancelAnchor = createNode('a', ['config-reset']);
cancelAnchor.innerHTML = "Cancel downloads";
cancelAnchor.addEventListener('click', () => {
cancelDownload();
});
resetConfigAnchor = createNode('a', ['config-reset']);
resetConfigAnchor.innerHTML = 'Reset to default';
resetConfigAnchor.addEventListener('click', () => {
saveConfig(true);
});
btnDiv.appendChild(saveConfigBtn);
btnDiv.appendChild(closeConfigBtn);
btnDiv.appendChild(cancelAnchor);
btnDiv.appendChild(resetConfigAnchor);
configDiv.appendChild(btnDiv);
}
(async function() {
'use strict';
window.addEventListener('beforeunload', (e) => {
if(downloading) {
e.preventDefault();
return '';
}
});
var aList, configBtn, checkAllDiv, checkAll;
configWrapper = createNode('div', ['config-wrapper']);
configDiv = createNode('div', ['config-div']);
configWrapper.addEventListener('click', () => {
if(event.target != configWrapper) {
return;
}
configWrapper.classList.remove('visible');
});
configWrapper.appendChild(configDiv);
addConfigMenu();
aList = document.querySelectorAll(".gallery > a, #cover > a");
for(let a of aList) {
if(configOptions.openInNewTab) a.setAttribute('target', '_blank');
var ref = a.href;
var parent = a.parentElement;
var code;
code = ref.split("/g/")[1];
if(code.endsWith("/")) {
code = code.split("/")[0];
}
var check = createNode('input', ['download-check']);
check.type = 'checkbox';
check.value = code;
parent.classList.add("relative");
parent.appendChild(check);
}
if(configOptions.openInNewTab) {
aList = document.getElementsByClassName("gallerythumb");
for(let a of aList) {
a.setAttribute('target', '_blank');
}
}
let classes = ['download-div'];
if(configOptions.btnOrientation == btnOrientations.HR) {
classes.push('div-horizontal');
}
downloadDiv = createNode('div', classes);
downloadBtn = createNode('button', ['red-box']);
downloadPercent = createNode('span', ['percent']);
downloadingSpan = createNode('span', ['downloading-span']);
compressingSpan = createNode('span', ['compressing-span']);
enableButton();
downloadBtn.appendChild(downloadingSpan);
downloadBtn.appendChild(compressingSpan);
downloadBtn.appendChild(downloadPercent);
configBtn = createNode('button', ['red-box']);
configBtn.innerHTML = btnStates.config;
configBtn.addEventListener("click", (event) => {
configWrapper.classList.add('visible');
});
checkAllDiv = createNode('div', ['red-box', 'relative']);
checkAll = createNode('input', ['download-check-all']);
checkAll.type = 'checkbox';
checkAll.addEventListener('change', () => {
let toCheck = checkAll.checked;
let boxes = document.querySelectorAll('.download-check');
for(let box of boxes) {
if(box === checkAll) {
continue;
}
box.checked = toCheck;
}
});
checkAllDiv.appendChild(checkAll);
downloadBtn.addEventListener("click", async () => {
var checked = document.querySelectorAll(".download-check:checked");
if(checked.length > 0) {
disableButton(btnStates.fetching);
}
else {
return;
}
zip = await new JSZip();
switch(configOptions.fileNameSep) {
case fileNameSeps.SP: configOptions.fileNameSep = " "; break;
case fileNameSeps.HY: configOptions.fileNameSep = "-"; break;
case fileNameSeps.US: configOptions.fileNameSep = "_"; break;
}
for(let c of checked) {
c.checked = false;
}
for(const c of checked) {
var code = c.getAttribute("value");
await addInfo(code);
sessionStorage.setItem('info', JSON.stringify(info));
}
startDownload();
});
downloadDiv.appendChild(downloadBtn);
downloadDiv.appendChild(configBtn);
downloadDiv.appendChild(checkAllDiv);
document.body.appendChild(downloadDiv);
document.body.appendChild(configWrapper);
if(info.length > 0) {
if(configOptions.autorestart) {
queue = [];
startDownload();
}
else {
info = [];
sessionStorage.removeItem('info');
}
}
})();
function startDownload() {
downloading = true;
cancelled = false;
currentDownloads = 0;
downloaded = 0;
zip = new JSZip();
populateQueue();
total = queue.length;
disableButton(btnStates.downloading);
downloadQueue();
}
function cancelDownload() {
info = [];
queue = [];
sessionStorage.removeItem('info');
currentDownloads = 0;
downloading = false;
cancelled = true;
enableButton();
}
function cleanString(string) {
string = string.replace(/(\.+$)|(^\.+)|(\|)/g, '');
string = string.replace(/\\\/\:\;/g, configOptions.fileNameSep);
string = string.replace(/\s\s+/, ' ');
string = string.trim();
return string;
}
async function makeGetRequest(url, code = null) {
return new Promise((resolve, reject) => {
if(onNET) {
fetch(url, {
method: 'GET',
mode: 'same-origin',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json'
},
referrerPolicy: 'same-origin',
})
.then(response => resolve(response.json()));
}
else {
GM_xmlhttpRequest({
method: "GET",
url: url,
onload: (response) => {
resolve(parseXXXResponse(response, code));
},
onerror: (error) => {
reject(error);
}
});
}
});
}
function addInfoNET(obj, code) {
var title;
if(configOptions.titleFormat == 'id') {
title = `${obj.id}`;
}
else {
title = obj.title[configOptions.titleFormat];
title = cleanString(title);
}
var pages = obj.num_pages;
var tagList = obj.tags;
var artists = [];
var tags = [];
for(let i=0; i<tagList.length; i++) {
let tagItem = tagList[i];
if(tagItem.type == "artist"
|| (configOptions.includeGroups && tagItem.type == "group")) {
artists.push(tagItem.name);
}
if(tagItem.type == "tag") {
tags.push(tagItem.name);
}
}
var mediaUrl = `${netMediaUrl}${obj.media_id}/`;
const constTitleExists = info.some((el) => el.title === title);
if(constTitleExists) {
title += " - "+code;
}
var fileNamePrep = configOptions.fileNamePrep;
fileNamePrep = cleanString(fileNamePrep);
var namePrep = "";
if(fileNamePrep != "") {
namePrep = fileNamePrep + configOptions.fileNameSep;
}
var coverExtension = getExtension(obj.images.pages[0].t);
info.push({
code: code,
title: title,
artists: artists,
tags: tags,
pages: pages,
mediaUrl: mediaUrl,
namePrep: namePrep,
coverExtension: coverExtension,
pagesInfo: obj.images.pages,
});
}
async function addInfo(code) {
var apiUrl, obj;
if(onNET) {
apiUrl = netAPI + code;
obj = await makeGetRequest(apiUrl);
addInfoNET(obj, code);
}
else {
apiUrl = xxxPage + code;
obj = await makeGetRequest(apiUrl, code);
info.push(obj);
}
}
function parseXXXResponse(response, code) {
var htmlDoc = parser.parseFromString(response.responseText,
'text/html');
var title, artists = [], tags = [], pages, mediaUrl, pagesInfo = [], coverExtension;
var titleTemplate = string => {
return `${string}.title > span`;
}
const cleanRegex = /(\[[^\]]*\])|(\([^)]*\))|(\{[^}]*\})|([\.\|\~]*)/g;
switch(configOptions.titleFormat) {
case 'english':
title = htmlDoc.querySelector(titleTemplate('h1'));
title = title.textContent;
break;
case 'japanese':
title = htmlDoc.querySelector(titleTemplate('h2'));
title = title.textContent;
break;
case 'pretty':
default:
title = htmlDoc.querySelector(titleTemplate('h1'));
title = title.textContent;
title = title.replace(cleanRegex, '');
title = title.replace(/\s\s+/g, ' ');
title = title.trim();
title = title.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
}
var tagTemplate = string => {
return `a[href*='${string}/'] > span.name`;
}
var artistSpans = htmlDoc.querySelectorAll(tagTemplate('artist'));
for(var artist of artistSpans) {
artists.push(artist.textContent);
}
if(configOptions.includeGroups) {
var groupSpans = htmlDoc.querySelectorAll(tagTemplate('group'));
for(var group of groupSpans) {
artists.push(group.textContent);
}
}
var tagSpans = htmlDoc.querySelectorAll(tagTemplate('tag'));
for(var tag of tagSpans) {
tags.push(tag.textContent);
}
pages = htmlDoc.querySelector(".tag[href*='#'] > span.name");
pages = parseInt(pages.textContent);
var fileNamePrep = configOptions.fileNamePrep;
fileNamePrep = cleanString(fileNamePrep);
var namePrep = "";
if(fileNamePrep != "") {
namePrep = fileNamePrep + configOptions.fileNameSep;
}
var thumbs = htmlDoc.querySelectorAll("a.gallerythumb > img");
mediaUrl = thumbs[0].src;
mediaUrl = mediaUrl.substring(0, mediaUrl.lastIndexOf("/")+1);
for(var thumb of thumbs) {
var extension = thumb.src.split('.').pop().charAt(0);
pagesInfo.push({t: extension});
}
coverExtension = getExtension(pagesInfo[0].t);
var obj = {
code: code,
title: title,
artists: artists,
tags: tags,
pages: pages,
mediaUrl: mediaUrl,
namePrep: namePrep,
coverExtension: coverExtension,
pagesInfo: pagesInfo,
}
return obj;
}
async function addToQueue(item, mediaId=null) {
var pages = item.pages;
var title = item.title;
var namePrep = item.namePrep;
var mediaUrl = item.mediaUrl;
for(let i=0; i<pages; i++) {
var extension = getExtension(item.pagesInfo[i].t);
let page = i + 1;
var imgUrl = `${mediaUrl}${page}${extension}`;
queue.push({
page: page,
url: imgUrl,
title: title,
namePrep: namePrep,
extension: extension,
});
}
}
function populateQueue() {
for(const item of info) {
addToQueue(item);
}
}
async function downloadQueue() {
while(queue.length > 0) {
if(currentDownloads >= configOptions.simulN) {
await sleep(125);
continue;
}
var item = queue.shift();
download(item);
}
}
function saveJSON(fileName) {
var data = [];
for(var item of info) {
data.push({
dirName: item.title,
dirCover: `${item.namePrep}1${item.coverExtension}`,
authors: item.artists,
tags: item.tags,
});
}
var fileContent = {
'directories': data
};
fileContent = JSON.stringify(fileContent, null, 2);
if(configOptions.saveJSONMode == saveJSONModes.CB) {
GM_setClipboard(fileContent, 'text');
return;
}
fileContent = new TextEncoder().encode(fileContent);
zip.file('', fileName, fileContent.buffer);
}
function generateZip() {
var dateName = Date.now();
var compressionType = configOptions.compressionLevel == 0
? 'STORE'
: 'DEFLATE';
var zipName = `${dateName}.zip`;
var jsonName = `${dateName}.json`;
if(configOptions.saveJSONMode != saveJSONModes.NO) {
saveJSON(jsonName);
}
zip.generateAsync(
{
type: 'blob',
compression: compressionType,
compressionOptions: {
level: configOptions.compressionLevel,
}},
({workerId, percent, currentFile}) => {
var fraction = percent / 100;
if(fraction == 1) {
enableButton();
return;
}
downloadPercent.innerHTML = `${percent.toFixed(2)}%`;
compressingSpan.style.width = fraction * downloadBtn.offsetWidth + 'px';
}
)
.then((url) => {
var a = createNode('a');
a.download = zipName;
a.href = url;
a.click();
})
.then(() => {
info = [];
sessionStorage.removeItem('info');
downloading = false;
});
}
function download(item) {
currentDownloads++;
const fileName = `${item.namePrep}${item.page}${item.extension}`;
GM_xmlhttpRequest({
method: 'GET',
url: item.url,
responseType: 'arraybuffer',
onload: (response) => {
if(cancelled) return;
var data = response.response;
zip.file(item.title, fileName, data);
currentDownloads--;
downloaded++;
var fraction = downloaded/total;
downloadPercent.innerHTML = `${(fraction * 100).toFixed(2)}%`;
downloadingSpan.style.width = fraction * downloadBtn.offsetWidth + 'px';
if(queue.length == 0 && currentDownloads <= 0) {
disableButton(btnStates.downloading);
generateZip();
}
},
onerror: (error) => {
currentDownloads--;
console.warn(`Could not download '${item.title}' - page ${item.page}`);
}
});
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}