Skip to content

Instantly share code, notes, and snippets.

@hiroshil
Last active April 27, 2024 10:36
Show Gist options
  • Save hiroshil/86723bc557efa88931cf6b135e42a2b2 to your computer and use it in GitHub Desktop.
Save hiroshil/86723bc557efa88931cf6b135e42a2b2 to your computer and use it in GitHub Desktop.
Script to download manga on some domain of nhentai into a zip file (Note: Requires "Allow CORS" extension with "Access-Control-Allow-Origin" mode set to "*" in options page for script to work)
// ==UserScript==
// @name n210-dl
// @namespace https://gist.github.com/hiroshil
// @version 0.1.1
// @description Script used to download manga R18 on some domain of nhentai
// @license MIT
// @author hiroshil
// @source https://gist.github.com/hiroshil/86723bc557efa88931cf6b135e42a2b2
// @match http*://nhentai.to/g/*
// @match http*://nhentai.xxx/g/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=nhentai.to
// @downloadURL https://gist.github.com/hiroshil/86723bc557efa88931cf6b135e42a2b2/raw/n210-dl.user.js
// @updateURL https://gist.github.com/hiroshil/86723bc557efa88931cf6b135e42a2b2/raw/n210-dl.user.js
// ==/UserScript==
/* jshint esversion: 8 */
//https://stackoverflow.com/questions/5525071/how-to-wait-until-an-element-exists
//https://stackoverflow.com/questions/30008114/how-do-i-promisify-native-xhr
//https://stackoverflow.com/questions/5582574/how-to-check-if-a-string-contains-text-from-an-array-of-substrings-in-javascript
//https://stackoverflow.com/questions/39993676/code-inside-domcontentloaded-event-not-working
function waitForElm(selector) {
return new Promise(resolve => {
if (document.querySelectorAll(selector).length) {
return resolve(document.querySelectorAll(selector));
}
const observer = new MutationObserver(mutations => {
if (document.querySelectorAll(selector).length) {
resolve(document.querySelectorAll(selector));
observer.disconnect();
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
});
}
function waitForXHRRequest(url,method,responseType) {
return new Promise( (resolve, reject) => {
var res = new XMLHttpRequest();
res.timeout = 12000; // slow internet catch
res.responseType = responseType;
res.open(method, url, true);
res.send();
res.onload = () => {
if (res.status >= 200 && res.status < 400) {
resolve(res.response);
} else {
resolve(false);
}
}
res.onerror = () => {
resolve(false);
}
res.ontimeout = () => {
resolve(false);
}
});
}
function sleeper(ms) {
return function(x) {
return new Promise(resolve => setTimeout(() => resolve(x), ms));
};
}
function parseStr(text) {
/*
Checks if the string matches the format "number-number" with the second number larger.
Args:
text: The string to check.
Returns:
Two numbers if the string matches the format and the second number is larger, False otherwise.
*/
const pattern = /^\d+-\d+$/; // Matches "number-number" format
const match = pattern.exec(text);
if (match) {
const [num1, num2] = match[0].split("-"); // Split into two numbers
if (parseInt(num1) < parseInt(num2)){ // Check if second number is larger
return [parseInt(num1), parseInt(num2)]
}
}
return false;
}
function getType(idx) {
if (xxx210) return gallery_.images.pages[idx].split(",").at(0);
return gallery_.images.pages[idx].t;
}
async function downloadChapter(id=null,filename=null, callback=(e)=>{}, ret = 10){
const dlBtn = document.querySelector("#dlz");
if (!(dlBtn.classList.contains(btnDblTag))) {
dlBtn.classList.add(btnDblTag);
var s_p = 1;
var e_p = gallery_.num_pages;
let pt = prompt("Please enter the number of pages you want to download (Pattern: first page-last page. Example: 15-30)", "Click Continue to download all");
const pg = parseStr(pt)
if (pg) {
s_p = pg[0];
e_p = pg[1];
}
if (pt != null) {
const zipFileWriter = new zip.BlobWriter();
const zipWriter = new zip.ZipWriter(zipFileWriter);
for (let i = s_p; i <= e_p; i++) {
var ext;
switch (getType(i-1)) {
case "j":
ext = "jpg";
break;
case "p":
ext = "png";
break;
}
const fname = i.toString() + "." + ext;
const src = media_url_ + id + "/"+ fname;
console.log(((e_p-s_p)-(e_p-i)+1).toString() + "/" + (e_p-s_p+1).toString() + ": downloading "+src); //debug
var blob;
do {
blob = await waitForXHRRequest(src,'GET','blob');
ret -= 1;
if (!blob && !ret) {
alert("Error while downloading file: " + src);
callback(dlBtn);
return;
}
else
{
await delay(1000);
}
}
while (!blob && ret);
if (blob.type.includes("image")){
const blobReader = new zip.BlobReader(blob);
await zipWriter.add(fname, blobReader);
}
}
await zipWriter.close();
const zipFileBlob = await zipFileWriter.getData();
const anchor = document.createElement("a");
const clickEvent = new MouseEvent("click");
anchor.href = window.URL.createObjectURL(zipFileBlob);
anchor.download = filename;
anchor.dispatchEvent(clickEvent);
}
}
callback(dlBtn);
}
function setupButton(jNode) {
const btnDiv = jNode.parentElement;
const clonedElement = jNode.cloneNode(true); // Clone the element with all its content
clonedElement.id = "dlz";
if (xxx210) {
clonedElement.style.marginLeft = "10px";
}
else{
clonedElement.classList.remove(btnDblTag);
clonedElement.querySelector(".top").textContent = "Click to download as zip";
}
btnDiv.appendChild(clonedElement); // Append the cloned element to the last child
clonedElement.addEventListener("click", (e)=>{
downloadChapter(gallery_.media_id, gallery_.title.japanese + ".zip", (el)=>{ el.classList.remove(btnDblTag); });
});
}
const regex = /^https?:\/\/nhentai\.([^/]+)\/g\/(\d{6})\/?$/;
if (regex.test(location.href)) {
var xxx210 = (location.host.split(".").at(-1) == "xxx") ? true : false;
var gallery_, media_url_, btnDblTag;
document.onreadystatechange = async () => {
if (document.readyState === 'interactive') {
const srcUrl = document.querySelector("img[src*='cover'").src;
media_url_ = "https://" + new URL(srcUrl).host;
if (xxx210){
gallery_ = {
"media_id": document.querySelector("#load_id").value,
"media_server": document.querySelector("#load_server").value,
"media_dir": document.querySelector("#load_dir").value,
"images": {
"pages" : g_th["fl"]
},
"title": {
"japanese": document.querySelector("div.info > h2").innerText,
},
"num_pages": parseInt(document.querySelector("#load_pages").value)
};
media_url_ += "/" + gallery_.media_dir + "/";
btnDblTag = "disabled";
}
else{
gallery_ = gallery;
media_url_ += "/galleries/";
btnDblTag = "btn-disabled";
}
var xhr = await waitForXHRRequest("https://raw.githubusercontent.com/gildas-lormeau/zip.js/v2.7.17/dist/zip.js","GET",undefined);
eval(xhr);
const btn = await waitForElm("i[class*='fa-download']");
setupButton(btn[0].parentElement);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment