Single page HTML download or EPUB (switch in option menu) for Literotica with improved readability
// ==UserScript==
// @name Literotica Downloader
// @description Single page HTML download or EPUB (switch in option menu) for Literotica with improved readability
// @namespace literotica_downloader
// @include https://tags.literotica.com/*
// @include https://*.tags.literotica.com/*
// @include https://www.literotica.com/new/stories*
// @include https://literotica.com/c/*
// @include https://**.literotica.com/c/*
// @include https://www.literotica.com/c/*
// @include https://www.literotica.com/authors/**/works/stories*
// @require https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js
// @require https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js
// @version 6.4
// @grant GM_info
// @grant GM_registerMenuCommand
// @grant GM.registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_openInTab
// @grant GM_getValue
// @grant GM.getValue
// @grant GM_setValue
// @grant GM.setValue
// @grant GM_notification
// @grant GM.notification
// @author Improved by a random redditor and @nylonmachete, originally by Patrick Kolodziejczyk and fixed by i23234234
// ==/UserScript==
/* jshint esversion: 8 */
// Those valuse can be modifyed by the icon GreaseMonkey > User script commands > Toggle ...
let GMsetValue, GMgetValue, GMregisterMenuCommand, GMunregisterMenuCommand, GMnotification, GMopenInTab;
var options = {
'isToEPUB': false,
'isNightMode': true,
'isNumberChapterInFilename': true,
'isUsernameInFilename': true,
'isDescriptionInFilename': false,
'isNoteInFilename': false,
'isCategoryInFilename': false,
'isBookmark': false,
'fontsize': "2.2"
};
// Creating style for a download icon
var downloadTooltip = 'Download as html';
var bookmarkTooltip = 'Check as read';
var bookmarkCheckTooltip = 'Un-check as read';
var iconDonwload = '<i class="bi bi-arrow-down-circle" style="cursor: pointer;cursor: hand;margin-right:5px; font-size:14px"></i>';
var iconBookmark = '<i class="bi bi-bookmark" style="cursor: pointer;cursor: hand;margin-right:5px; font-size:14px"></i>';
var iconBookmarkCheck = '<i class="bi bi-bookmark-check-fill" style="cursor: pointer;cursor: hand;margin-right:5px; font-size:14px"></i>';
var PREFIX_URL_PAGE = 'https://www.literotica.com';
/**
* Style HTML downloaded
*/
var chapterStyle = '_style="line-height: 1.4em;" ';
var descriptionStyle = ' style="line-height: 1.2em;" ';
/*
* Constante for Filename
*/
var SEPARATOR = '_';
var PREFIX_NOTE = 'RATING_';
var POSTFIX_NUMER_CHAPTER = '_Part_Series';
/*
* Constant For logging
*/
var PREFIX_LOG = '[Literotica Downloader]';
function calculateBodyStyle() {
var toReturn = " style=\"";
if (options.isNightMode) {
toReturn += "background-color:#333333; color: #EEEEEE;";
}
toReturn += "font-family: Helvetica,Arial,sans-serif; width: 50%; margin: 0 auto; line-height: 1.5em;";
toReturn += "font-size:" + options.fontsize + "em; padding: 50px 0 50px 0;";
toReturn += "\"";
return toReturn;
}
// Create link elements
const link1 = document.createElement('link');
link1.href = 'https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css';
link1.rel = 'stylesheet';
link1.type = 'text/css';
const link2 = document.createElement('link');
link2.href = 'https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css';
link2.rel = 'stylesheet';
const styleElement = document.createElement('style');
// Set the type attribute
styleElement.type = 'text/css';
// Set the CSS rules
styleElement.textContent = '.ldText{background-color: lightgray;width: 3em;},.ldChapter,.ldSerie {margin-left: auto;} ._works_wrapper_29o2p_1 ._works_item__series_expanded_parts_29o2p_40{margin-left: 0px !important} .lbIconOnStoryCardComponent {float:left}';
// Append link elements to the head
document.head.appendChild(link1);
document.head.appendChild(link2);
document.head.appendChild(styleElement);
console.log("Literotica Downloader start addEventListener");
window.addEventListener("load", function() {
'use strict';
setTimeout(function() {
const GMinfo = GM_info;
const handlerInfo = GMinfo.scriptHandler;
const isGM = Boolean(handlerInfo.toLowerCase() === 'greasemonkey');
var panelOptionDownloader = `
<div id="literoticaDownloaderOption" title="Options Literotica Downloader" >
Please Configure the options :
<fieldset>
<legend>Filename: </legend>
<div>
<input type="checkbox" name="isToEPUB" id="isToEPUB" class="ldCheckbox">
<span>Export to Epub instead of HTML</span>
</div>
</fieldset>
<fieldset>
<legend>Interface: </legend>
<div>
<input type="checkbox" name="isBookmark" id="isBookmark" class="ldCheckbox">
<span>Bookmark (locale storage)</span>
</div>
<div>Please refresh the page for thoses options</div>
</fieldset>
<fieldset>
<legend>Reading: </legend>
<div>
<input type="checkbox" name="isNightMode" id="isNightMode" class="ldCheckbox">
<span>Night Mode</span>
</div>
</fieldset>
<fieldset>
<legend>Filename: </legend>
<div>
<input type="checkbox" name="isNumberChapterInFilename" id="isNumberChapterInFilename" class="ldCheckbox">
<span>Number of chapters in filename (for series)</span>
</div>
<div>
<input type="checkbox" name="isUsernameInFilename" id="isUsernameInFilename" class="ldCheckbox">
<span>Username in filename</span>
</div>
<div>
<input type="checkbox" name="isDescriptionInFilename" id="isDescriptionInFilename" class="ldCheckbox">
<span>Description in filename (Not on series)</span>
</div>
<div>
<input type="checkbox" name="isNoteInFilename" id="isNoteInFilename" class="ldCheckbox">
<span>Rating in filename (Not on series)</span>
</div>
<div>
<input type="checkbox" name="isCategoryInFilename" id="isCategoryInFilename" class="ldCheckbox">
<span>Category in filename (Not on series)</span>
</div>
<div>
<span>FontSize (in em with . for decimal)</span>
<input type="text" name="fontsize" id="fontsize" class="ldText">
</div>
</fieldset>
</div>
`;
document.body.insertAdjacentHTML('beforeend', panelOptionDownloader);
var ldCheckboxes = document.querySelectorAll('.ldCheckbox');
ldCheckboxes.forEach(function(checkbox) {
checkbox.addEventListener('change', async function() {
console.log(this.id);
GMsetValue(this.id, this.checked);
console.log(PREFIX_LOG + 'new value =>' + await GMgetValue(this.id));
options[this.id] = this.checked;
});
});
var ldInputTexts = document.querySelectorAll('.ldText');
ldInputTexts.forEach(function(checkbox) {
checkbox.addEventListener('change', async function() {
console.log(this.id);
GMsetValue(this.id, this.value);
console.log('new value =>' + await GMgetValue(this.id));
options[this.id] = this.value;
console.log(options);
});
});
console.log(PREFIX_LOG + "Start init dialog option");
$("#literoticaDownloaderOption").dialog({
autoOpen: false,
width: 450
});
console.log(PREFIX_LOG + "Snd init dialog option");
if (isGM) {
GMsetValue = GM.setValue;
GMgetValue = GM.getValue;
GMregisterMenuCommand = GM.registerMenuCommand;
GMunregisterMenuCommand = function() {};
GMnotification = GM.notification;
console.log(PREFIX_LOG + "It's GM !");
} else {
console.log(PREFIX_LOG + "Other than GM...");
GMsetValue = GM_setValue;
GMgetValue = GM_getValue;
GMregisterMenuCommand = GM_registerMenuCommand;
GMunregisterMenuCommand = GM_unregisterMenuCommand;
}
initOptions();
// Open dialog when the menu command is clicked
GMregisterMenuCommand("Literotica Downloader Options", function() {
$("#literoticaDownloaderOption").dialog("open");
});
// Dialog setup
var dialog = document.getElementById('literoticaDownloaderOption');
dialog.style.display = 'none'; // Initially hide the dialog
/*
* Download Button for Chapter
*/
addDownloadChapterButtonTo(document);
/*
* Download Button for Series
*/
addDownloadSeriesButtonTo(document);
/*
* Download Button for individual Story
*/
addDownloadIndividualStoryButtonTo(document);
/*
* Section download on tags
*/
document.addEventListener('DOMNodeInserted', function(e) {
if (e.target != null && e.target.classList != null && e.target.classList.contains('ai_gJ')) {
var target = e.target;
var ai_iGElements = target.querySelectorAll('.ai_iG');
if (ai_iGElements) {
ai_iGElements.forEach(function(element) {
element.insertAdjacentHTML('afterbegin', '<span class="ldDownloadTag lbIconOnStoryCardComponent">' + iconDonwload + '</span>');
});
target.querySelectorAll('.ldDownloadTag.lbIconOnStoryCardComponent')
.forEach(function(tag) {
tag.addEventListener('click', function() {
getABookForStoryOnStoryCardComponent(tag.parentNode.parentNode);
});
});
}
}
});
/*
* Section download on category
*/
console.log(PREFIX_LOG + "init category");
document.querySelectorAll('.b-slb-item').forEach(function(element) {
element.insertAdjacentHTML('afterbegin', '<span class="ldChapter lbIconOnStoryCardComponent">' + iconDonwload + '</span>');
});
document.querySelectorAll(".ldChapter.lbIconOnStoryCardComponent").forEach(function(tag) {
tag.addEventListener('click', function(tag) {
getABookForStoryItemCategory(tag.target.parentNode.parentNode);
});
});
}, 250); // 1000 milliseconds = 1 second
});
function addDownloadSeriesButtonTo(parentElement) {
const titlesSeries = parentElement.querySelectorAll("[class*='_series_head_expanded_'] [class*='_title_'] [class*='_title_link_']");
console.log("addDownloadSeriesButtonTo");
console.log(titlesSeries);
titlesSeries.forEach(element => {
// Create a new element to prepend
const newElement = document.createElement('div');
newElement.textContent = 'Prepended Element'; // Change this to whatever content you want
var idIcon = Math.floor(Math.random() * 1000);
// Prepend the new element before the selected element
element.insertAdjacentHTML('beforebegin', '<span id="' + idIcon + '" class="ldSerie"> ' + iconDonwload + '</span>');
element.classList.add("lbButtonAdded");
});
parentElement.querySelectorAll('.ldSerie').forEach(element => {
element.addEventListener('click', function() {
getABookForSerieDiv(element.parentNode.parentNode.parentNode)
});
});
}
function addDownloadChapterButtonTo(parentElement) {
const titlesChapter = parentElement.querySelectorAll("[class*='_part_card_'] [class*='_title_'] [class*=' _title_']");
console.log("addDownloadChapterButtonTo");
console.log(titlesChapter);
titlesChapter.forEach(element => {
// Create a new element to prepend
const newElement = document.createElement('div');
newElement.textContent = 'Prepended Element'; // Change this to whatever content you want
var idIcon = Math.floor(Math.random() * 1000);
// Prepend the new element before the selected element
element.insertAdjacentHTML('beforebegin', '<span id="' + idIcon + '" class="ldChapter"> ' + iconDonwload + '</span>');
element.classList.add("lbButtonAdded");
});
parentElement.querySelectorAll('.ldChapter').forEach(element => {
element.addEventListener('click', function() {
getABookForStoryDiv(element.parentNode)
});
});
}
function addDownloadIndividualStoryButtonTo(parentElement) {
const titlesChapter = parentElement.querySelectorAll("[class*='_content_'] [class*='_title_link_']:not(.lbButtonAdded)");
console.log("addDownloadIndividualStoryButtonTo");
console.log(titlesChapter);
titlesChapter.forEach(element => {
// Create a new element to prepend
const newElement = document.createElement('div');
newElement.textContent = 'Prepended Element'; // Change this to whatever content you want
var idIcon = Math.floor(Math.random() * 1000);
// Prepend the new element before the selected element
element.insertAdjacentHTML('beforebegin', '<span id="' + idIcon + '" class="ldChapter"> ' + iconDonwload + '</span>');
element.classList.add("lbButtonAdded");
});
parentElement.querySelectorAll('.ldChapter').forEach(element => {
element.addEventListener('click', function() {
getABookForStoryDiv(element.parentNode)
});
});
}
async function initOptions() {
// Init of all options
for (const element of Object.getOwnPropertyNames(options)) {
await initCheckBox(element);
await initInputText(element);
}
}
async function initCheckBox(element, index, array) {
options[element] = await GMgetValue(element, options[element]);
$("#" + element)[0].checked = options[element];
return true;
}
async function initInputText(element, index, array) {
options[element] = await GMgetValue(element, options[element]);
$("#" + element)[0].value = options[element];
console.log(PREFIX_LOG + element + "-> " + options[element]);
return true;
}
function getABookForStoryItemCategory(cardComponent) {
console.log(PREFIX_LOG + "In getABookForStoryItemCategory");
console.log(cardComponent);
var link = cardComponent.querySelector('h3 a').getAttribute('href');
var storyId = extractStoryId(link);
var jsonStory = getJsonStoryByStoryId(extractStoryId(link));
console.log(jsonStory);
var title = jsonStory.submission.title;
var note = jsonStory.submission.rate_all;
var chapterTitle = jsonStory.submission.title;
var description = jsonStory.submission.description;
var category = jsonStory.submission.category_info.pageUrl;
var author = jsonStory.submission.author.username;
getABookForStory(storyId, title, note, chapterTitle, description, category, author);
}
function getABookForStoryOnStoryCardComponent(cardComponent) {
console.log(PREFIX_LOG + "In getABookForStoryOnStoryCardComponent");
console.log(cardComponent);
var link = cardComponent.querySelector('.ai_ii').getAttribute('href');
var storyId = extractStoryId(link);
var jsonStory = getJsonStoryByStoryId(extractStoryId(link));
var title = jsonStory.submission.title;
var note = jsonStory.submission.rate_all;
var chapterTitle = jsonStory.submission.title;
var description = jsonStory.submission.description;
var category = jsonStory.submission.category_info.pageUrl;
var author = jsonStory.submission.author.username;
getABookForStory(storyId, title, note, chapterTitle, description, category, author);
}
function openFullViewForDownloadSerie() {
var moreExist = false;
document.querySelectorAll("[class*='_show_more']").forEach(element => {
if (element.innerText.trim().startsWith('View Full')) {
moreExist = true;
console.log(PREFIX_LOG + "Opening " + element);
element.click();
}
});
if (moreExist) {
setTimeout(function() {
openFullViewForDownloadSerie();
}, 250);
}
}
function extractSerieId(url) {
// Split the URL by slashes
const parts = url.split('/');
// Get the last part of the array
const lastPart = parts[parts.length - 1];
return lastPart;
}
function getSerieLinkFormDiv(serieDiv) {
return serieDiv.querySelectorAll("[class*=_title_link_]")[0].href;
}
function getABookForSerieDiv(serieDiv) {
console.log(PREFIX_LOG + "Processing series");
console.log(serieDiv);
openFullViewForDownloadSerie();
var link = getSerieLinkFormDiv(serieDiv);
console.log(link)
var serieId = extractSerieId(link);
var jsonSerie = getJsonSerieBySerieId(serieId).data;
console.log(serieDiv.nextElementSibling);
var allChapter = serieDiv.nextElementSibling.querySelectorAll("[class*='_part_card_'] h3[class*='_title_']");
var lastChapter = allChapter[allChapter.length - 1];
console.log(lastChapter);
var linkLastChapter = getChapterLinkFormDiv(lastChapter);
console.log(PREFIX_LOG + "linkLastChapter => " + linkLastChapter);
var storyId = extractStoryId(linkLastChapter);
var jsonLastChapter = getJsonStoryByStoryId(storyId);
if (jsonLastChapter.submission.series.items == null) {
console.log(PREFIX_LOG + "Last Chapter doesn't have series informations");
console.log(PREFIX_LOG + "Falback on previous one");
console.log(allChapter);
lastChapter = allChapter[allChapter.length - 2];
console.log(lastChapter);
linkLastChapter = getChapterLinkFormDiv(lastChapter);
console.log(PREFIX_LOG + "linkLastChapter => " + linkLastChapter);
storyId = extractStoryId(linkLastChapter);
jsonLastChapter = getJsonStoryByStoryId(storyId);
}
if (options.isToEPUB) {
downloadSerieAsEPUBFromJsonAndDiv(jsonSerie, jsonLastChapter, allChapter);
}else {
downloadSerieAsHTMLFromJsonAndDiv(jsonLastChapter, allChapter);
}
}
function downloadSerieAsHTMLFromJsonAndDiv(jsonLastChapter, allChapter) {
var title = jsonLastChapter.submission.series.meta.title;
// Get the X Part Series
var descriptionSeries = ""
var author = jsonLastChapter.submission.author.username;
var numberChapter = jsonLastChapter.submission.series.meta.order.length;
alert("Starting building file for " + title + " of " + author + " with " + numberChapter + ".\nPlease wait...");
var book = '<html>\n<head>\n<meta content="text/html; charset=UTF-8" http-equiv="Content-Type">\n';
book += '<title>' + title + '</title>';
book += '<meta content="' + author + '" name="author">';
book += '</head>\n<body ' + calculateBodyStyle() + ' >';
function addChapter(itemChapter) {
console.log(PREFIX_LOG + "addChapter" + itemChapter);
var storyId = itemChapter.url;
var jsonStory = getJsonStoryByStoryId(storyId);
console.log(jsonStory);
var chapterTitle = jsonStory.submission.title;
var description = jsonStory.submission.description;
book += '<h1 class=\'chapter\'' + chapterStyle + '>' + chapterTitle + '</h1>';
book += '<h2 class=\'chapter\'' + descriptionStyle + '>' + description + '</h2>';
book += getContentOfStory(storyId);
}
jsonLastChapter.submission.series.items.forEach(element => {
console.log(element);
// Your logic to perform on each element
addChapter(element);
});
saveTextAsFile(book, buildFilename(title, author, descriptionSeries, null, null, numberChapter));
}
function getChapterLinkFormDiv(storyDiv) {
return storyDiv.querySelectorAll("[class*='_title_link_']")[0].href;
}
function getABookForStoryDiv(storyDiv) {
console.log(PREFIX_LOG + "Processing getABookForStoryDiv");
console.log(storyDiv);
var link = getChapterLinkFormDiv(storyDiv);
var storyId = extractStoryId(link);
var jsonStory = getJsonStoryByStoryId(extractStoryId(link));
var title = jsonStory.submission.title;
var note = jsonStory.submission.rate_all;
var chapterTitle = jsonStory.submission.title;
var description = jsonStory.submission.description;
var category = jsonStory.submission.category_info.pageUrl;
var author = jsonStory.submission.author.username;
if (options.isToEPUB) {
downloadStoryAsEPUBFromJsonAndDiv(jsonStory);
} else {
getABookForStory(storyId, title, note, chapterTitle, description, category, author);
}
}
function getABookForStory(storyId, title, note, chapterTitle, description, category, author) {
var book = '<html>\n<head>\n<meta content="text/html; charset=UTF-8" http-equiv="Content-Type">\n';
console.log(PREFIX_LOG + 'title' + title);
book += '<title>' + title + '</title>';
book += '<meta content="' + author + '" name="author">';
book += '</head>\n<body ' + calculateBodyStyle() + ' >';
if (chapterTitle != null) {
book += '<h1 class=\'chapter\'' + chapterStyle + '>' + chapterTitle + '</h1>';
}
if (description != null) {
book += '<h2 class=\'chapter\'' + descriptionStyle + '>' + description + '</h2>';
}
book += getContentOfStory(storyId);
saveTextAsFile(book, buildFilename(title, author, description, note, category, null));
}
function extractStoryId(url) {
// Split the URL by slashes
const parts = url.split('/');
// Get the last part of the array
const lastPart = parts[parts.length - 1];
return lastPart;
}
function getContentOfStoryOfPage(storyId, current_page) {
var apiURL = "https://literotica.com/api/3/stories/" + storyId + '?params=%7B"contentPage"%3A' + current_page + '%7D';
var xhr = new XMLHttpRequest();
var toReturn = "[Page " + current_page + " not Found]";
xhr.open('GET', apiURL, false); // false makes the request synchronous
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
const response = JSON.parse(xhr.responseText);
toReturn = response.pageText;
} else {
console.error(PREFIX_LOG + 'Request failed with status ' + xhr.status);
}
};
xhr.onerror = function() {
console.error(PREFIX_LOG + 'Request failed');
};
xhr.send();
return toReturn;
}
function getJsonSerieBySerieId(serieId) {
var apiURL = "https://literotica.com/api/3/series/" + serieId;
console.log(PREFIX_LOG + "Fetching on API -> " + apiURL);
var toReturn;
var xhr = new XMLHttpRequest();
xhr.open('GET', apiURL, false); // false makes the request synchronous
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
toReturn = JSON.parse(xhr.responseText);
console.log(toReturn);
} else {
console.error(PREFIX_LOG + 'Request failed with status ' + xhr.status);
}
};
xhr.onerror = function() {
console.error(PREFIX_LOG + 'Request failed');
};
xhr.send();
return toReturn;
}
function getJsonStoryByStoryId(storyId) {
var apiURL = "https://literotica.com/api/3/stories/" + storyId;
console.log(PREFIX_LOG + "Fetching on API -> " + apiURL);
var toReturn;
var xhr = new XMLHttpRequest();
xhr.open('GET', apiURL, false); // false makes the request synchronous
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
toReturn = JSON.parse(xhr.responseText);
console.log(toReturn);
} else {
console.error(PREFIX_LOG + 'Request failed with status ' + xhr.status);
}
};
xhr.onerror = function() {
console.error(PREFIX_LOG + 'Request failed');
};
xhr.send();
return toReturn;
}
function convertTextToHTML(text) {
// Replace CRLF with <br> tags
var htmlText = text.replace(/\r\n/g, "<br>");
// Replace LF with <br> tags
htmlText = htmlText.replace(/\n/g, "<br>");
// Replace CR with <br> tags
htmlText = htmlText.replace(/\r/g, "<br>");
return htmlText;
}
// Function parsing all pages to get the storie based
function getContentOfStory(storyId) {
console.log(PREFIX_LOG + "Processing -> " + storyId);
var apiURL = "https://literotica.com/api/3/stories/" + storyId;
console.log(PREFIX_LOG + "Fetching on API -> " + apiURL);
var toReturn;
var xhr = new XMLHttpRequest();
xhr.open('GET', apiURL, false); // false makes the request synchronous
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
const response = JSON.parse(xhr.responseText);
toReturn = response.pageText;
var pageCount = response.meta.pages_count;
console.log(PREFIX_LOG + "pageCount => " + pageCount);
if (pageCount > 0) {
for (let currentPage = 2; currentPage <= pageCount; currentPage++) {
const result = getContentOfStoryOfPage(storyId, currentPage); // Call the given function
toReturn += result; // Concatenate the result to toReturn
}
}
} else {
console.error(PREFIX_LOG + 'Request failed with status ' + xhr.status);
}
};
xhr.onerror = function() {
console.error(PREFIX_LOG + 'Request failed');
};
xhr.send();
toReturn = convertTextToHTML(toReturn);
return toReturn;
}
// Function used to return content as a file for the user.
function saveTextAsFile(textToWrite, fileNameToSaveAs) {
var textFileAsBlob = new Blob([textToWrite], {
type: 'text/javascript'
});
var downloadLink = document.createElement('a');
downloadLink.download = fileNameToSaveAs;
downloadLink.innerHTML = 'Download File';
// Firefox requires the link to be added to the DOM
// before it can be clicked.
downloadLink.href = window.URL.createObjectURL(textFileAsBlob);
//downloadLink.onclick = destroyClickedElement;
downloadLink.style.display = 'none';
document.body.appendChild(downloadLink);
downloadLink.click();
}
function buildFilename(title, author, description, note, category, numberChapter) {
var toReturn = title;
if (options.isNumberChapterInFilename) {
toReturn = toReturn + SEPARATOR + numberChapter + POSTFIX_NUMER_CHAPTER;
}
if (options.isUsernameInFilename) {
toReturn = toReturn + SEPARATOR + author;
}
if (options.isDescriptionInFilename) {
if (description != null && description != "") {
toReturn = toReturn + SEPARATOR + description.replace(/[^\w\s]/gi, '');
}
}
if (options.isNoteInFilename) {
if (note != null && note != "") {
toReturn = toReturn + SEPARATOR + PREFIX_NOTE + note;
}
}
if (options.isCategoryInFilename) {
if (category != null && category != "") {
toReturn = toReturn + SEPARATOR + category;
}
}
// Add file extension;
toReturn = toReturn + '.html';
return toReturn;
}
function buildFilenameEpub(title, author, description, note, category, numberChapter) {
var toReturn = title;
if (options.isNumberChapterInFilename) {
toReturn = toReturn + SEPARATOR + numberChapter + POSTFIX_NUMER_CHAPTER;
}
if (options.isUsernameInFilename) {
toReturn = toReturn + SEPARATOR + author;
}
if (options.isDescriptionInFilename) {
if (description != null && description != "") {
toReturn = toReturn + SEPARATOR + description.replace(/[^\w\s]/gi, '');
}
}
if (options.isNoteInFilename) {
if (note != null && note != "") {
toReturn = toReturn + SEPARATOR + PREFIX_NOTE + note;
}
}
if (options.isCategoryInFilename) {
if (category != null && category != "") {
toReturn = toReturn + SEPARATOR + category;
}
}
// Add file extension;
toReturn = toReturn + '.epub';
return toReturn;
}
function hasNodeClassStartingWith(node, startclass) {
const classList = node.classList;
for (let i = 0; i < classList.length; i++) {
if (classList[i].startsWith(startclass)) {
return true;
}
}
return false;
}
// Callback function to be called when an element is added to the DOM
function handleElementAdded(mutationsList, observer) {
for (var mutation of mutationsList) {
if (mutation.type === 'childList') {
// Check if nodes were added
mutation.addedNodes.forEach(function(node) {
if (node.nodeType === 1 &&
(hasNodeClassStartingWith(node, '_part_row_') ||
hasNodeClassStartingWith(node, '_card_') ||
hasNodeClassStartingWith(node, '_content_') ||
hasNodeClassStartingWith(node, 'container_2025')
)
) {
addDownloadChapterButtonTo(node);
addDownloadSeriesButtonTo(node);
addDownloadIndividualStoryButtonTo(node);
// Call your function here or do any other necessary action
}
});
}
}
}
// Create a MutationObserver instance
var observer = new MutationObserver(handleElementAdded);
// Define the options for the observer (in this case, we're observing changes to the child list)
var observerConfig = {
childList: true,
subtree: true // This option allows us to observe changes within the entire subtree of the target node
};
// Start observing the target node (in this example, we're observing the entire document)
observer.observe(document, observerConfig);
function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
async function downloadSerieAsEPUBFromJsonAndDiv(jsonSerie, jsonLastChapter, allChapter) {
console.log(PREFIX_LOG+"[jsonSerie]");console.log(jsonSerie);
var title = jsonSerie.title;
// Get the X Part Series
var author = jsonLastChapter.submission.author.username;
var numberChapter = jsonLastChapter.submission.series.meta.order.length;
alert("Starting building EPUB for " + title + " of " + author + " with " + numberChapter + ".\nPlease wait...");
// ============================================================
// SECTION E.1 Static part
// ============================================================
// ============================================================
// SECTION E.1.a Zip
// ============================================================
var zip = new JSZip();
// ============================================================
// SECTION E.1.b mimetype (Required: uncompressed mimetype)
// ============================================================
zip.file("mimetype", "application/epub+zip", {compression: "STORE"});
// ============================================================
// SECTION E.1.c META-INF\container.xml
// ============================================================
zip.folder("META-INF").file("container.xml",
`<?xml version="1.0"?>
<container version="1.0"
xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
<rootfiles>
<rootfile
full-path="OEBPS/content.opf"
media-type="application/oebps-package+xml"/>
</rootfiles>
</container>`
);
// ============================================================
// SECTION E.2 Global part
// ============================================================
var folderOEBPS = zip.folder("OEBPS");
// SECTION E.2 cover.xhtml
var coverContent =
`<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>[[title]]</title>
<!-- optional style-->
</head>
<body>
<div class="cover">
<h1>[[title]]</h1>
<p class="desc">[[description]]</p>
</div>
</body>
</html>`;
coverContent = coverContent.replaceAll("[[title]]", jsonSerie.title!=null ? jsonSerie.title: "");
coverContent = coverContent.replaceAll("[[description]]", jsonSerie.description!=null ? jsonSerie.description: "");
folderOEBPS.file("cover.xhtml", coverContent);
// SECTION E.2 toc.ncx (Table of Contents)
var tocContent =
`<?xml version="1.0" encoding="UTF-8"?>
<ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1">
<head>
<meta name="dtb:uid" content="urn:uuid:[[uid]]"/>
<meta name="dtb:depth" content="1"/>
<meta name="dtb:totalPageCount" content="0"/>
<meta name="dtb:maxPageNumber" content="0"/>
</head>
<docTitle><text>[[title]]</text></docTitle>
<navMap>
<navPoint id="cover" playOrder="1"><navLabel><text>Cover</text></navLabel><content src="cover.xhtml"/></navPoint>
[[allFragments]]
</navMap>
</ncx>`;
var fragmentTOC = `<navPoint id="[[chapter-id]]" playOrder="[[order]]"><navLabel><text>[[chapter-title]]</text></navLabel><content src="[[chapter-id]].xhtml"/></navPoint>`;
var allFragments ="";
jsonLastChapter.submission.series.items.forEach((value, index) => {
currentFragment = fragmentTOC.replaceAll("[[chapter-id]]", "chapter_"+index+1);
currentFragment = currentFragment.replaceAll("[[order]]", index+2);
allFragments+=currentFragment;
});
tocContent = tocContent.replaceAll("[[title]]", jsonSerie.title!=null ? jsonSerie.title: "");
tocContent = tocContent.replaceAll("[[uid]]", generateUUID());
tocContent = tocContent.replaceAll("[[allFragments]]", allFragments);
folderOEBPS.file("toc.ncx", tocContent);
// ============================================================
// SECTION E.2 content.opf (Table of Contents)
// ============================================================
opfContent =`<?xml version="1.0" encoding="UTF-8"?>
<package xmlns="http://www.idpf.org/2007/opf" unique-identifier="BookId" version="2.0">
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">
<dc:identifier id="BookId">urn:uuid:c065e548-d835-4f91-b4eb-fc918a7e2639</dc:identifier>
<dc:title>[[title]]</dc:title>
<dc:creator opf:role="aut">[[author]]</dc:creator>
<dc:subject>[[subject]]</dc:subject>
<dc:description>[[description]]</dc:description>
<dc:publisher>Literotica Downloader</dc:publisher>
<dc:date>[[date]]</dc:date>
<dc:language>en</dc:language>
<dc:source>[[source]]</dc:source>
<meta name="cover" content="cover"/>
</metadata>
<manifest>
<item id="ncx" href="toc.ncx" media-type="application/x-dtbncx+xml"/>
<item id="cover" href="cover.xhtml" media-type="application/xhtml+xml"/>
[[allFragmentsManifest]]
</manifest>
<spine toc="ncx">
<itemref idref="cover"/>
[[allFragmentsSpine]]
</spine>
</package>`;
var fragmentManifestOpf = `<<item id="[[chapter-id]]" href="[[chapter-id]].xhtml" media-type="application/xhtml+xml"/>`;
var fragmentSpineOpf = `<itemref idref="[[chapter-id]]"/>`;
var allFragmentsManifest ="";
var allFragmentsSpine ="";
jsonLastChapter.submission.series.items.forEach((value, index) => {
allFragmentsManifest+= fragmentManifestOpf.replaceAll("[[chapter-id]]", "chapter_"+index+1);
allFragmentsSpine+= fragmentSpineOpf.replaceAll("[[chapter-id]]", "chapter_"+index+1);
});
opfContent = opfContent.replaceAll("[[title]]", jsonSerie.title!=null ? jsonSerie.title: "");
opfContent = opfContent.replaceAll("[[author]]", jsonLastChapter.submission.author.username);
opfContent = opfContent.replaceAll("[[subject]]", jsonLastChapter.submission.category_info.pageUrl);
opfContent = opfContent.replaceAll("[[description]]", jsonSerie.description!=null ? jsonSerie.description: "");
opfContent = opfContent.replaceAll("[[author]]", jsonLastChapter.submission.author.username);
opfContent = opfContent.replaceAll("[[date]]", new Date().toISOString().split('T')[0]);
opfContent = opfContent.replaceAll("[[source]]", "literotica.com");
opfContent = opfContent.replaceAll("[[allFragmentsManifest]]", allFragmentsManifest);
opfContent = opfContent.replaceAll("[[allFragmentsSpine]]", allFragmentsSpine);
folderOEBPS.file("content.opf", opfContent);
// ============================================================
// SECTION E.3 chapter(s) chapter-XXX.xhtml
// ============================================================
jsonLastChapter.submission.series.items.forEach((value, index) => {
var chapterId = "chapter_"+index+1;
var chapterContent = `<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>[[chapter-title]]</title>
<!-- optional style-->
</head>
<body>
<h1>[[chapter-title]]</h1>
<h2>[[chapter-description]]</h2>
<h3>[[chapter-tags]]</h3>
[[chapter-content]]
</body>
</html>`;
console.log(PREFIX_LOG + "addChapter" + value);
var storyId = value.url;
var jsonStory = getJsonStoryByStoryId(storyId);
console.log(jsonStory);
chapterContent = chapterContent.replaceAll("[[chapter-title]]",value.title);
chapterContent = chapterContent.replaceAll("[[chapter-description]]",jsonStory.submission.description);
chapterContent = chapterContent.replaceAll("[[chapter-tags]]","Tags : "+jsonStory.submission.tags.map(item => item.tag).join(", "));
chapterContent = chapterContent.replaceAll("[[chapter-content]]",getContentOfStory(storyId));
folderOEBPS.file(chapterId+".xhtml",chapterContent);
});
try {
console.log(PREFIX_LOG+"generateAsync");
const blob = await zip.generateAsync({
type: "blob",
mimeType: "application/epub+zip"
});
console.log(blob);
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = buildFilenameEpub(title, author, null, null, null, numberChapter);
a.click();
} catch (e) {
console.error(e);
}
}
async function downloadStoryAsEPUBFromJsonAndDiv(jsonStory) {
console.log(PREFIX_LOG+"jsonStory]");console.log(jsonStory);
var title = jsonStory.submission.title;
// Get the X Part Series
var author = jsonStory.submission.author.username;
// ============================================================
// SECTION E.1 Static part
// ============================================================
// ============================================================
// SECTION E.1.a Zip
// ============================================================
var zip = new JSZip();
// ============================================================
// SECTION E.1.b mimetype (Required: uncompressed mimetype)
// ============================================================
zip.file("mimetype", "application/epub+zip", {compression: "STORE"});
// ============================================================
// SECTION E.1.c META-INF\container.xml
// ============================================================
zip.folder("META-INF").file("container.xml",
`<?xml version="1.0"?>
<container version="1.0"
xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
<rootfiles>
<rootfile
full-path="OEBPS/content.opf"
media-type="application/oebps-package+xml"/>
</rootfiles>
</container>`
);
// ============================================================
// SECTION E.2 Global part
// ============================================================
var folderOEBPS = zip.folder("OEBPS");
// SECTION E.2 cover.xhtml
var coverContent =
`<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>[[title]]</title>
<!-- optional style-->
</head>
<body>
<div class="cover">
<h1>[[title]]</h1>
<p class="desc">[[description]]</p>
</div>
</body>
</html>`;
coverContent = coverContent.replaceAll("[[title]]", jsonStory.submission.title!=null ? jsonStory.submission.title: "");
coverContent = coverContent.replaceAll("[[description]]", jsonStory.submission.description!=null ? jsonStory.submission.description: "");
folderOEBPS.file("cover.xhtml", coverContent);
// SECTION E.2 toc.ncx (Table of Contents)
var tocContent =
`<?xml version="1.0" encoding="UTF-8"?>
<ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1">
<head>
<meta name="dtb:uid" content="urn:uuid:[[uid]]"/>
<meta name="dtb:depth" content="1"/>
<meta name="dtb:totalPageCount" content="0"/>
<meta name="dtb:maxPageNumber" content="0"/>
</head>
<docTitle><text>[[title]]</text></docTitle>
<navMap>
<navPoint id="cover" playOrder="1"><navLabel><text>Cover</text></navLabel><content src="cover.xhtml"/></navPoint>
[[allFragments]]
</navMap>
</ncx>`;
var fragmentTOC = `<navPoint id="[[chapter-id]]" playOrder="[[order]]"><navLabel><text>[[chapter-title]]</text></navLabel><content src="[[chapter-id]].xhtml"/></navPoint>`;
var allFragments ="";
currentFragment = fragmentTOC.replaceAll("[[chapter-id]]", "chapter_"+1);
currentFragment = currentFragment.replaceAll("[[order]]", 2);
allFragments+=currentFragment;
tocContent = tocContent.replaceAll("[[title]]", jsonStory.submission.title!=null ? jsonStory.submission.title: "");
tocContent = tocContent.replaceAll("[[uid]]", generateUUID());
tocContent = tocContent.replaceAll("[[allFragments]]", allFragments);
folderOEBPS.file("toc.ncx", tocContent);
// ============================================================
// SECTION E.2 content.opf (Table of Contents)
// ============================================================
opfContent =`<?xml version="1.0" encoding="UTF-8"?>
<package xmlns="http://www.idpf.org/2007/opf" unique-identifier="BookId" version="2.0">
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">
<dc:identifier id="BookId">urn:uuid:c065e548-d835-4f91-b4eb-fc918a7e2639</dc:identifier>
<dc:title>[[title]]</dc:title>
<dc:creator opf:role="aut">[[author]]</dc:creator>
<dc:subject>[[subject]]</dc:subject>
<dc:description>[[description]]</dc:description>
<dc:publisher>Literotica Downloader</dc:publisher>
<dc:date>[[date]]</dc:date>
<dc:language>en</dc:language>
<dc:source>[[source]]</dc:source>
<meta name="cover" content="cover"/>
</metadata>
<manifest>
<item id="ncx" href="toc.ncx" media-type="application/x-dtbncx+xml"/>
<item id="cover" href="cover.xhtml" media-type="application/xhtml+xml"/>
[[allFragmentsManifest]]
</manifest>
<spine toc="ncx">
<itemref idref="cover"/>
[[allFragmentsSpine]]
</spine>
</package>`;
var fragmentManifestOpf = `<<item id="[[chapter-id]]" href="[[chapter-id]].xhtml" media-type="application/xhtml+xml"/>`;
var fragmentSpineOpf = `<itemref idref="[[chapter-id]]"/>`;
var allFragmentsManifest ="";
var allFragmentsSpine ="";
allFragmentsManifest+= fragmentManifestOpf.replaceAll("[[chapter-id]]", "chapter_"+1);
allFragmentsSpine+= fragmentSpineOpf.replaceAll("[[chapter-id]]", "chapter_"+1);
opfContent = opfContent.replaceAll("[[title]]", jsonStory.submission.title!=null ? jsonStory.submission.title: "");
opfContent = opfContent.replaceAll("[[author]]", jsonStory.submission.author.username);
opfContent = opfContent.replaceAll("[[subject]]", jsonStory.submission.category_info.pageUrl);
opfContent = opfContent.replaceAll("[[description]]", jsonStory.submission.description!=null ? jsonStory.submission.description: "");
opfContent = opfContent.replaceAll("[[author]]", jsonStory.submission.author.username);
opfContent = opfContent.replaceAll("[[date]]", new Date().toISOString().split('T')[0]);
opfContent = opfContent.replaceAll("[[source]]", "literotica.com");
opfContent = opfContent.replaceAll("[[allFragmentsManifest]]", allFragmentsManifest);
opfContent = opfContent.replaceAll("[[allFragmentsSpine]]", allFragmentsSpine);
folderOEBPS.file("content.opf", opfContent);
// ============================================================
// SECTION E.3 chapter(s) chapter-XXX.xhtml
// ============================================================
var chapterId = "chapter_"+1;
var chapterContent = `<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>[[chapter-title]]</title>
<!-- optional style-->
</head>
<body>
<h1>[[chapter-title]]</h1>
<h2>[[chapter-description]]</h2>
<h3>[[chapter-tags]]</h3>
[[chapter-content]]
</body>
</html>`;
chapterContent = chapterContent.replaceAll("[[chapter-title]]",jsonStory.submission.title);
chapterContent = chapterContent.replaceAll("[[chapter-description]]",jsonStory.submission.description);
chapterContent = chapterContent.replaceAll("[[chapter-tags]]","Tags : "+jsonStory.submission.tags.map(item => item.tag).join(", "));
chapterContent = chapterContent.replaceAll("[[chapter-content]]",getContentOfStory(jsonStory.submission.url));
folderOEBPS.file(chapterId+".xhtml",chapterContent);
try {
console.log(PREFIX_LOG+"generateAsync");
const blob = await zip.generateAsync({
type: "blob",
mimeType: "application/epub+zip"
});
console.log(blob);
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = buildFilenameEpub(title, author, jsonStory.submission.description, jsonStory.submission.rate_all, jsonStory.submission.category_info.pageUrl, null);
a.click();
} catch (e) {
console.error(e);
}
}