akuma.Moe Downloader

Helps download mangas from akuma.moe with ComicInfo.xml

  1. // ==UserScript==
  2. // @name akuma.Moe Downloader
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.1a
  5. // @description Helps download mangas from akuma.moe with ComicInfo.xml
  6. // @author Nuark & temp.user
  7. // @connect s1.akuma.moe
  8. // @connect s2.akuma.moe
  9. // @match https://*akuma.moe/*
  10. // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.9.1/jszip.min.js
  11. // @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.8/FileSaver.min.js
  12. // @grant GM_xmlhttpRequest
  13. // @grant GM_download
  14. // @run-at document-end
  15. // @license MIT
  16. // ==/UserScript==
  17.  
  18. (function() {
  19. 'use strict';
  20. let w = unsafeWindow;
  21. let jq = w.$;
  22.  
  23. let blobholder = {
  24. init: function(filename, details, maxcount, caller) {
  25. if (!filename.endsWith('.zip')) {
  26. filename += '.zip';
  27. }
  28. this.filename = filename;
  29. this.details = details;
  30. this.maxcount = maxcount;
  31. this.caller = caller;
  32. },
  33. blobarray: [],
  34. currcount: 0,
  35. addBlob: function(blob) {
  36. this.blobarray.push(blob);
  37. this.currcount++;
  38. console.log("blob insertion dispatched", blob, this.currcount, this.maxcount);
  39. if (this.currcount === this.maxcount) {
  40. this.callback();
  41. this.caller.text("Serializing...");
  42. console.log("calling serialization", blob, this.currcount, this.maxcount);
  43. }
  44. },
  45. callback: function() {
  46. let zip = new JSZip();
  47. let comicInfoContent = createComicInfo();
  48. zip.file("Comicinfo.xml", comicInfoContent);
  49. let img = zip.folder("images");
  50. this.blobarray.forEach(file => {
  51. img.file(file[0], file[1]);
  52. });
  53. zip.generateAsync({type:"blob"}, function updateCallback(metadata) {
  54. blobholder.caller.text(metadata.percent.toFixed(2) + '%');
  55. })
  56.  
  57. .then(function(content) {
  58. saveAs(content, blobholder.filename);
  59. blobholder.caller.toggleClass("btn-info").toggleClass("btn-success");
  60. blobholder.caller.text("Done");
  61. });
  62. }
  63. }
  64.  
  65. w.zxc = async function (caller) {
  66. caller = jq(caller);
  67. let todo = w.pag.cnt;
  68. let tpl = "{origin}{pathname}/{page}".replace(/(\{origin\})/, location.origin).replace(/(\{pathname\})/, location.pathname);
  69. blobholder.init(jq(".entry-title").text(), "", todo, caller);
  70.  
  71. caller.text(`Pages: ${todo}`);
  72.  
  73. for (let i = 1; i <= todo; i++) {
  74. caller.text(`Fetching ${i} of ${todo}...`);
  75. let page_response = await fetch(tpl.replace(/(\{page\})/, i));
  76. let html = await page_response.text();
  77. let div = jq("<div>").html(html.replace(/(script)/g, "scr").replace(/(link)/g, "lnk"))[0];
  78. let link = div.querySelector("#image-container > img").src;
  79. GM_xmlhttpRequest({
  80. method: "GET",
  81. url: link,
  82. synchronous: true,
  83. responseType: "blob",
  84. onload: function (response) {
  85. let title = response.finalUrl .split("/").pop();;
  86. let blob = response.response;
  87. blobholder.addBlob([title, blob]);
  88. }
  89. });
  90. }
  91. }
  92.  
  93. function init() { //TODO: location.pathname detection
  94. let new_btn = jq("#start").clone();
  95. new_btn.id = "download";
  96. new_btn.text("Download");
  97. new_btn.attr("href", "#download");
  98. new_btn.attr("title", "Download");
  99. new_btn.toggleClass("btn-primary").toggleClass("btn-info");
  100. new_btn.attr("onclick", "window.zxc(this)");
  101. jq("#start").parent().append(new_btn);
  102. }
  103.  
  104. function createComicInfo() {
  105. const title = document.querySelector('h1.entry-title')?.textContent.trim();
  106. const categories = Array.from(document.querySelectorAll('li.meta-data .value a[href*="category"]')).map(el => el.textContent.trim());
  107. const languages = Array.from(document.querySelectorAll('li.meta-data .value a[href*="language"]')).map(el => el.textContent.trim());
  108. const artists = Array.from(document.querySelectorAll('li.meta-data .value a[href*="artist"], li.meta-data .value a[href*="group"]')).map(el => el.textContent.trim());
  109. const maleTags = Array.from(document.querySelectorAll('li.meta-data .value a[href*="male"]')).map(el => `male:${el.textContent.trim()}`);
  110. const femaleTags = Array.from(document.querySelectorAll('li.meta-data .value a[href*="female"]')).map(el => `female:${el.textContent.trim()}`);
  111. const mixedTags = Array.from(document.querySelectorAll('li.meta-data .value a[href*="mixed"]')).map(el => `mixed:${el.textContent.trim()}`);
  112. const otherTags = Array.from(document.querySelectorAll('li.meta-data .value a[href*="other"]')).map(el => `other:${el.textContent.trim()}`);
  113. const tags = [...maleTags, ...femaleTags, ...mixedTags, ...otherTags];
  114. const date = document.querySelector('li.meta-data.date .value time')?.getAttribute('datetime').split(' ')[0]; // YYYY-MM-DD
  115. const pages = document.querySelector('li.meta-data.pages .value')?.textContent.trim();
  116. const url = window.location.href;
  117.  
  118. const comicInfo = `<?xml version="1.0"?>
  119. <ComicInfo xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  120. <Title>${title}</Title>
  121. <Format>${categories.join(', ')}</Format>
  122. <Language>${languages.join(', ')}</Language>
  123. <Writer>${artists.join(', ')}</Writer>
  124. <Tags>${tags.join(', ')}</Tags>
  125. <Date>${date}</Date>
  126. <Pages>${pages}</Pages>
  127. <Web>${url}</Web>
  128. </Comicinfo>`;
  129.  
  130. return comicInfo;
  131. }
  132.  
  133. init();
  134. })();