Literotica Author Story Collection Downloader

Select stories to download from author page on Literotica - will collect all into simplified HTMLs and download as a .zip (includes support for series), can also sort stories

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Literotica Author Story Collection Downloader
// @namespace    http://tampermonkey.net/
// @version      0.4
// @description  Select stories to download from author page on Literotica - will collect all into simplified HTMLs and download as a .zip (includes support for series), can also sort stories
// @author       DUVish
// @match        https://www.literotica.com/stories/memberpage.php?uid=*&page=submissions
// ==/UserScript==

let totalScripts = 2;
let scriptsLoaded = 0;

let zipScr = document.createElement("script");
zipScr.src = "https://cdn.jsdelivr.net/gh/Stuk/jszip/dist/jszip.min.js";
zipScr.async = false;
zipScr.type = "text/javascript";
zipScr.onload = function() {
  scriptsLoaded++;
  if (scriptsLoaded === totalScripts) scriptFunc();
};
document.querySelector("head").appendChild(zipScr);

let fileScr = document.createElement("script");
fileScr.src = "https://cdn.jsdelivr.net/gh/eligrey/FileSaver.js/src/FileSaver.js";
fileScr.async = false;
fileScr.type = "text/javascript";
fileScr.onload = function() {
  scriptsLoaded++;
  if (scriptsLoaded === totalScripts) scriptFunc();
};
document.querySelector("head").appendChild(fileScr);

const selectorForPageText = ".panel.article";

function scriptFunc() {
    var zip = new JSZip();

    const author = document.querySelector(".contactheader").innerText;

    document.querySelector("head").innerHTML += `
<style>
.customSaveBtn:hover {
background-color: #9bc7ff !important;
}
.customSaveBtn:active {
background-color: #56a0ff !important;
}
.selectAllBtn:hover {
background-color: #9bc7ff !important;
}
.selectAllBtn:active {
background-color: #56a0ff !important;
}
.selectNotSeriesBtn:hover {
background-color: #9bc7ff !important;
}
.selectNotSeriesBtn:active {
background-color: #56a0ff !important;
}
.titleSort, .ratingSort, .categorySort, .dateSort {
background-color: antiquewhite;
margin: 0px 5px;
padding: 5px 11px;
border-radius: 8px;
}
.titleSort:hover, .ratingSort:hover, .categorySort:hover, .dateSort:hover {
background-color: #ffe2ba;
}
.titleSort:active, .ratingSort:active, .categorySort:active, .dateSort:active {
background-color: #ffd499;
}
.sortText {
font-size: 15px;
align-self: center;
}
</style>
`;
    Array.from(document.querySelectorAll(".root-story, .ser-ttl, .sl")).forEach((el, i) => el.dataset.idx = i);

    Array.from($(".root-story")).forEach(el => el.innerHTML += `<div style="min-width: 120px;position: relative;line-height: 40px;left: 10px;">Save Story: <input class="customSave" style="position: relative;top: 2px;" type="checkbox"></div>`);

    Array.from($(".sl")).forEach(el => el.innerHTML += `<div style="min-width: 120px;position: relative;line-height: 40px;left: 10px;">Save Chapter: <input class="customSave" style="position: relative;top: 2px;" type="checkbox"></div>`);

    Array.from($(".ser-ttl")).forEach(el => el.innerHTML += `<div style="min-width: 120px;position: relative;line-height: 40px;left: 10px;">Save Series: <input class="customSaveSeries" style="position: relative;top: 2px;" type="checkbox"></div>`);


    Array.from(document.querySelectorAll("td")).filter(el => el.innerText === "STORY SUBMISSIONS")[0].parentNode.innerHTML += `<div style="display: flex;justify-content: flex-end;">
    <div class="selectAllBtn" style="width: 155px;;top: 0px;right: -160px;border-radius:8px;background-color: #c6dfff;text-align: center;height: 22px;padding-top: 5px;margin-left: 10px">Select All (Series and Stories)</div>
<div class="selectNotSeriesBtn" style="width: 195px;;bottom: 30px;right: -160px;border-radius:8px;background-color: #c6dfff;text-align: center;height: 22px;padding-top: 5px;margin-left: 10px;margin-right: 10px">Select All Chapters/Stories Individually</div>
<div class="customSaveBtn" style="width: 125px;top: 0px;left: 485px;border-radius:8px;background-color: #c6dfff;text-align: center;height: 22px;padding-top: 5px;margin-right: 20px">Save Selected to Zip</div></div>`;

    document.querySelector(".selectAllBtn").addEventListener("click", function(e) {
      Array.from(document.querySelectorAll(".customSave, .customSaveSeries")).filter(el => el.parentNode.innerText.match(/Series|Story/i)).forEach(el => el.click());
    });

    document.querySelector(".selectNotSeriesBtn").addEventListener("click", function(e) {
      Array.from(document.querySelectorAll(".customSave, .customSaveSeries")).filter(el => el.parentNode.innerText.match(/Chapter|Story/i)).forEach(el => el.click());
    });

    document.querySelector(".customSaveBtn").addEventListener("click", function(e) {
        Array.from(document.querySelectorAll(".customSave:checked")).forEach(el => {
            let htmlFileTitle = el.parentNode.parentNode.children[0].querySelector("a").innerText;
            console.log("Now adding story "+ htmlFileTitle);
            let link = el.parentNode.parentNode.children[0].querySelector("a").href;
            let htmlFileContent;
            $.ajax({
                type: "GET",
                url: link,
                async: false,
                success: function(data) {
                    let firstPageText = getTextFromPage(data);
                    let pages = getNumberOfPages(data);
                    if (pages > 1) {
                        let arr = [];
                        let counter = 2;
                        while(counter <= pages) {
                            arr.push(link + `?page=${counter}`);
                            counter++;
                        }
                        //console.log("adding from array", arr);
                        arr.forEach(newLink => {
                            $.ajax({
                                type: "GET",
                                url: newLink,
                                async: false,
                                success: function(data) {
                                    firstPageText += getTextFromPage(data);
                                }
                            });
                        });
                    }
                    htmlFileContent = `
<!DOCTYPE html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>${getTitleFromPage(data)}</title>
</head>
<body>
<div style="text-align:center;font-size: 22px;">
${getTitleFromPage(data)}
</div>
<br>
<div style="text-align:center;font-size: 18px;font-style: italic">
${getDescriptionFromPage(data)}
</div>
<br>
${firstPageText}
</body>
`;
                }
            });
            zip.file(htmlFileTitle + ".html", htmlFileContent);
            console.log("just added one file to zip", htmlFileTitle);
        });
        Array.from(document.querySelectorAll(".customSaveSeries:checked")).forEach(el => {
          let text = el.parentNode.parentNode.children[0].innerText;
          let seriesName = text.split(":")[0];
          let numOfChapters = text.split(" ")[text.split(" ").length - 3];
          let arrOfParts = [];
          let currentPart = el.parentNode.parentNode;
          let seriesCounter = 0;
          let bodyStrWhole = ``;
          while (seriesCounter < numOfChapters) {
            currentPart = currentPart.nextSibling;
            arrOfParts.push(currentPart.children[0].querySelector("a"));
            seriesCounter++;
          }
          //console.log("arr of parts in series", arrOfParts);
          arrOfParts.forEach(part => {
            let link = part.href;
            //console.log("getting one part host link", link);
            let bodyStrPart = ``;
            $.ajax({
              type: "GET",
              url: link,
              async: false,
              success: function(data) {
                    //console.log("host page in one part of series");
                    //console.log($(data));
                    let firstPageText = getTextFromPage(data);
                    let pages = getNumberOfPages(data)
                    if (pages > 1) {
                        let arr = [];
                        let counter = 2;
                        while(counter <= pages) {
                            arr.push(link + `?page=${counter}`);
                            counter++;
                        }
                        //console.log("adding from array series", arr);
                        arr.forEach(newLink => {
                            $.ajax({
                                type: "GET",
                                url: newLink,
                                async: false,
                                success: function(data) {
                                    firstPageText += getTextFromPage(data);
                                }
                            });
                        });
                    }
                  bodyStrWhole += `
<div style="text-align:center;font-size: 22px;">
${getTitleFromPage(data)}
</div>
<br>
<div style="text-align:center;font-size: 18px;font-style: italic">
${getDescriptionFromPage(data)}
</div>
<br>
${firstPageText}
<br>
<br>
                  `;
              }
            });
          });
          let htmlSeriesFile = `
<!DOCTYPE html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <title>${seriesName}</title>
</head>
<body>
${bodyStrWhole}
</body>
          `;
          console.log("about to add series to zip", seriesName);
          zip.file(seriesName + ".html", htmlSeriesFile);
        });
        zip.generateAsync({type:"blob"})
            .then(function (blob) {
            saveAs(blob, `${author}'s Stories`);
        });
    });

  //sorting logic
  Array.from(document.querySelectorAll("tbody"))[6].children[2].innerHTML += `<div class="sortingContainer" style="display: flex; justify-content: space-between;">
    <span class="sortText">Sort by: </span><div class="titleSort sorting">Reset</div><div class="ratingSort sorting">Rating</div><div class="categorySort sorting">Category</div><div class="dateSort sorting">Date</Date>
  </div>`;

  Array.from(document.querySelectorAll(".sorting")).forEach(el => el.addEventListener("click", function(e) {
    let collection = Array.from(document.querySelectorAll(".root-story, .sl"));
    switch(e.target.innerText) {
      case "Reset":
        collection = Array.from(document.querySelectorAll(".root-story, .sl, .ser-ttl"));
        collection.sort((a, b) => Number(a.dataset.idx) - Number(b.dataset.idx));
        break;
      case "Rating":
        collection.sort((a, b) => Number(a.children[0].innerText.slice(-5).slice(0, 4)) - Number(b.children[0].innerText.slice(-5).slice(0, 4)));
        break;
      case "Category":
        let store = {};
        collection.forEach(el => {
          if (store[el.children[2].innerText.trim()]) store[el.children[2].innerText.trim()] = store[el.children[2].innerText.trim()].concat(el);
          else store[el.children[2].innerText.trim()] = [el];
        });
        collection = [];
        Object.keys(store).sort().forEach(key => collection = collection.concat(store[key].sort((a, b) => Number(a.dataset.idx) - Number(b.dataset.idx))));
        break;
      case "Date":
        collection.sort((a, b) => compareDates(b.children[3].innerText, a.children[3].innerText));
        break;
    }
    collection.forEach(el => el.parentNode.appendChild(el));
  }));
}

function getTextFromPage(data) {
  let textStr = "";
  let container = $(data)?.find(selectorForPageText)[0]?.childNodes[0]?.childNodes[0];
  Array.from(container.childNodes).forEach(node => node.innerText && node.innerText.trim().length ? textStr += node.outerHTML : null);
  return textStr;
}

function getNumberOfPages(data) {
  let parsedData = $(data).find(".l_bJ");
  let maxPage = 1;
  for (let key in parsedData) {
    let parsedNumber = Number(parsedData[key]?.innerText);
    if (!isNaN(parsedNumber) && (parsedNumber > maxPage)) {
      maxPage = Number(parsedData[key]?.innerText);
    }
  }
  return maxPage;
}

function getTitleFromPage(data) {
  return $(data)[2]?.innerText || "Story Title";
}

function getDescriptionFromPage(data) {
  let parsedData = $(data);
  let metaTags = parsedData.find("meta");
  for (let key in parsedData) {
    if (!isNaN(Number(key)) && parsedData[key].name === "description") return parsedData[key].content;
  }
  return "Series Description";
}

function compareStrings(str1, str2) {
  let shorterStr = str1.length > str2.length ? str2 : str1;
  for (let i = 0; i < shorterStr.length; i++) {
    if (str1.toLowerCase().charCodeAt(i) > str2.toLowerCase().charCodeAt(i)) return false;
  }
  return true;
}

function compareDates(strDate1, strDate2) {
  let arr1 = strDate1.split("/").map(el => Number(el));
  let arr2 = strDate2.split("/").map(el => Number(el));
  let num1 = (arr1[2] * 10000) + (arr1[0] * 100) + (arr1[0]);
  let num2 = (arr2[2] * 10000) + (arr2[0] * 100) + (arr2[0]);
  return num1 > num2 ? false : true;
}