Mananelo/Mangakakalot/Manganato/Manga4life Bookmarks Export

Import and export bookmakrs from Mangakakalot, Manganato, Nataomanga, Nelomanga (id, name and visited number) to a txt or csv file on "Export Bookmarks"/"Import Bookmarks" button click

  1. // ==UserScript==
  2. // @name Mananelo/Mangakakalot/Manganato/Manga4life Bookmarks Export
  3. // @namespace http://smoondev.com/
  4. // @version 3.00
  5. // @description Import and export bookmakrs from Mangakakalot, Manganato, Nataomanga, Nelomanga (id, name and visited number) to a txt or csv file on "Export Bookmarks"/"Import Bookmarks" button click
  6. // @author Shawn Moon
  7. // @match https://*.mangakakalot.gg/bookmark*
  8. // @match https://*.nelomanga.com/bookmark*
  9. // @match https://*.natomanga.com/bookmark*
  10. // @match https://*.manganato.gg/bookmark*
  11. // @match https://mangakakalot.fun/user/bookmarks
  12. // @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.8/FileSaver.min.js
  13. // ==/UserScript==
  14.  
  15. (function () {
  16. "use strict";
  17.  
  18. // Inject CSS styles for bookmark export/import UI
  19. function addBookarkStyles(css) {
  20. const head = document.head || document.getElementsByTagName("head")[0];
  21. if (!head) return;
  22. const style = document.createElement("style");
  23. style.type = "text/css";
  24. style.innerHTML = css;
  25. head.appendChild(style);
  26. }
  27.  
  28. addBookarkStyles(`
  29. #export_container_nato, #export_container_kakalot, #export_container_m4l {
  30. color: #000;
  31. cursor: pointer;
  32. float: right;
  33. }
  34.  
  35. #export_container_fun {
  36. display: inline-flex;
  37. vertical-align: bottom;
  38. align-items: baseline;
  39. margin-top: 20px;
  40. text-align: right;
  41. float: right;
  42. margin-right: 20px;
  43. }
  44.  
  45. #export_container_kakalot {
  46. margin-right: 10px;
  47. }
  48.  
  49. #export_nato:hover, #export_kakalot:hover, #import_kakalot:hover, #export_m4l:hover {
  50. background-color: #b6e4e3;
  51. color: #000;
  52. cursor: pointer;
  53. text-shadow: none;
  54. }
  55.  
  56. #export_nato, #export_kakalot, #import_kakalot, #export_m4l {
  57. border-radius: 5px;
  58. text-decoration: none;
  59. color:rgb(255, 255, 255);
  60. text-shadow: 1px 1px #3d7f7d;
  61. background-color: #76cdcb;
  62. border: none;
  63. font-weight: bold;
  64. }
  65.  
  66. #export_nato, #export_kakalot, #import_kakalot {
  67. padding: 4px 8px;
  68. letter-spacing: 0.5px;
  69. }
  70.  
  71. #import_kakalot {
  72. margin-right: 20px;
  73. }
  74.  
  75. #export_m4l {
  76. padding: 1px 12px;
  77. font-size: 16.5px;
  78. }
  79.  
  80. #export_fun {
  81. color: #f05759;
  82. background-color: #fff;
  83. border: 1px solid #f05759;
  84. display: inline-block;
  85. margin-bottom: 0;
  86. font-weight: 400;
  87. text-align: center;
  88. touch-action: manipulation;
  89. cursor: pointer;
  90. white-space: nowrap;
  91. padding: 6px 12px;
  92. border-radius: 0;
  93. user-select: none;
  94. transition: all .2s ease-in-out;
  95. margin-left: 5px;
  96. }
  97.  
  98. #import_fun {
  99. display: none;
  100. }
  101.  
  102. #export_fun:hover {
  103. color: #fff;
  104. background-color: #f05759;
  105. margin-left: 5px;
  106. }
  107.  
  108. #inclURL_nato, #inclURL_kakalot, #inclURL_fun {
  109. margin-left: 10px;
  110. }
  111.  
  112. #inclURL_fun {
  113. margin-right: 5px;
  114. }
  115.  
  116. .inclURL_kakalot {
  117. color: #ffffff;
  118. margin-top: 0;
  119. font-size: 14px;
  120. margin-bottom: 0;
  121. }
  122.  
  123. .inclURL_fun {
  124. font-weight:normal;
  125. }
  126.  
  127. #temp_data {
  128. position: absolute;
  129. top: -9999px;
  130. left: -9999px;
  131. }
  132.  
  133. .bm-container {
  134. background-color: #218f8c;
  135. border-top-left-radius: 7px;
  136. border-top: solid #288b89 1px;
  137. border-left: solid #288b89 1px;
  138. padding-left: 10px;
  139. }
  140. `);
  141.  
  142. // Global constants and variables
  143. const MAX_RETRIES = 10;
  144. const CONCURRENCY_LIMIT = 5;
  145. const DELAY_TIMING = 1000;
  146.  
  147. // Setup selectors and IDs based on domain
  148. let pageI,
  149. bmTag,
  150. bmTitle,
  151. lastViewed,
  152. btnContainer,
  153. exportButtonID,
  154. importButtonID,
  155. inclURL,
  156. bookmarkedTitles = "",
  157. exportContainer,
  158. pageCount = 0,
  159. removeBtn,
  160. bmContainer,
  161. domain = window.location.hostname,
  162. tld = domain.replace("www.", ""),
  163. bmLabel = "Bookmarks";
  164.  
  165. let mangagakalotDomains = ["mangakakalot.gg", "nelomanga.com", "natomanga.com", "manganato.gg"];
  166.  
  167. if (mangagakalotDomains.includes(tld)) {
  168. pageI = ".group-page a";
  169. bmTag = ".user-bookmark-item-right";
  170. bmTitle = ".bm-title";
  171. lastViewed = "span:nth-of-type(2) a";
  172. btnContainer = ".breadcrumbs p";
  173. inclURL = "inclURL_kakalot";
  174. removeBtn = ".btn-remove-bookmark";
  175. bmContainer = "bm-container";
  176.  
  177. let pageElList = document.querySelectorAll(pageI);
  178. if (pageElList.length > 0) {
  179. let lastText = pageElList[pageElList.length - 1].textContent;
  180. pageCount = parseInt(lastText.replace(/\D+/g, ""), 10) || 0;
  181. }
  182. exportButtonID = "export_kakalot";
  183. exportContainer = "export_container_kakalot";
  184. importButtonID = "import_kakalot";
  185. } else if (domain.indexOf("mangakakalot.fun") !== -1) {
  186. bmTag = ".list-group-item";
  187. bmTitle = ".media-heading a";
  188. lastViewed = ".media-body p a";
  189. btnContainer = ".container-fluid:first-child div:last-child";
  190. inclURL = "inclURL_fun";
  191. exportButtonID = "export_fun";
  192. importButtonID = "import_fun";
  193. exportContainer = "export_container_fun";
  194. bmContainer = "";
  195. }
  196.  
  197. // Delay utility function
  198. const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
  199.  
  200. // Save data to file with FileSaver.js
  201. const saveFile = (saveData, format) => {
  202. const ext = format === "csv" ? "csv" : "txt";
  203. const fileData = new Blob([saveData], { type: "application/octet-stream" });
  204. saveAs(fileData, `${tld}_bookmarks.${ext}`);
  205. const btn = document.getElementById(exportButtonID);
  206. if (btn) {
  207. btn.innerHTML = `Export ${bmLabel}`;
  208. btn.disabled = false;
  209. }
  210. };
  211.  
  212. // Remove temporary data container from DOM
  213. const deleteTemp = () => {
  214. const tempData = document.getElementById("temp_data");
  215. if (tempData) {
  216. tempData.remove();
  217. }
  218. };
  219.  
  220. // Generate export content for mangakakalot.fun bookmarks
  221. const getFunBMs = (url, format) => {
  222. const bmItems = document.querySelectorAll(bmTag);
  223. for (let i = 0; i < bmItems.length; i++) {
  224. const titleElem = bmItems[i].querySelector(bmTitle);
  225. const title = titleElem ? titleElem.textContent.trim() : "";
  226. const lastViewedElem = bmItems[i].querySelector(".media-body p a");
  227. const viewedText = lastViewedElem ? lastViewedElem.textContent.trim() : "None";
  228. if (format === "csv") {
  229. const csvTitle = `"${title.replace(/"/g, '""')}"`;
  230. const csvViewed = `"${viewedText.replace(/"/g, '""')}"`;
  231. let csvURL = "";
  232. if (url && lastViewedElem && lastViewedElem.href) {
  233. csvURL = `"${lastViewedElem.href.replace(/"/g, '""')}"`;
  234. }
  235. bookmarkedTitles += `${csvTitle},${csvViewed},${csvURL}\n`;
  236. } else {
  237. const linkText = url && lastViewedElem && lastViewedElem.href ? `- ${lastViewedElem.href}` : "";
  238. bookmarkedTitles += `${title} || Viewed: ${viewedText} ${linkText} \n`;
  239. }
  240. }
  241. saveFile(bookmarkedTitles, format);
  242. };
  243.  
  244. // Insert export/import UI container if not present
  245. if (!document.getElementById(exportContainer)) {
  246. const btnContElem = document.querySelector(btnContainer);
  247. if (btnContElem) {
  248. btnContElem.insertAdjacentHTML(
  249. "beforeend",
  250. `<div class='${bmContainer}'>
  251. <div id='${exportContainer}'>
  252. <button id='${importButtonID}'>Import ${bmLabel}</button>
  253. <select id="export_option" style="border-radius: 5px; padding: 4px 8px;">
  254. <option value="csv">CSV</option>
  255. <option value="text">Text</option>
  256. </select>
  257. <button id='${exportButtonID}'>Export ${bmLabel}</button>
  258. <input type="checkbox" id="${inclURL}">
  259. <span>
  260. <label for="${inclURL}" class='${inclURL}'>Add URL</label>
  261. </span>
  262. </div>
  263. </div>`
  264. );
  265. }
  266. }
  267.  
  268. // Generate export content for mangakakalot (and like) bookmarks
  269. const getBookmarks = (url, bookmarkHeader, format) => {
  270. deleteTemp();
  271. document.body.insertAdjacentHTML("beforeend", "<div id='temp_data'></div>");
  272. let bookmarkedContent = bookmarkHeader;
  273. const tempData = document.getElementById("temp_data");
  274. const fetches = [];
  275.  
  276. for (let i = 0; i < pageCount; i++) {
  277. const pageId = `page${i + 1}`;
  278. const pageDiv = document.createElement("div");
  279. pageDiv.id = pageId;
  280. tempData.appendChild(pageDiv);
  281. const fetchPromise = retryFetch(`https://${domain}/bookmark?page=${i + 1}`)
  282. .then((response) => response.text())
  283. .then((htmlText) => {
  284. const parser = new DOMParser();
  285. const doc = parser.parseFromString(htmlText, "text/html");
  286. const items = doc.querySelectorAll(bmTag);
  287. pageDiv.innerHTML = Array.from(items)
  288. .map((item) => item.outerHTML)
  289. .join("");
  290. })
  291. .catch((error) => console.error("ExportError", error));
  292. fetches.push(fetchPromise);
  293. }
  294.  
  295. Promise.all(fetches).then(() => {
  296. const bmItems = document.querySelectorAll(`#temp_data ${bmTag}`);
  297. bmItems.forEach((item) => {
  298. const titleElem = item.querySelector(bmTitle);
  299.  
  300. if (titleElem && titleElem.textContent) {
  301. const titleText = titleElem.textContent.trim();
  302. const lastViewedElem = item.querySelector(lastViewed);
  303. const viewedText = lastViewedElem ? lastViewedElem.textContent.trim() : "Not Found";
  304. const mID = item.querySelector(removeBtn)?.getAttribute("data-url")?.match(/\d+/)?.[0] || "";
  305.  
  306. if (format === "csv") {
  307. const csvTitle = `"${titleText.replace(/"/g, '""')}"`;
  308. const csvViewed = `"${viewedText.replace(/"/g, '""')}"`;
  309. let csvURL = "";
  310. if (url && lastViewedElem && lastViewedElem.href) {
  311. csvURL = `"${lastViewedElem.href.replace(/"/g, '""')}"`;
  312. }
  313. bookmarkedContent += `${mID},${csvTitle},${csvViewed},${csvURL}\n`;
  314. } else {
  315. const linkPart = url && lastViewedElem && lastViewedElem.href ? `- ${lastViewedElem.href}` : "";
  316. bookmarkedContent += `${titleText} || Viewed: ${viewedText} ${linkPart} \n`;
  317. }
  318. }
  319. });
  320. saveFile(bookmarkedContent, format);
  321. deleteTemp();
  322. });
  323. };
  324.  
  325. // Retrieve existing bookmark IDs to avoid duplicates during import for mangakakalot (and like) sites
  326. const getExistingIDs = async () => {
  327. const ids = new Set();
  328. if (pageCount > 1) {
  329. const promises = [];
  330. for (let i = 1; i <= pageCount; i++) {
  331. const pageUrl = `https://${domain}/bookmark?page=${i}`;
  332. const promise = fetch(pageUrl)
  333. .then((response) => response.text())
  334. .then((htmlText) => {
  335. const parser = new DOMParser();
  336. const doc = parser.parseFromString(htmlText, "text/html");
  337. const items = doc.querySelectorAll(bmTag);
  338. items.forEach((item) => {
  339. const mID = item.querySelector(removeBtn)?.getAttribute("data-url")?.match(/\d+/)?.[0];
  340. if (mID) {
  341. ids.add(mID);
  342. }
  343. });
  344. })
  345. .catch((error) => console.error("Error fetching page:", error));
  346. promises.push(promise);
  347. }
  348. await Promise.all(promises);
  349. } else {
  350. document.querySelectorAll(bmTag).forEach((item) => {
  351. const mID = item.querySelector(removeBtn)?.getAttribute("data-url")?.match(/\d+/)?.[0];
  352. if (mID) {
  353. ids.add(mID);
  354. }
  355. });
  356. }
  357. return ids;
  358. };
  359.  
  360. // Fetch with timeout using AbortController
  361. const fetchWithTimeout = async (url, options = {}, timeout = 2000) => {
  362. const controller = new AbortController();
  363. const id = setTimeout(() => controller.abort(), timeout);
  364. try {
  365. const response = await fetch(url, {
  366. ...options,
  367. signal: controller.signal
  368. });
  369. return response;
  370. } finally {
  371. clearTimeout(id);
  372. }
  373. };
  374.  
  375. // Retry fetch upon failure up to MAX_RETRIES times
  376. const retryFetch = async (url, options = {}, retries = MAX_RETRIES) => {
  377. for (let attempt = 0; attempt < retries; attempt++) {
  378. try {
  379. const response = await fetchWithTimeout(url, options, 2000); // timeout of 2 seconds
  380. if (!response.ok) {
  381. throw new Error(`Status: ${response.status}`);
  382. }
  383. return response;
  384. } catch (err) {
  385. if (attempt === retries - 1) {
  386. throw err;
  387. }
  388. await delay(DELAY_TIMING);
  389. }
  390. }
  391. };
  392.  
  393. // Import bookmarks from CSV file
  394. const importButton = document.getElementById(importButtonID);
  395. if (importButton) {
  396. importButton.addEventListener("click", () => {
  397. const input = document.createElement("input");
  398. input.type = "file";
  399. input.accept = ".csv";
  400. input.style.display = "none";
  401. document.body.appendChild(input);
  402. input.addEventListener("change", async (event) => {
  403. const file = event.target.files[0];
  404. if (file) {
  405. const reader = new FileReader();
  406. reader.onload = async function (e) {
  407. try {
  408. const contents = e.target.result;
  409. const lines = contents.split("\n").filter((line) => line.trim() !== "");
  410. if (lines.length === 0) {
  411. alert("CSV file is empty or invalid.");
  412. return;
  413. }
  414.  
  415. const headerValues = lines[0].split(",").map((item) =>
  416. item
  417. .trim()
  418. .replace(/^"(.*)"$/, "$1")
  419. .toLowerCase()
  420. );
  421.  
  422. if (headerValues[0] !== "id") {
  423. alert("CSV file is corrupt or invalid: Missing 'ID' header in first column.");
  424. return;
  425. }
  426.  
  427. const rawExistingIDs = await getExistingIDs();
  428. const existingIDs = new Set(Array.from(rawExistingIDs).map((id) => String(parseInt(id, 10))));
  429. const tasks = [];
  430.  
  431. for (let index = 1; index < lines.length; index++) {
  432. const values = lines[index].split(",");
  433. if (values.length === 0) continue;
  434.  
  435. let rawId = values[0].trim();
  436. let idStr = rawId.replace(/^"(.*)"$/, "$1").trim();
  437. if (!idStr || isNaN(idStr)) {
  438. alert(`CSV file is corrupt or invalid at line ${index + 1}: '${rawId}' is not a valid number.`);
  439. return;
  440. }
  441.  
  442. const normalizedCSVId = String(parseInt(idStr, 10));
  443.  
  444. if (existingIDs.has(normalizedCSVId)) {
  445. continue;
  446. }
  447.  
  448. tasks.push(async () => {
  449. const url = `https://${domain}/action/bookmark/${normalizedCSVId}?action=add`;
  450. try {
  451. const response = await retryFetch(url);
  452. } catch (error) {
  453. console.error(`Line ${index + 1}: Error with id ${normalizedCSVId} after ${MAX_RETRIES} retries.`, error);
  454. }
  455. completed++;
  456. importButton.innerHTML = `Importing (${completed} of ${tasks.length})`;
  457. await delay(DELAY_TIMING);
  458. });
  459. }
  460.  
  461. const total = tasks.length;
  462. if (total === 0) {
  463. alert("No ew bookmarks to import.");
  464. return;
  465. }
  466. let completed = 0;
  467.  
  468. const runTasks = async (tasks, limit) => {
  469. let index = 0;
  470. const runners = [];
  471. const next = async () => {
  472. if (index >= tasks.length) return;
  473. const currentTask = tasks[index++];
  474. await currentTask();
  475. await next();
  476. };
  477. for (let i = 0; i < limit; i++) {
  478. runners.push(next());
  479. }
  480. await Promise.all(runners);
  481. };
  482.  
  483. await runTasks(tasks, CONCURRENCY_LIMIT);
  484. } finally {
  485. location.reload();
  486. }
  487. };
  488. reader.readAsText(file);
  489. }
  490. document.body.removeChild(input);
  491. });
  492. input.click();
  493. });
  494. }
  495.  
  496. // Export bookmarks based on selected format and domain
  497. const exportButton = document.getElementById(exportButtonID);
  498. if (exportButton) {
  499. exportButton.addEventListener("click", async function () {
  500. const format = document.getElementById("export_option").value;
  501. const inclURLCheck = document.getElementById(inclURL).checked;
  502. let bookmarkHeader = "";
  503. if (format === "csv") {
  504. bookmarkHeader = inclURLCheck ? `"ID","Title","Viewed","URL"\n` : `"ID","Title","Viewed"\n`;
  505. } else {
  506. bookmarkHeader = `===========================\n${domain} ${bmLabel}\n===========================\n`;
  507. }
  508. bookmarkedTitles = bookmarkHeader;
  509.  
  510. if (mangagakalotDomains.includes(tld)) {
  511. exportButton.innerHTML = "Generating File...";
  512. exportButton.disabled = true;
  513. getBookmarks(inclURLCheck, bookmarkedTitles, format);
  514. } else if (domain === "mangakakalot.fun") {
  515. getFunBMs(inclURLCheck, format);
  516. }
  517. });
  518. }
  519. })();