- // ==UserScript==
- // @name Mananelo/Mangakakalot/Manganato/Manga4life Bookmarks Export
- // @namespace http://smoondev.com/
- // @version 3.00
- // @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
- // @author Shawn Moon
- // @match https://*.mangakakalot.gg/bookmark*
- // @match https://*.nelomanga.com/bookmark*
- // @match https://*.natomanga.com/bookmark*
- // @match https://*.manganato.gg/bookmark*
- // @match https://mangakakalot.fun/user/bookmarks
- // @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.8/FileSaver.min.js
- // ==/UserScript==
-
- (function () {
- "use strict";
-
- // Inject CSS styles for bookmark export/import UI
- function addBookarkStyles(css) {
- const head = document.head || document.getElementsByTagName("head")[0];
- if (!head) return;
- const style = document.createElement("style");
- style.type = "text/css";
- style.innerHTML = css;
- head.appendChild(style);
- }
-
- addBookarkStyles(`
- #export_container_nato, #export_container_kakalot, #export_container_m4l {
- color: #000;
- cursor: pointer;
- float: right;
- }
-
- #export_container_fun {
- display: inline-flex;
- vertical-align: bottom;
- align-items: baseline;
- margin-top: 20px;
- text-align: right;
- float: right;
- margin-right: 20px;
- }
-
- #export_container_kakalot {
- margin-right: 10px;
- }
-
- #export_nato:hover, #export_kakalot:hover, #import_kakalot:hover, #export_m4l:hover {
- background-color: #b6e4e3;
- color: #000;
- cursor: pointer;
- text-shadow: none;
- }
-
- #export_nato, #export_kakalot, #import_kakalot, #export_m4l {
- border-radius: 5px;
- text-decoration: none;
- color:rgb(255, 255, 255);
- text-shadow: 1px 1px #3d7f7d;
- background-color: #76cdcb;
- border: none;
- font-weight: bold;
- }
-
- #export_nato, #export_kakalot, #import_kakalot {
- padding: 4px 8px;
- letter-spacing: 0.5px;
- }
-
- #import_kakalot {
- margin-right: 20px;
- }
-
- #export_m4l {
- padding: 1px 12px;
- font-size: 16.5px;
- }
-
- #export_fun {
- color: #f05759;
- background-color: #fff;
- border: 1px solid #f05759;
- display: inline-block;
- margin-bottom: 0;
- font-weight: 400;
- text-align: center;
- touch-action: manipulation;
- cursor: pointer;
- white-space: nowrap;
- padding: 6px 12px;
- border-radius: 0;
- user-select: none;
- transition: all .2s ease-in-out;
- margin-left: 5px;
- }
-
- #import_fun {
- display: none;
- }
-
- #export_fun:hover {
- color: #fff;
- background-color: #f05759;
- margin-left: 5px;
- }
-
- #inclURL_nato, #inclURL_kakalot, #inclURL_fun {
- margin-left: 10px;
- }
-
- #inclURL_fun {
- margin-right: 5px;
- }
-
- .inclURL_kakalot {
- color: #ffffff;
- margin-top: 0;
- font-size: 14px;
- margin-bottom: 0;
- }
-
- .inclURL_fun {
- font-weight:normal;
- }
-
- #temp_data {
- position: absolute;
- top: -9999px;
- left: -9999px;
- }
-
- .bm-container {
- background-color: #218f8c;
- border-top-left-radius: 7px;
- border-top: solid #288b89 1px;
- border-left: solid #288b89 1px;
- padding-left: 10px;
- }
- `);
-
- // Global constants and variables
- const MAX_RETRIES = 10;
- const CONCURRENCY_LIMIT = 5;
- const DELAY_TIMING = 1000;
-
- // Setup selectors and IDs based on domain
- let pageI,
- bmTag,
- bmTitle,
- lastViewed,
- btnContainer,
- exportButtonID,
- importButtonID,
- inclURL,
- bookmarkedTitles = "",
- exportContainer,
- pageCount = 0,
- removeBtn,
- bmContainer,
- domain = window.location.hostname,
- tld = domain.replace("www.", ""),
- bmLabel = "Bookmarks";
-
- let mangagakalotDomains = ["mangakakalot.gg", "nelomanga.com", "natomanga.com", "manganato.gg"];
-
- if (mangagakalotDomains.includes(tld)) {
- pageI = ".group-page a";
- bmTag = ".user-bookmark-item-right";
- bmTitle = ".bm-title";
- lastViewed = "span:nth-of-type(2) a";
- btnContainer = ".breadcrumbs p";
- inclURL = "inclURL_kakalot";
- removeBtn = ".btn-remove-bookmark";
- bmContainer = "bm-container";
-
- let pageElList = document.querySelectorAll(pageI);
- if (pageElList.length > 0) {
- let lastText = pageElList[pageElList.length - 1].textContent;
- pageCount = parseInt(lastText.replace(/\D+/g, ""), 10) || 0;
- }
- exportButtonID = "export_kakalot";
- exportContainer = "export_container_kakalot";
- importButtonID = "import_kakalot";
- } else if (domain.indexOf("mangakakalot.fun") !== -1) {
- bmTag = ".list-group-item";
- bmTitle = ".media-heading a";
- lastViewed = ".media-body p a";
- btnContainer = ".container-fluid:first-child div:last-child";
- inclURL = "inclURL_fun";
- exportButtonID = "export_fun";
- importButtonID = "import_fun";
- exportContainer = "export_container_fun";
- bmContainer = "";
- }
-
- // Delay utility function
- const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
-
- // Save data to file with FileSaver.js
- const saveFile = (saveData, format) => {
- const ext = format === "csv" ? "csv" : "txt";
- const fileData = new Blob([saveData], { type: "application/octet-stream" });
- saveAs(fileData, `${tld}_bookmarks.${ext}`);
- const btn = document.getElementById(exportButtonID);
- if (btn) {
- btn.innerHTML = `Export ${bmLabel}`;
- btn.disabled = false;
- }
- };
-
- // Remove temporary data container from DOM
- const deleteTemp = () => {
- const tempData = document.getElementById("temp_data");
- if (tempData) {
- tempData.remove();
- }
- };
-
- // Generate export content for mangakakalot.fun bookmarks
- const getFunBMs = (url, format) => {
- const bmItems = document.querySelectorAll(bmTag);
- for (let i = 0; i < bmItems.length; i++) {
- const titleElem = bmItems[i].querySelector(bmTitle);
- const title = titleElem ? titleElem.textContent.trim() : "";
- const lastViewedElem = bmItems[i].querySelector(".media-body p a");
- const viewedText = lastViewedElem ? lastViewedElem.textContent.trim() : "None";
- if (format === "csv") {
- const csvTitle = `"${title.replace(/"/g, '""')}"`;
- const csvViewed = `"${viewedText.replace(/"/g, '""')}"`;
- let csvURL = "";
- if (url && lastViewedElem && lastViewedElem.href) {
- csvURL = `"${lastViewedElem.href.replace(/"/g, '""')}"`;
- }
- bookmarkedTitles += `${csvTitle},${csvViewed},${csvURL}\n`;
- } else {
- const linkText = url && lastViewedElem && lastViewedElem.href ? `- ${lastViewedElem.href}` : "";
- bookmarkedTitles += `${title} || Viewed: ${viewedText} ${linkText} \n`;
- }
- }
- saveFile(bookmarkedTitles, format);
- };
-
- // Insert export/import UI container if not present
- if (!document.getElementById(exportContainer)) {
- const btnContElem = document.querySelector(btnContainer);
- if (btnContElem) {
- btnContElem.insertAdjacentHTML(
- "beforeend",
- `<div class='${bmContainer}'>
- <div id='${exportContainer}'>
- <button id='${importButtonID}'>Import ${bmLabel}</button>
- <select id="export_option" style="border-radius: 5px; padding: 4px 8px;">
- <option value="csv">CSV</option>
- <option value="text">Text</option>
- </select>
- <button id='${exportButtonID}'>Export ${bmLabel}</button>
- <input type="checkbox" id="${inclURL}">
- <span>
- <label for="${inclURL}" class='${inclURL}'>Add URL</label>
- </span>
- </div>
- </div>`
- );
- }
- }
-
- // Generate export content for mangakakalot (and like) bookmarks
- const getBookmarks = (url, bookmarkHeader, format) => {
- deleteTemp();
- document.body.insertAdjacentHTML("beforeend", "<div id='temp_data'></div>");
- let bookmarkedContent = bookmarkHeader;
- const tempData = document.getElementById("temp_data");
- const fetches = [];
-
- for (let i = 0; i < pageCount; i++) {
- const pageId = `page${i + 1}`;
- const pageDiv = document.createElement("div");
- pageDiv.id = pageId;
- tempData.appendChild(pageDiv);
- const fetchPromise = retryFetch(`https://${domain}/bookmark?page=${i + 1}`)
- .then((response) => response.text())
- .then((htmlText) => {
- const parser = new DOMParser();
- const doc = parser.parseFromString(htmlText, "text/html");
- const items = doc.querySelectorAll(bmTag);
- pageDiv.innerHTML = Array.from(items)
- .map((item) => item.outerHTML)
- .join("");
- })
- .catch((error) => console.error("ExportError", error));
- fetches.push(fetchPromise);
- }
-
- Promise.all(fetches).then(() => {
- const bmItems = document.querySelectorAll(`#temp_data ${bmTag}`);
- bmItems.forEach((item) => {
- const titleElem = item.querySelector(bmTitle);
-
- if (titleElem && titleElem.textContent) {
- const titleText = titleElem.textContent.trim();
- const lastViewedElem = item.querySelector(lastViewed);
- const viewedText = lastViewedElem ? lastViewedElem.textContent.trim() : "Not Found";
- const mID = item.querySelector(removeBtn)?.getAttribute("data-url")?.match(/\d+/)?.[0] || "";
-
- if (format === "csv") {
- const csvTitle = `"${titleText.replace(/"/g, '""')}"`;
- const csvViewed = `"${viewedText.replace(/"/g, '""')}"`;
- let csvURL = "";
- if (url && lastViewedElem && lastViewedElem.href) {
- csvURL = `"${lastViewedElem.href.replace(/"/g, '""')}"`;
- }
- bookmarkedContent += `${mID},${csvTitle},${csvViewed},${csvURL}\n`;
- } else {
- const linkPart = url && lastViewedElem && lastViewedElem.href ? `- ${lastViewedElem.href}` : "";
- bookmarkedContent += `${titleText} || Viewed: ${viewedText} ${linkPart} \n`;
- }
- }
- });
- saveFile(bookmarkedContent, format);
- deleteTemp();
- });
- };
-
- // Retrieve existing bookmark IDs to avoid duplicates during import for mangakakalot (and like) sites
- const getExistingIDs = async () => {
- const ids = new Set();
- if (pageCount > 1) {
- const promises = [];
- for (let i = 1; i <= pageCount; i++) {
- const pageUrl = `https://${domain}/bookmark?page=${i}`;
- const promise = fetch(pageUrl)
- .then((response) => response.text())
- .then((htmlText) => {
- const parser = new DOMParser();
- const doc = parser.parseFromString(htmlText, "text/html");
- const items = doc.querySelectorAll(bmTag);
- items.forEach((item) => {
- const mID = item.querySelector(removeBtn)?.getAttribute("data-url")?.match(/\d+/)?.[0];
- if (mID) {
- ids.add(mID);
- }
- });
- })
- .catch((error) => console.error("Error fetching page:", error));
- promises.push(promise);
- }
- await Promise.all(promises);
- } else {
- document.querySelectorAll(bmTag).forEach((item) => {
- const mID = item.querySelector(removeBtn)?.getAttribute("data-url")?.match(/\d+/)?.[0];
- if (mID) {
- ids.add(mID);
- }
- });
- }
- return ids;
- };
-
- // Fetch with timeout using AbortController
- const fetchWithTimeout = async (url, options = {}, timeout = 2000) => {
- const controller = new AbortController();
- const id = setTimeout(() => controller.abort(), timeout);
- try {
- const response = await fetch(url, {
- ...options,
- signal: controller.signal
- });
- return response;
- } finally {
- clearTimeout(id);
- }
- };
-
- // Retry fetch upon failure up to MAX_RETRIES times
- const retryFetch = async (url, options = {}, retries = MAX_RETRIES) => {
- for (let attempt = 0; attempt < retries; attempt++) {
- try {
- const response = await fetchWithTimeout(url, options, 2000); // timeout of 2 seconds
- if (!response.ok) {
- throw new Error(`Status: ${response.status}`);
- }
- return response;
- } catch (err) {
- if (attempt === retries - 1) {
- throw err;
- }
- await delay(DELAY_TIMING);
- }
- }
- };
-
- // Import bookmarks from CSV file
- const importButton = document.getElementById(importButtonID);
- if (importButton) {
- importButton.addEventListener("click", () => {
- const input = document.createElement("input");
- input.type = "file";
- input.accept = ".csv";
- input.style.display = "none";
- document.body.appendChild(input);
- input.addEventListener("change", async (event) => {
- const file = event.target.files[0];
- if (file) {
- const reader = new FileReader();
- reader.onload = async function (e) {
- try {
- const contents = e.target.result;
- const lines = contents.split("\n").filter((line) => line.trim() !== "");
- if (lines.length === 0) {
- alert("CSV file is empty or invalid.");
- return;
- }
-
- const headerValues = lines[0].split(",").map((item) =>
- item
- .trim()
- .replace(/^"(.*)"$/, "$1")
- .toLowerCase()
- );
-
- if (headerValues[0] !== "id") {
- alert("CSV file is corrupt or invalid: Missing 'ID' header in first column.");
- return;
- }
-
- const rawExistingIDs = await getExistingIDs();
- const existingIDs = new Set(Array.from(rawExistingIDs).map((id) => String(parseInt(id, 10))));
- const tasks = [];
-
- for (let index = 1; index < lines.length; index++) {
- const values = lines[index].split(",");
- if (values.length === 0) continue;
-
- let rawId = values[0].trim();
- let idStr = rawId.replace(/^"(.*)"$/, "$1").trim();
- if (!idStr || isNaN(idStr)) {
- alert(`CSV file is corrupt or invalid at line ${index + 1}: '${rawId}' is not a valid number.`);
- return;
- }
-
- const normalizedCSVId = String(parseInt(idStr, 10));
-
- if (existingIDs.has(normalizedCSVId)) {
- continue;
- }
-
- tasks.push(async () => {
- const url = `https://${domain}/action/bookmark/${normalizedCSVId}?action=add`;
- try {
- const response = await retryFetch(url);
- } catch (error) {
- console.error(`Line ${index + 1}: Error with id ${normalizedCSVId} after ${MAX_RETRIES} retries.`, error);
- }
- completed++;
- importButton.innerHTML = `Importing (${completed} of ${tasks.length})`;
- await delay(DELAY_TIMING);
- });
- }
-
- const total = tasks.length;
- if (total === 0) {
- alert("No ew bookmarks to import.");
- return;
- }
- let completed = 0;
-
- const runTasks = async (tasks, limit) => {
- let index = 0;
- const runners = [];
- const next = async () => {
- if (index >= tasks.length) return;
- const currentTask = tasks[index++];
- await currentTask();
- await next();
- };
- for (let i = 0; i < limit; i++) {
- runners.push(next());
- }
- await Promise.all(runners);
- };
-
- await runTasks(tasks, CONCURRENCY_LIMIT);
- } finally {
- location.reload();
- }
- };
- reader.readAsText(file);
- }
- document.body.removeChild(input);
- });
- input.click();
- });
- }
-
- // Export bookmarks based on selected format and domain
- const exportButton = document.getElementById(exportButtonID);
- if (exportButton) {
- exportButton.addEventListener("click", async function () {
- const format = document.getElementById("export_option").value;
- const inclURLCheck = document.getElementById(inclURL).checked;
- let bookmarkHeader = "";
- if (format === "csv") {
- bookmarkHeader = inclURLCheck ? `"ID","Title","Viewed","URL"\n` : `"ID","Title","Viewed"\n`;
- } else {
- bookmarkHeader = `===========================\n${domain} ${bmLabel}\n===========================\n`;
- }
- bookmarkedTitles = bookmarkHeader;
-
- if (mangagakalotDomains.includes(tld)) {
- exportButton.innerHTML = "Generating File...";
- exportButton.disabled = true;
- getBookmarks(inclURLCheck, bookmarkedTitles, format);
- } else if (domain === "mangakakalot.fun") {
- getFunBMs(inclURLCheck, format);
- }
- });
- }
- })();