Dynasty Scans Batch Downloader

Download doujinshi from Dynasty Scans

  1. // ==UserScript==
  2. // @name Dynasty Scans Batch Downloader
  3. // @namespace mccranky83.github.io
  4. // @version 2024-10-23
  5. // @description Download doujinshi from Dynasty Scans
  6. // @author Mccranky83
  7. // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js
  8. // @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.0/FileSaver.min.js
  9. // @match https://dynasty-scans.com/series/*
  10. // @icon https://www.google.com/s2/favicons?sz=64&domain=dynasty-scans.com
  11. // @grant none
  12. // @license MIT
  13. // ==/UserScript==
  14.  
  15. class Semaphore {
  16. constructor() {
  17. this.counter = 200;
  18. this.queue = [];
  19. }
  20. async acquire() {
  21. this.counter > 0
  22. ? this.counter--
  23. : await new Promise((res) => {
  24. this.queue.push(res);
  25. });
  26. }
  27. release() {
  28. if (this.queue.length > 0) {
  29. this.queue.shift()();
  30. this.counter--;
  31. }
  32. this.counter++;
  33. }
  34. }
  35.  
  36. (() => {
  37. "use strict";
  38.  
  39. window.dl = dl;
  40. window.dlAll = dlAll;
  41. const s = new Semaphore();
  42. const h = {
  43. get(tar, key) {
  44. const val = Reflect.get(tar, key);
  45. if (typeof val === "object") return new Proxy(val, h);
  46. else return val;
  47. },
  48. set(tar, key, val) {
  49. Reflect.set(...arguments);
  50. const dl = $("#dl-all");
  51. if (key === "count") {
  52. Reflect.get(tar, key) ? dl.show() : dl.hide();
  53. }
  54. return true;
  55. },
  56. };
  57. const selected = new Proxy({ count: 0, index: [] }, h);
  58. $(".chapter-list dd").each((i, cur) => {
  59. $("<a>", {
  60. href: `javascript:;`,
  61. text: "Download",
  62. class: "label",
  63. }).appendTo(cur);
  64. $("<input>", { type: "checkbox", checked: false }).prependTo(cur);
  65. });
  66. $("dd")
  67. .slice(1)
  68. .find("a:last")
  69. .each((i, cur) => {
  70. $(cur).click(() => {
  71. dl(i + 1, s);
  72. });
  73. });
  74. $(".chapter-list").prepend(`
  75. <dd>
  76. <input type="checkbox">
  77. <span><b>Toggle All</b></span>
  78. </dd>
  79. `);
  80. const $checkbox = $("input[type='checkbox']");
  81. $checkbox.eq(0).on("change", function () {
  82. const checked = $(this).is(":checked");
  83. if (checked) {
  84. const length = $checkbox.length - 1;
  85. selected.count = length;
  86. selected.index = Array.from({ length }, (_, i) => i);
  87. } else {
  88. selected.count = 0;
  89. selected.index = [];
  90. }
  91. $checkbox.slice(1).each((_, cur) => {
  92. $(cur).prop("checked", checked);
  93. });
  94. console.log(JSON.stringify(selected));
  95. });
  96. $checkbox.slice(1).each(function (i) {
  97. $(this).on("change", () => {
  98. if ($(this).prop("checked")) {
  99. selected.count++;
  100. selected.index.push(i);
  101. selected.count === $checkbox.length - 1 &&
  102. $checkbox.eq(0).prop("checked", true);
  103. } else {
  104. selected.count--;
  105. selected.index.splice(selected.index.indexOf(i), 1);
  106. $checkbox.eq(0).prop("checked", false);
  107. }
  108. console.log(JSON.stringify(selected));
  109. });
  110. });
  111. $("<a>", {
  112. href: `javascript:;`,
  113. text: "Download all",
  114. class: "label",
  115. id: "dl-all",
  116. css: {
  117. display: "none",
  118. },
  119. }).appendTo("dd:first");
  120. $("a:contains('Download all')").click(() => {
  121. dlAll(selected, s);
  122. });
  123. })();
  124.  
  125. async function dl(i, s) {
  126. const dl = $("dd")
  127. .eq(i + 1)
  128. .find("a:last");
  129. const text = dl.text();
  130. dl.text("Loading...");
  131. const zip = new JSZip();
  132. const name =
  133. $(".tag-title b").text() +
  134. "_" +
  135. $("dd")
  136. .eq(i + 1)
  137. .find("a:first")
  138. .text();
  139. const folder = zip.folder(name);
  140. const src =
  141. location.origin + $("dd").slice(1).eq(i).find("a:first").attr("href");
  142. const { pages, iframe } = await getPages(src);
  143. iframe.remove();
  144. await Promise.all(
  145. pages.map(async (page) => {
  146. const url = location.origin + page.image;
  147. const filename = page.image.split("/").slice(-1)[0];
  148. await s.acquire();
  149. await fetch(url, { signal: AbortSignal.timeout(30_000) })
  150. .then((res) => res.arrayBuffer())
  151. .then((res) => {
  152. folder.file(filename, res, { binary: true });
  153. })
  154. .catch(() => {})
  155. .finally(s.release.bind(s));
  156. }),
  157. );
  158. saveAs(
  159. await zip.generateAsync({
  160. type: "blob",
  161. compression: "DEFLATE",
  162. compressionOptions: {
  163. level: 6,
  164. },
  165. }),
  166. name,
  167. );
  168. dl.text(text);
  169. }
  170.  
  171. async function dlAll(selected, s) {
  172. selected.index.forEach(async (i) => {
  173. await dl(i, s);
  174. selected.count = 0;
  175. selected.index = [];
  176. $("dd input").prop("checked", false);
  177. });
  178. }
  179.  
  180. async function getPages(src) {
  181. return new Promise((res) => {
  182. const iframe = $("<iframe>", {
  183. src,
  184. css: {
  185. display: "none",
  186. },
  187. });
  188. iframe.appendTo("body");
  189. iframe.on("load", () =>
  190. res({
  191. pages: iframe[0].contentWindow.pages,
  192. iframe,
  193. }),
  194. );
  195. });
  196. }