n210-dl

Script used to download manga R18 on some domain of nhentai

  1. // ==UserScript==
  2. // @name n210-dl
  3. // @namespace https://gist.github.com/hiroshil
  4. // @version 0.1.1
  5. // @description Script used to download manga R18 on some domain of nhentai
  6. // @license MIT
  7. // @author hiroshil
  8. // @source https://gist.github.com/hiroshil/86723bc557efa88931cf6b135e42a2b2
  9. // @match http*://nhentai.to/g/*
  10. // @match http*://nhentai.xxx/g/*
  11. // @icon https://www.google.com/s2/favicons?sz=64&domain=nhentai.to
  12. // ==/UserScript==
  13. /* jshint esversion: 8 */
  14.  
  15. //https://stackoverflow.com/questions/5525071/how-to-wait-until-an-element-exists
  16. //https://stackoverflow.com/questions/30008114/how-do-i-promisify-native-xhr
  17. //https://stackoverflow.com/questions/5582574/how-to-check-if-a-string-contains-text-from-an-array-of-substrings-in-javascript
  18. //https://stackoverflow.com/questions/39993676/code-inside-domcontentloaded-event-not-working
  19.  
  20. function waitForElm(selector) {
  21. return new Promise(resolve => {
  22. if (document.querySelectorAll(selector).length) {
  23. return resolve(document.querySelectorAll(selector));
  24. }
  25. const observer = new MutationObserver(mutations => {
  26. if (document.querySelectorAll(selector).length) {
  27. resolve(document.querySelectorAll(selector));
  28. observer.disconnect();
  29. }
  30. });
  31. observer.observe(document.body, {
  32. childList: true,
  33. subtree: true
  34. });
  35. });
  36. }
  37. function waitForXHRRequest(url,method,responseType) {
  38. return new Promise( (resolve, reject) => {
  39. var res = new XMLHttpRequest();
  40. res.timeout = 12000; // slow internet catch
  41. res.responseType = responseType;
  42. res.open(method, url, true);
  43. res.send();
  44. res.onload = () => {
  45. if (res.status >= 200 && res.status < 400) {
  46. resolve(res.response);
  47. } else {
  48. resolve(false);
  49. }
  50. }
  51. res.onerror = () => {
  52. resolve(false);
  53. }
  54. res.ontimeout = () => {
  55. resolve(false);
  56. }
  57. });
  58.  
  59. }
  60. function sleeper(ms) {
  61. return function(x) {
  62. return new Promise(resolve => setTimeout(() => resolve(x), ms));
  63. };
  64. }
  65. function parseStr(text) {
  66. /*
  67. Checks if the string matches the format "number-number" with the second number larger.
  68.  
  69. Args:
  70. text: The string to check.
  71.  
  72. Returns:
  73. Two numbers if the string matches the format and the second number is larger, False otherwise.
  74. */
  75. const pattern = /^\d+-\d+$/; // Matches "number-number" format
  76. const match = pattern.exec(text);
  77.  
  78. if (match) {
  79. const [num1, num2] = match[0].split("-"); // Split into two numbers
  80. if (parseInt(num1) < parseInt(num2)){ // Check if second number is larger
  81. return [parseInt(num1), parseInt(num2)]
  82. }
  83. }
  84.  
  85. return false;
  86. }
  87. function getType(idx) {
  88. if (xxx210) return gallery_.images.pages[idx].split(",").at(0);
  89. return gallery_.images.pages[idx].t;
  90. }
  91. async function downloadChapter(id=null,filename=null, callback=(e)=>{}, ret = 10){
  92. const dlBtn = document.querySelector("#dlz");
  93. if (!(dlBtn.classList.contains(btnDblTag))) {
  94. dlBtn.classList.add(btnDblTag);
  95. var s_p = 1;
  96. var e_p = gallery_.num_pages;
  97. 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");
  98. const pg = parseStr(pt)
  99. if (pg) {
  100. s_p = pg[0];
  101. e_p = pg[1];
  102. }
  103. if (pt != null) {
  104. const zipFileWriter = new zip.BlobWriter();
  105. const zipWriter = new zip.ZipWriter(zipFileWriter);
  106. for (let i = s_p; i <= e_p; i++) {
  107. var ext;
  108. switch (getType(i-1)) {
  109. case "j":
  110. ext = "jpg";
  111. break;
  112. case "p":
  113. ext = "png";
  114. break;
  115. }
  116. const fname = i.toString() + "." + ext;
  117. const src = media_url_ + id + "/"+ fname;
  118. console.log(((e_p-s_p)-(e_p-i)+1).toString() + "/" + (e_p-s_p+1).toString() + ": downloading "+src); //debug
  119. var blob;
  120. do {
  121. blob = await waitForXHRRequest(src,'GET','blob');
  122. ret -= 1;
  123. if (!blob && !ret) {
  124. alert("Error while downloading file: " + src);
  125. callback(dlBtn);
  126. return;
  127. }
  128. else
  129. {
  130. await delay(1000);
  131. }
  132. }
  133. while (!blob && ret);
  134. if (blob.type.includes("image")){
  135. const blobReader = new zip.BlobReader(blob);
  136. await zipWriter.add(fname, blobReader);
  137. }
  138. }
  139. await zipWriter.close();
  140. const zipFileBlob = await zipFileWriter.getData();
  141. const anchor = document.createElement("a");
  142. const clickEvent = new MouseEvent("click");
  143. anchor.href = window.URL.createObjectURL(zipFileBlob);
  144. anchor.download = filename;
  145. anchor.dispatchEvent(clickEvent);
  146. }
  147. }
  148. callback(dlBtn);
  149. }
  150. function setupButton(jNode) {
  151. const btnDiv = jNode.parentElement;
  152. const clonedElement = jNode.cloneNode(true); // Clone the element with all its content
  153. clonedElement.id = "dlz";
  154. if (xxx210) {
  155. clonedElement.style.marginLeft = "10px";
  156. }
  157. else{
  158. clonedElement.classList.remove(btnDblTag);
  159. clonedElement.querySelector(".top").textContent = "Click to download as zip";
  160. }
  161.  
  162. btnDiv.appendChild(clonedElement); // Append the cloned element to the last child
  163.  
  164. clonedElement.addEventListener("click", (e)=>{
  165. downloadChapter(gallery_.media_id, gallery_.title.japanese + ".zip", (el)=>{ el.classList.remove(btnDblTag); });
  166. });
  167. }
  168. const regex = /^https?:\/\/nhentai\.([^/]+)\/g\/(\d{6})\/?$/;
  169. if (regex.test(location.href)) {
  170. var xxx210 = (location.host.split(".").at(-1) == "xxx") ? true : false;
  171. var gallery_, media_url_, btnDblTag;
  172. document.onreadystatechange = async () => {
  173. if (document.readyState === 'interactive') {
  174. const srcUrl = document.querySelector("img[src*='cover'").src;
  175. media_url_ = "https://" + new URL(srcUrl).host;
  176. if (xxx210){
  177. gallery_ = {
  178. "media_id": document.querySelector("#load_id").value,
  179. "media_server": document.querySelector("#load_server").value,
  180. "media_dir": document.querySelector("#load_dir").value,
  181. "images": {
  182. "pages" : g_th["fl"]
  183. },
  184. "title": {
  185. "japanese": document.querySelector("div.info > h2").innerText,
  186. },
  187. "num_pages": parseInt(document.querySelector("#load_pages").value)
  188. };
  189. media_url_ += "/" + gallery_.media_dir + "/";
  190. btnDblTag = "disabled";
  191. }
  192. else{
  193. gallery_ = gallery;
  194. media_url_ += "/galleries/";
  195. btnDblTag = "btn-disabled";
  196. }
  197. var xhr = await waitForXHRRequest("https://raw.githubusercontent.com/gildas-lormeau/zip.js/v2.7.17/dist/zip.js","GET",undefined);
  198. eval(xhr);
  199. const btn = await waitForElm("i[class*='fa-download']");
  200. setupButton(btn[0].parentElement);
  201. }
  202. }
  203. }