MRM Downloader

Download video and bulk images from myreadingmanga manga/doujin page.

  1. // ==UserScript==
  2. // @name MRM Downloader
  3. // @namespace https://nyt92.eu.org
  4. // @version 2025-3-20
  5. // @description Download video and bulk images from myreadingmanga manga/doujin page.
  6. // @author nyt92
  7. // @match https://myreadingmanga.info/*
  8. // @exclude https://myreadingmanga.info/about/
  9. // @exclude https://myreadingmanga.info/whats-that-book/
  10. // @exclude https://myreadingmanga.info/upload
  11. // @exclude https://myreadingmanga.info/popular/*
  12. // @exclude https://myreadingmanga.info/video/*
  13. // @exclude https://myreadingmanga.info/cats/*
  14. // @exclude https://myreadingmanga.info/pairing/*
  15. // @exclude https://myreadingmanga.info/group/*
  16. // @exclude https://myreadingmanga.info/privacy-policy/
  17. // @exclude https://myreadingmanga.info/dmca-notice/
  18. // @exclude https://myreadingmanga.info/contact/
  19. // @exclude https://myreadingmanga.info/terms-service/
  20. // @exclude https://myreadingmanga.info/sitemap/
  21. // @exclude https://myreadingmanga.info/my-bookmark/
  22. // @exclude https://myreadingmanga.info/tag/*
  23. // @exclude https://myreadingmanga.info/genre/*
  24. // @exclude https://myreadingmanga.info/status/*
  25. // @exclude https://myreadingmanga.info/lang/*
  26. // @exclude https://myreadingmanga.info/yaoi-manga/*
  27. // @exclude https://myreadingmanga.info/manhwa/*
  28. // @connect myreadingmanga.info
  29. // @connect i1.myreadingmanga.info
  30. // @connect i2.myreadingmanga.info
  31. // @connect i3.myreadingmanga.info
  32. // @connect i4.myreadingmanga.info
  33. // @connect i5.myreadingmanga.info
  34. // @connect i6.myreadingmanga.info
  35. // @supportURL https://github.com/NYT92/mrm-downloader
  36. // @icon https://www.google.com/s2/favicons?sz=64&domain=myreadingmanga.info
  37. // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js
  38. // @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js
  39. // @grant GM_xmlhttpRequest
  40. // @license GPLv3
  41. // ==/UserScript==
  42.  
  43. // THIS USERSCRIPT IS LICENSE UNDER THE GPLv3 License
  44. (function () {
  45. ("use strict");
  46.  
  47. const excludedPaths = [
  48. "/about/",
  49. "/upload/",
  50. "/whats-that-book/",
  51. "/popular/",
  52. "/video/",
  53. "/cats/",
  54. "/pairing/",
  55. "/group/",
  56. "/privacy-policy/",
  57. "/dmca-notice/",
  58. "/contact/",
  59. "/terms-service/",
  60. "/sitemap/",
  61. "/my-bookmark/",
  62. "/tag/",
  63. "/genre/",
  64. "/status/",
  65. "/lang/",
  66. "/yaoi-manga/",
  67. "/manhwa/",
  68. ];
  69.  
  70. const currentPath = window.location.pathname;
  71. if (excludedPaths.some((path) => currentPath.startsWith(path))) {
  72. return;
  73. }
  74.  
  75. const wpadminbar = document.querySelector("#wpadminbar");
  76. if (wpadminbar) {
  77. wpadminbar.remove();
  78. document.documentElement.setAttribute("style", "margin-top: 0px !important;");
  79. }
  80.  
  81. const style = document.createElement("style");
  82. style.textContent = `
  83. .mrm-dl-btn {
  84. background-color: #ffab23;
  85. border-radius: 5px;
  86. display: inline-block;
  87. cursor: pointer;
  88. color: black;
  89. font-size: 16px;
  90. font-weight: bold;
  91. padding: 16px 32px;
  92. text-decoration: none;
  93. }
  94. .mrm-dl-btn:hover {
  95. background-color:rgb(196, 120, 24);
  96. }
  97. .mrm-dl-btn:active {
  98. position: relative;
  99. top: 1px;
  100. }
  101. `;
  102. document.head.appendChild(style);
  103.  
  104. function saveCookies() {
  105. const cookies = prompt("Please paste your cookies here:");
  106. if (cookies) {
  107. localStorage.setItem("mrm_cookies", cookies);
  108. alert("Cookies saved!");
  109. window.location.reload();
  110. }
  111. }
  112.  
  113. const cookiesBtn = document.createElement("button");
  114. cookiesBtn.setAttribute("class", "mrm-dl-btn");
  115. cookiesBtn.id = "saveCookiesBtn";
  116. cookiesBtn.textContent = "Load 🍪";
  117. cookiesBtn.style.cssText =
  118. "position: fixed; bottom: 10px; right: 10px; z-index: 9999;";
  119. document.body.appendChild(cookiesBtn);
  120.  
  121. cookiesBtn.addEventListener("click", saveCookies);
  122.  
  123. const title =
  124. document
  125. .querySelector(".entry-header h1.entry-title")
  126. ?.textContent.trim() || "Untitled";
  127.  
  128. const imageDlBtn = document.createElement("button");
  129. imageDlBtn.setAttribute("class", "mrm-dl-btn");
  130. imageDlBtn.id = "downloadImagesBtn";
  131. imageDlBtn.textContent = "Download Images (.zip)";
  132. imageDlBtn.style.cssText =
  133. "position: fixed; top: 10px; right: 10px; z-index: 9999;";
  134.  
  135. const videoDlBtn = document.createElement("button");
  136. videoDlBtn.setAttribute("class", "mrm-dl-btn");
  137. videoDlBtn.id = "downloadVideoBtn";
  138. videoDlBtn.textContent = "Download Video";
  139. videoDlBtn.style.cssText =
  140. "position: fixed; top: 10px; right: 10px; z-index: 9999;";
  141.  
  142. const progressBar = document.createElement("div");
  143. progressBar.id = "downloadProgress";
  144. progressBar.style.cssText =
  145. "position: fixed; top:120px; right: 10px; width: 235px; right: 10px; height: 20px; background-color: #f0f0f0; display: none; z-index: 9999;";
  146.  
  147. const progressInner = document.createElement("div");
  148. progressInner.style.cssText =
  149. "width: 0%; height: 100%; background-color: #4CAF50; transition: width 0.5s;";
  150. progressBar.appendChild(progressInner);
  151.  
  152. const progressText = document.createElement("div");
  153. progressText.style.cssText =
  154. "position: fixed; top: 145px; right: 10px; z-index: 9999; display: none;";
  155. progressText.textContent = "Preparing download...";
  156.  
  157. const checkVidinTag = Array.from(
  158. document.querySelectorAll(".entry-categories a")
  159. ).map((tag) => tag.textContent.trim().toLowerCase());
  160.  
  161. const hasVideo = document.querySelector("#MRM_video") !== null;
  162. const hasYouTube =
  163. document.querySelector("iframe[src*='youtube.com']") !== null;
  164. const isHomePage = document.querySelector(".content-archive") !== null;
  165.  
  166. const imageSelectors = [
  167. ".img-myreadingmanga",
  168. ".img-myreadingmanga img",
  169. ".entry-content img",
  170. ".separator img",
  171. "img[decoding='async']",
  172. ];
  173.  
  174. let hasImages = false;
  175. for (const selector of imageSelectors) {
  176. if (document.querySelectorAll(selector).length > 0) {
  177. hasImages = true;
  178. break;
  179. }
  180. }
  181. if (
  182. excludedPaths.some((path) => currentPath.startsWith(path)) ||
  183. hasYouTube ||
  184. isHomePage
  185. ) {
  186. return;
  187. }
  188. if (checkVidinTag.includes("video") && hasVideo) {
  189. document.body.appendChild(videoDlBtn);
  190. } else if (!checkVidinTag.includes("video") && hasImages) {
  191. document.body.appendChild(imageDlBtn);
  192. } else {
  193. return;
  194. }
  195.  
  196. document.body.appendChild(progressBar);
  197. document.body.appendChild(progressText);
  198.  
  199. console.log(
  200. "Info: Cookies are required for this script to work due to browser limitations and Cloudflare protection. See https://github.com/NYT92/mrm-downloader/tree/main?tab=readme-ov-file#using-the-script for more information."
  201. );
  202.  
  203. const savedCookies = localStorage.getItem("mrm_cookies");
  204.  
  205. const lastAlertTime = localStorage.getItem("mrm_last_alert");
  206. const ONE_WEEK = 7 * 24 * 60 * 60 * 1000;
  207.  
  208. if (!savedCookies) {
  209. const currentTime = Date.now();
  210. if (!lastAlertTime || currentTime - parseInt(lastAlertTime) > ONE_WEEK) {
  211. alert(
  212. "Please set the cookies first before downloading images. See https://github.com/NYT92/mrm-downloader/tree/main?tab=readme-ov-file#using-the-script for more information. This alert will not show again until week has passed."
  213. );
  214. localStorage.setItem("mrm_last_alert", currentTime.toString());
  215. }
  216. imageDlBtn.style.display = "none";
  217. videoDlBtn.style.display = "none";
  218. return;
  219. }
  220. const cookiesValue = savedCookies || "";
  221.  
  222. imageDlBtn.addEventListener("click", function () {
  223. imageDlBtn.disabled = true;
  224. imageDlBtn.textContent = "Downloading...";
  225.  
  226. const imageSelectors = [
  227. ".img-myreadingmanga",
  228. ".img-myreadingmanga img",
  229. ".entry-content img",
  230. ".separator img",
  231. "img[decoding='async']",
  232. ];
  233.  
  234. let imageSources = [];
  235.  
  236. for (const selector of imageSelectors) {
  237. const images = document.querySelectorAll(selector);
  238. if (images.length > 0) {
  239. imageSources = Array.from(images)
  240. .map((img) => img.src || img.dataset.src)
  241. .filter(Boolean);
  242. break;
  243. }
  244. }
  245.  
  246. if (imageSources.length === 0) {
  247. alert(
  248. "No images found on this page. Check the console for debugging information."
  249. );
  250. imageDlBtn.disabled = false;
  251. imageDlBtn.textContent = "Download Images (.zip)";
  252. return;
  253. }
  254.  
  255. const pageElement = document.querySelector(".post-page-numbers.current");
  256. const page = pageElement ? pageElement.textContent.trim() : "1";
  257.  
  258. const zip = new JSZip();
  259.  
  260. progressBar.style.display = "block";
  261. progressText.style.display = "block";
  262. progressInner.style.width = "0%";
  263.  
  264. function getExtensionFromMimeType(mimeType) {
  265. const mimeToExt = {
  266. "image/jpeg": "jpg",
  267. "image/png": "png",
  268. "image/gif": "gif",
  269. "image/webp": "webp",
  270. "image/jpg": "jpg",
  271. "text/html": "html",
  272. };
  273. return mimeToExt[mimeType.toLowerCase()];
  274. }
  275.  
  276. function addImageToZip(src, index) {
  277. return new Promise((resolve, reject) => {
  278. progressText.textContent = `Downloading image ${index + 1} of ${
  279. imageSources.length
  280. }...`;
  281.  
  282. GM_xmlhttpRequest({
  283. method: "GET",
  284. url: src,
  285. headers: {
  286. Cookie: cookiesValue,
  287. },
  288. responseType: "arraybuffer",
  289. onload: function (response) {
  290. try {
  291. const arrayBuffer = response.response;
  292. const byteArray = new Uint8Array(arrayBuffer);
  293. let mimeType = "image/jpeg";
  294. try {
  295. const contentTypeMatch = response.responseHeaders.match(
  296. /Content-Type:\s*(\S+)/i
  297. );
  298. if (contentTypeMatch && contentTypeMatch[1]) {
  299. mimeType = contentTypeMatch[1];
  300. }
  301. } catch (headerError) {
  302. console.warn(
  303. `Could not parse Content-Type header for ${src}:`,
  304. headerError
  305. );
  306. }
  307.  
  308. const blob = new Blob([byteArray], { type: mimeType });
  309.  
  310. if (blob.type.includes("text/html")) {
  311. alert(
  312. "The script have detected the Cloudflare is blocking the page. You need to re-enter the cookies info due to Cloudflare resetting the cookies or the cookies is expired. See https://github.com/NYT92/mrm-downloader/tree/main?tab=readme-ov-file#using-the-script for more information."
  313. );
  314. reject(new Error("Invalid cookies"));
  315. window.location.reload();
  316. } else {
  317. const ext = getExtensionFromMimeType(blob.type);
  318. const fileName = `image_${index + 1}.${ext}`;
  319. zip.file(fileName, blob, { binary: true });
  320. console.log(
  321. `Added ${fileName} to ZIP (${blob.size} bytes, type: ${blob.type})`
  322. );
  323.  
  324. const progress = ((index + 1) / imageSources.length) * 100;
  325. progressInner.style.width = `${progress}%`;
  326. resolve();
  327. }
  328. } catch (error) {
  329. console.error(`Error processing ${src}:`, error);
  330. reject(error);
  331. }
  332. },
  333. onerror: function (error) {
  334. console.error(`Error fetching ${src}:`, error);
  335. reject(error);
  336. },
  337. });
  338. });
  339. }
  340.  
  341. Promise.all(imageSources.map(addImageToZip))
  342. .then(() => {
  343. progressText.textContent = "Creating ZIP file...";
  344. return zip.generateAsync({ type: "blob" });
  345. })
  346. .then(function (content) {
  347. const safeTitle = title.replace(/[^a-z0-9]/gi, "_").toLowerCase();
  348. const fileName = `${safeTitle}_ch${page}.zip`;
  349. saveAs(content, fileName);
  350. console.log("ZIP file saved successfully");
  351. progressBar.style.display = "none";
  352. progressText.style.display = "none";
  353. imageDlBtn.disabled = false;
  354. imageDlBtn.textContent = "Download Images (.zip)";
  355. })
  356. .catch((error) => {
  357. console.error("Error creating ZIP file:", error);
  358. alert(
  359. "An error occurred while creating the ZIP file. Please check the console for details."
  360. );
  361. progressBar.style.display = "none";
  362. progressText.style.display = "none";
  363. imageDlBtn.disabled = false;
  364. imageDlBtn.textContent = "Download Images (.zip)";
  365. });
  366. });
  367.  
  368. videoDlBtn.addEventListener("click", function () {
  369. videoDlBtn.disabled = true;
  370. videoDlBtn.textContent = "Downloading...";
  371.  
  372. const videoElement = document.querySelector("#MRM_video > video > source");
  373. if (!videoElement) {
  374. alert("No video found on this page.");
  375. videoDlBtn.disabled = false;
  376. videoDlBtn.textContent = "Download Video";
  377. return;
  378. }
  379.  
  380. const videoSrc = videoElement.src;
  381. if (!videoSrc) {
  382. alert("Unable to find video source.");
  383. videoDlBtn.disabled = false;
  384. videoDlBtn.textContent = "Download Video";
  385. return;
  386. }
  387.  
  388. progressBar.style.display = "block";
  389. progressText.style.display = "block";
  390. progressText.textContent = "Starting video download...";
  391. progressInner.style.width = "0%";
  392.  
  393. GM_xmlhttpRequest({
  394. method: "GET",
  395. url: videoSrc,
  396. headers: {
  397. Cookie: cookiesValue,
  398. },
  399. responseType: "arraybuffer",
  400. onprogress: function (progress) {
  401. if (progress.lengthComputable) {
  402. const percentComplete = (progress.loaded / progress.total) * 100;
  403. progressInner.style.width = percentComplete + "%";
  404. const downloadedMB = (progress.loaded / (1024 * 1024)).toFixed(2);
  405. const totalMB = (progress.total / (1024 * 1024)).toFixed(2);
  406. console.log(`Downloaded: ${downloadedMB}MB / ${totalMB}MB`);
  407. progressText.textContent = `Downloaded: ${downloadedMB}MB / ${totalMB}MB`;
  408. }
  409. },
  410. onload: function (response) {
  411. const blob = new Blob([response.response], { type: "video/mp4" });
  412. if (new Blob([response.response]).type.includes("text/html")) {
  413. alert(
  414. "The script have detected the Cloudflare is blocking the page. You need to re-enter the cookies info due to Cloudflare resetting the cookies or the cookies is expired. See https://github.com/NYT92/mrm-downloader/tree/main?tab=readme-ov-file#using-the-script for more information."
  415. );
  416. window.location.reload();
  417. } else {
  418. const safeTitle = title.replace(/[^a-z0-9]/gi, "_").toLowerCase();
  419. const fileName = `${safeTitle}.mp4`;
  420. saveAs(blob, fileName);
  421. console.log("Video downloaded successfully");
  422. progressBar.style.display = "none";
  423. progressText.style.display = "none";
  424. videoDlBtn.disabled = false;
  425. videoDlBtn.textContent = "Download Video";
  426. }
  427. },
  428. onerror: function (error) {
  429. console.error("Error downloading video:", error);
  430. alert(
  431. "An error occurred while downloading the video. Please check the console for details."
  432. );
  433. progressBar.style.display = "none";
  434. progressText.style.display = "none";
  435. videoDlBtn.disabled = false;
  436. videoDlBtn.textContent = "Download Video";
  437. },
  438. });
  439. });
  440. })();