MRM Downloader

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

目前为 2025-01-27 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name MRM Downloader
  3. // @namespace https://nyt92.eu.org
  4. // @version 2025-1-27
  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/cats/*
  10. // @exclude https://myreadingmanga.info/pairing/*
  11. // @exclude https://myreadingmanga.info/group/*
  12. // @exclude https://myreadingmanga.info/privacy-policy/
  13. // @exclude https://myreadingmanga.info/dmca-notice/
  14. // @exclude https://myreadingmanga.info/contact/
  15. // @exclude https://myreadingmanga.info/terms-service/
  16. // @exclude https://myreadingmanga.info/sitemap/
  17. // @exclude https://myreadingmanga.info/my-bookmark/
  18. // @exclude https://myreadingmanga.info/tag/*
  19. // @exclude https://myreadingmanga.info/genre/*
  20. // @exclude https://myreadingmanga.info/status/*
  21. // @exclude https://myreadingmanga.info/lang/*
  22. // @exclude https://myreadingmanga.info/yaoi-manga/*
  23. // @exclude https://myreadingmanga.info/manhwa/*
  24. // @connect myreadingmanga.info
  25. // @connect i1.myreadingmanga.info
  26. // @connect i2.myreadingmanga.info
  27. // @connect i3.myreadingmanga.info
  28. // @connect i4.myreadingmanga.info
  29. // @connect i5.myreadingmanga.info
  30. // @connect i6.myreadingmanga.info
  31. // @supportURL https://github.com/NYT92/mrm-downloader
  32. // @icon https://www.google.com/s2/favicons?sz=64&domain=myreadingmanga.info
  33. // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js
  34. // @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js
  35. // @grant GM_xmlhttpRequest
  36. // @license GPLv3
  37. // ==/UserScript==
  38.  
  39. // THIS USERSCRIPT IS LICENSE UNDER THE GPLv3 License
  40. (function () {
  41. ("use strict");
  42.  
  43. function saveCookies() {
  44. const cookies = prompt("Please paste your cookies here:");
  45. if (cookies) {
  46. localStorage.setItem("mrm_cookies", cookies);
  47. alert("Cookies saved!");
  48. window.location.reload();
  49. }
  50. }
  51.  
  52. const cookiesBtn = document.createElement("button");
  53. cookiesBtn.id = "saveCookiesBtn";
  54. cookiesBtn.textContent = "Load 🍪";
  55. cookiesBtn.style.cssText =
  56. "position: fixed; bottom: 10px; right: 10px; z-index: 9999;";
  57. document.body.appendChild(cookiesBtn);
  58.  
  59. cookiesBtn.addEventListener("click", saveCookies);
  60.  
  61. const title =
  62. document
  63. .querySelector(".entry-header h1.entry-title")
  64. ?.textContent.trim() || "Untitled";
  65.  
  66. const imageDlBtn = document.createElement("button");
  67. imageDlBtn.id = "downloadImagesBtn";
  68. imageDlBtn.textContent = "Download Images (.zip)";
  69. imageDlBtn.style.cssText =
  70. "position: fixed; top: 10px; right: 10px; z-index: 9999;";
  71.  
  72. const videoDlBtn = document.createElement("button");
  73. videoDlBtn.id = "downloadVideoBtn";
  74. videoDlBtn.textContent = "Download Video";
  75. videoDlBtn.style.cssText =
  76. "position: fixed; top: 10px; right: 10px; z-index: 9999;";
  77.  
  78. const progressBar = document.createElement("div");
  79. progressBar.id = "downloadProgress";
  80. progressBar.style.cssText =
  81. "position: fixed; top:120px; right: 10px; width: 235px; right: 10px; height: 20px; background-color: #f0f0f0; display: none; z-index: 9999;";
  82.  
  83. const progressInner = document.createElement("div");
  84. progressInner.style.cssText =
  85. "width: 0%; height: 100%; background-color: #4CAF50; transition: width 0.5s;";
  86. progressBar.appendChild(progressInner);
  87.  
  88. const progressText = document.createElement("div");
  89. progressText.style.cssText =
  90. "position: fixed; top: 145px; right: 10px; z-index: 9999; display: none;";
  91. progressText.textContent = "Preparing download...";
  92.  
  93. const checkIfVid = document.querySelectorAll(".entry-categories a");
  94.  
  95. if (!checkIfVid) {
  96. console.log("no media found!");
  97. } else if (document.querySelector("#MRM_video") !== null) {
  98. document.body.appendChild(videoDlBtn);
  99. } else if (document.querySelector(".img-myreadingmanga") !== null) {
  100. document.body.appendChild(imageDlBtn);
  101. }
  102. document.body.appendChild(progressBar);
  103. document.body.appendChild(progressText);
  104.  
  105. console.log(
  106. "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."
  107. );
  108.  
  109. const savedCookies = localStorage.getItem("mrm_cookies");
  110.  
  111. const lastAlertTime = localStorage.getItem("mrm_last_alert");
  112. const ONE_WEEK = 7 * 24 * 60 * 60 * 1000;
  113.  
  114. if (!savedCookies) {
  115. const currentTime = Date.now();
  116. if (!lastAlertTime || currentTime - parseInt(lastAlertTime) > ONE_WEEK) {
  117. alert(
  118. "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 only shown once per week"
  119. );
  120. localStorage.setItem("mrm_last_alert", currentTime.toString());
  121. }
  122. imageDlBtn.style.display = "none";
  123. videoDlBtn.style.display = "none";
  124. return;
  125. }
  126. const cookiesValue = savedCookies || "";
  127.  
  128. imageDlBtn.addEventListener("click", function () {
  129. imageDlBtn.disabled = true;
  130. imageDlBtn.textContent = "Downloading...";
  131.  
  132. const images = document.querySelectorAll(".img-myreadingmanga");
  133.  
  134. let imageSources = [];
  135.  
  136. imageSources = Array.from(images)
  137. .map((img) => img.dataset.src)
  138. .filter(Boolean);
  139.  
  140. if (imageSources.length === 0) {
  141. imageSources = Array.from(images)
  142. .map((img) => img.src)
  143. .filter(Boolean);
  144. }
  145.  
  146. if (imageSources.length === 0) {
  147. const nestedImages = document.querySelectorAll(".img-myreadingmanga img");
  148. imageSources = Array.from(nestedImages)
  149. .map((img) => img.src || img.dataset.src)
  150. .filter(Boolean);
  151. }
  152.  
  153. if (imageSources.length === 0) {
  154. alert(
  155. "No images found on this page. Check the console for debugging information."
  156. );
  157. imageDlBtn.disabled = false;
  158. imageDlBtn.textContent = "Download Images (.zip)";
  159. return;
  160. }
  161.  
  162. const pageElement = document.querySelector(".post-page-numbers.current");
  163. const page = pageElement ? pageElement.textContent.trim() : "1";
  164.  
  165. const zip = new JSZip();
  166.  
  167. progressBar.style.display = "block";
  168. progressText.style.display = "block";
  169. progressInner.style.width = "0%";
  170.  
  171. function getExtensionFromMimeType(mimeType) {
  172. const mimeToExt = {
  173. "image/jpeg": "jpg",
  174. "image/png": "png",
  175. "image/gif": "gif",
  176. "image/webp": "webp",
  177. "image/jpg": "jpg",
  178. "text/html": "html",
  179. };
  180. return mimeToExt[mimeType.toLowerCase()];
  181. }
  182.  
  183. function addImageToZip(src, index) {
  184. return new Promise((resolve, reject) => {
  185. progressText.textContent = `Downloading image ${index + 1} of ${
  186. imageSources.length
  187. }...`;
  188.  
  189. GM_xmlhttpRequest({
  190. method: "GET",
  191. url: src,
  192. headers: {
  193. Cookie: cookiesValue,
  194. },
  195. responseType: "arraybuffer",
  196. onload: function (response) {
  197. try {
  198. const arrayBuffer = response.response;
  199. const byteArray = new Uint8Array(arrayBuffer);
  200. let mimeType = "image/jpeg";
  201. try {
  202. const contentTypeMatch = response.responseHeaders.match(
  203. /Content-Type:\s*(\S+)/i
  204. );
  205. if (contentTypeMatch && contentTypeMatch[1]) {
  206. mimeType = contentTypeMatch[1];
  207. }
  208. } catch (headerError) {
  209. console.warn(
  210. `Could not parse Content-Type header for ${src}:`,
  211. headerError
  212. );
  213. }
  214.  
  215. const blob = new Blob([byteArray], { type: mimeType });
  216.  
  217. if (blob.type.includes("text/html")) {
  218. alert(
  219. "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."
  220. );
  221. reject(new Error("Invalid cookies"));
  222. window.location.reload();
  223. } else {
  224. const ext = getExtensionFromMimeType(blob.type);
  225. const fileName = `image_${index + 1}.${ext}`;
  226. zip.file(fileName, blob, { binary: true });
  227. console.log(
  228. `Added ${fileName} to ZIP (${blob.size} bytes, type: ${blob.type})`
  229. );
  230.  
  231. const progress = ((index + 1) / imageSources.length) * 100;
  232. progressInner.style.width = `${progress}%`;
  233. resolve();
  234. }
  235. } catch (error) {
  236. console.error(`Error processing ${src}:`, error);
  237. reject(error);
  238. }
  239. },
  240. onerror: function (error) {
  241. console.error(`Error fetching ${src}:`, error);
  242. reject(error);
  243. },
  244. });
  245. });
  246. }
  247.  
  248. Promise.all(imageSources.map(addImageToZip))
  249. .then(() => {
  250. progressText.textContent = "Creating ZIP file...";
  251. return zip.generateAsync({ type: "blob" });
  252. })
  253. .then(function (content) {
  254. const safeTitle = title.replace(/[^a-z0-9]/gi, "_").toLowerCase();
  255. const fileName = `${safeTitle}_ch${page}.zip`;
  256. saveAs(content, fileName);
  257. console.log("ZIP file saved successfully");
  258. progressBar.style.display = "none";
  259. progressText.style.display = "none";
  260. imageDlBtn.disabled = false;
  261. imageDlBtn.textContent = "Download Images (.zip)";
  262. })
  263. .catch((error) => {
  264. console.error("Error creating ZIP file:", error);
  265. alert(
  266. "An error occurred while creating the ZIP file. Please check the console for details."
  267. );
  268. progressBar.style.display = "none";
  269. progressText.style.display = "none";
  270. imageDlBtn.disabled = false;
  271. imageDlBtn.textContent = "Download Images (.zip)";
  272. });
  273. });
  274.  
  275. videoDlBtn.addEventListener("click", function () {
  276. videoDlBtn.disabled = true;
  277. videoDlBtn.textContent = "Downloading...";
  278.  
  279. const videoElement = document.querySelector("#MRM_video > video > source");
  280. if (!videoElement) {
  281. alert("No video found on this page.");
  282. videoDlBtn.disabled = false;
  283. videoDlBtn.textContent = "Download Video";
  284. return;
  285. }
  286.  
  287. const videoSrc = videoElement.src;
  288. if (!videoSrc) {
  289. alert("Unable to find video source.");
  290. videoDlBtn.disabled = false;
  291. videoDlBtn.textContent = "Download Video";
  292. return;
  293. }
  294.  
  295. progressBar.style.display = "block";
  296. progressText.style.display = "block";
  297. progressText.textContent = "Starting video download...";
  298. progressInner.style.width = "0%";
  299.  
  300. GM_xmlhttpRequest({
  301. method: "GET",
  302. url: videoSrc,
  303. headers: {
  304. Cookie: cookiesValue,
  305. },
  306. responseType: "arraybuffer",
  307. onprogress: function (progress) {
  308. if (progress.lengthComputable) {
  309. const percentComplete = (progress.loaded / progress.total) * 100;
  310. progressInner.style.width = percentComplete + "%";
  311. const downloadedMB = (progress.loaded / (1024 * 1024)).toFixed(2);
  312. const totalMB = (progress.total / (1024 * 1024)).toFixed(2);
  313. console.log(`Downloaded: ${downloadedMB}MB / ${totalMB}MB`);
  314. progressText.textContent = `Downloaded: ${downloadedMB}MB / ${totalMB}MB`;
  315. }
  316. },
  317. onload: function (response) {
  318. const blob = new Blob([response.response], { type: "video/mp4" });
  319. if (new Blob([response.response]).type.includes("text/html")) {
  320. alert(
  321. "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."
  322. );
  323. window.location.reload();
  324. } else {
  325. const safeTitle = title.replace(/[^a-z0-9]/gi, "_").toLowerCase();
  326. const fileName = `${safeTitle}.mp4`;
  327. saveAs(blob, fileName);
  328. console.log("Video downloaded successfully");
  329. progressBar.style.display = "none";
  330. progressText.style.display = "none";
  331. videoDlBtn.disabled = false;
  332. videoDlBtn.textContent = "Download Video";
  333. }
  334. },
  335. onerror: function (error) {
  336. console.error("Error downloading video:", error);
  337. alert(
  338. "An error occurred while downloading the video. Please check the console for details."
  339. );
  340. progressBar.style.display = "none";
  341. progressText.style.display = "none";
  342. videoDlBtn.disabled = false;
  343. videoDlBtn.textContent = "Download Video";
  344. },
  345. });
  346. });
  347. })();