Literotica Downloader

Single page HTML download for Literotica with improved readability

// ==UserScript==
// @name        Literotica Downloader
// @description Single page HTML download for Literotica with improved readability
// @namespace   literotica_downloader
// @include     https://tags.literotica.com/*
// @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
// @version     4.0
// @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
// ==/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= {
	'isNightMode' : true,
	'isUsernameInFilename' : true,
	'isDescriptionInFilename' : false,
	'isNoteInFilename' : false,
	'isCategoryInFilename' : false,
  'isBookmark' : false
};


// 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 bodyStyleNight = ' style="background-color:#333333; color: #EEEEEE; font-family: Helvetica,Arial,sans-serif; width: 50%; margin: 0 auto; line-height: 1.5em; font-size:2.2em; padding: 50px 0 50px 0;" ';
var bodyStyle = ' style="font-family: Helvetica,Arial,sans-serif; width: 50%; margin: 0 auto; line-height: 1.5em; font-size:2.2em; padding: 50px 0 50px 0;" ';
var chapterStyle = '_style="line-height: 1.4em;" ';
var descriptionStyle = ' style="line-height: 1.2em;" ';

/*
 * Constante for Filename
 */
var SEPARATOR = '_';
var PREFIX_NOTE = 'RATING_';




// 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/bootstrap-icons@1.4.1/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 = '.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);

document.addEventListener("DOMContentLoaded", function() {
    'use strict';
setTimeout(function() {
  const GMinfo = GM_info;
    const handlerInfo = GMinfo.scriptHandler;
    const isGM = Boolean(handlerInfo.toLowerCase() === 'greasemonkey');
  console.log(isGM);
  var panelOptionDownloader=  `
<div id="literoticaDownloaderOption" title="Options Literotica Downloader" >
   Please Configure the options :
	<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="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>
   </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('new value =>' + await GMgetValue(this.id));
        options[this.id] = this.checked;
    });
});
  console.log("init diaalog option start");
  $( "#literoticaDownloaderOption" ).dialog({ autoOpen: false, width: 450 });
  console.log("init diaalog option end");
if (isGM) {
    GMsetValue = GM.setValue;
    GMgetValue = GM.getValue;
    GMregisterMenuCommand = GM.registerMenuCommand;
    GMunregisterMenuCommand = function() {};
    GMnotification = GM.notification;
    console.log("It's GM !");
} else {
    console.log("Other");

    GMsetValue = GM_setValue;
    GMgetValue = GM_getValue;
    GMregisterMenuCommand = GM_registerMenuCommand;
    GMunregisterMenuCommand = GM_unregisterMenuCommand;
}

// 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
   */
  const titlesChapter = document.querySelectorAll('._series_parts__item__series_part_card_29o2p_264   ._works_item__title_29o2p_36 ._item_title_29o2p_162 ');
  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('afterend', '<span id="' + idIcon + '" class="ldChapter"> ' + iconDonwload + '</span>');
	});
  
  document.querySelectorAll('.ldChapter').forEach(element => {
    element.addEventListener('click', function() {getABookForStoryDiv(element.parentNode.parentNode)});
  });
  /*
   * Download Button for Series
   */
  const titlesSeries = document.querySelectorAll('._works_item__series_expanded_header_card_29o2p_15 ._works_item__title_29o2p_36 ._item_title_29o2p_162');
  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('afterend', '<span id="' + idIcon + '" class="ldSerie"> ' + iconDonwload + '</span>');
	});
  
  document.querySelectorAll('.ldSerie').forEach(element => {
    element.addEventListener('click', function() {getABookForSerieDiv(element.parentNode.parentNode)});
  });
  /*
   * Section download on tags
   */
  document.addEventListener('DOMNodeInserted', function(e) {
    if (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("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 getTitleChapterFormStoryOnItemCategory(cardComponent){
    return cardComponent.querySelector('h3 a').textContent;
}

function getNoteChapterFormStoryOnItemCategory(cardComponent){
    return cardComponent.querySelector('.b-slib-rating').textContent;
}

function getAuthorChapterFormStoryOnItemCategory(cardComponent){
    return cardComponent.querySelector('.b-user-info-name').textContent;
}

function getABookForStoryItemCategory(cardComponent) {
	console.log("In getABookForStoryItemCategory");
    console.log(cardComponent);
	console.log("0");
    var title = getTitleChapterFormStoryOnItemCategory(cardComponent);
	console.log("1");
    var note =  getNoteChapterFormStoryOnItemCategory(cardComponent);
	console.log("2");
    var chapterTitle = title;
    var description = cardComponent.querySelector('.b-slib-description').textContent.trim();
    var category = document.querySelector('h1').textContent.replace(' Stories Hub', "");
    var link = cardComponent.querySelector('h3 a').getAttribute('href');
    var author = getAuthorChapterFormStoryOnItemCategory(cardComponent);
    console.log("title:"+title)
    console.log("note:"+note)
    console.log("chapterTitle:"+chapterTitle)
    console.log("description:"+description)
    console.log("category:"+category)
    console.log("link:"+link)
    getABookForStory(link, title, note, chapterTitle, description, category, author);
}


 function getTitleChapterFormStoryCardComponent(cardComponent){
    return cardComponent.querySelector('.ai_ii').textContent;
}

function getNoteChapterFormStoryCardComponent(cardComponent){
    return cardComponent.querySelector('.K_H').textContent;
}

function getAuthorChapterFormStoryCardComponent(cardComponent){
    return cardComponent.querySelector('[typeof="Person"] .ai_im').textContent;
}

function getABookForStoryOnStoryCardComponent(cardComponent) {
    console.log(cardComponent);
    var title = getTitleChapterFormStoryCardComponent(cardComponent);
    var note =  getNoteChapterFormStoryCardComponent(cardComponent);
    var chapterTitle = title;
    var description = cardComponent.querySelector('.ai_ij').textContent;
    var category = cardComponent.querySelector('.ai_im').textContent;
    var link = cardComponent.querySelector('.ai_ii').getAttribute('href');
    var author = getAuthorChapterFormStoryCardComponent(cardComponent);
    console.log("title:"+title)
    console.log("note:"+note)
    console.log("chapterTitle:"+chapterTitle)
    console.log("description:"+description)
    console.log("category:"+category)
    console.log("link:"+link)
    getABookForStory(link, title, note, chapterTitle, description, category, author);
}


function getABookForSerieDiv(storyDiv ) {
        var title = getTitleChapterFormDiv(storyDiv);
        // Get the X Part Series
        var descriptionSeries = getDescriptionFormDiv(storyDiv);
  			var author = getAuthorFormDiv(storyDiv);
        alert("Starting building file for " + title + " of " + author + ".\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">';
      	if(options.isNightMode){
          book += '</head>\n<body ' + bodyStyleNight + ' >';
        }else  {
        	book += '</head>\n<body ' + bodyStyle + ' >';
        }

        function addChapter(element) {
          console.log("addChapter"+element);
          var chapterTitle = getTitleChapterFormDiv(element);
          var description = getDescriptionFormDiv(element);
          book += '<h1 class=\'chapter\'' + chapterStyle + '>' + chapterTitle + '</h1>';
          book += '<h2 class=\'chapter\'' + descriptionStyle + '>' + description + '</h2>';
          var link =getChapterLinkFormDiv(element);
          book += getContentOfStoie(link);
        }
      console.log(storyDiv.nextElementSibling);
  	storyDiv.nextElementSibling.querySelectorAll('._series_parts__wrapper_29o2p_246 ._series_parts__item_29o2p_259')
  	.forEach(element => {
      console.log(element);
      // Your logic to perform on each element
      addChapter(element);
    });
        saveTextAsFile(book, buildFilename(title, author, descriptionSeries, ));
    }

function getTitleChapterFormDiv(storyDiv){
  return storyDiv.querySelectorAll('._item_title_29o2p_162')[0].text;
}
function getNoteChapterFormDiv(storyDiv){
  return storyDiv.querySelectorAll('._work_item__stat_29o2p_109')[0].text;
  
}
function getDescriptionFormDiv(storyDiv){
  return storyDiv.querySelectorAll('._item_description_29o2p_176')[0].textContent;
  
} 
function getCategoryFormDiv(storyDiv){
  return storyDiv.querySelectorAll('._item_category_29o2p_72')[0].text;
  
}
function getChapterLinkFormDiv(storyDiv){
  return storyDiv.querySelectorAll('._item_title_29o2p_162')[0].href;
}
function getAuthorFormDiv(storyDiv){
  return storyDiv.querySelectorAll('._item_authorname__link_29o2p_71 ')[0].text;
}


function getABookForStoryDiv(storyDiv){
  console.log("test getABookForStoryDiv");
  console.log(storyDiv);
  var title = getTitleChapterFormDiv(storyDiv);
  var note =  getNoteChapterFormDiv(storyDiv);
  var chapterTitle = getTitleChapterFormDiv(storyDiv);;
  var description  =getDescriptionFormDiv(storyDiv);
  var category =getCategoryFormDiv(storyDiv);
  var link = getChapterLinkFormDiv(storyDiv);
  console.log("chapterTitle:"+chapterTitle)
  getABookForStory(link, title, note, chapterTitle, description, category, getAuthorFormDiv(storyDiv));
}

function getABookForStory(link, 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('title' + title);
        book += '<title>' + title + '</title>';
        book += '<meta content="' + author + '" name="author">';
        if(options.isNightMode){
          book += '</head>\n<body ' + bodyStyleNight + ' >';
        }else  {
        	book += '</head>\n<body ' + bodyStyle + ' >';
        }
      	if(chapterTitle != null){
        	book += '<h1 class=\'chapter\'' + chapterStyle + '>' + chapterTitle + '</h1>';
        }
      	if(description != null){
        	book += '<h2 class=\'chapter\'' + descriptionStyle + '>' + description + '</h2>';
        }
        book += getContentOfStoie(link);

        saveTextAsFile(book, buildFilename(title, author, description, note, category));
  	}

// Function parsing all pages to get the storie based
    function getContentOfStoie(baseURL) {
        console.log("Fetching " + baseURL);
        var remote;
        var xhr = new XMLHttpRequest();
        xhr.open('GET', baseURL, false); // false makes the request synchronous
        xhr.onload = function() {
            if (xhr.status >= 200 && xhr.status < 300) {
                var data = xhr.responseText;
                var parser = new DOMParser();
                var doc = parser.parseFromString(data, 'text/html');

                if (doc.querySelectorAll('a.l_bJ.l_bL').length > 0) {
                  
                  var linkNextPage = doc.querySelectorAll('a.l_bJ.l_bL')[0].href;
                  console.log("linkNextPage => "+linkNextPage);
                  if(linkNextPage.startsWith("https://tags.literotica.com")){
                    linkNextPage =linkNextPage.replace("https://tags.literotica.com",PREFIX_URL_PAGE)
                  }
                    
                    remote = doc.querySelector('.panel.article.aa_eQ .aa_ht').innerHTML + getContentOfStoie(linkNextPage);
                } else {
                    remote = doc.querySelector('.panel.article.aa_eQ .aa_ht').innerHTML;
                }
            } else {
                console.error('Request failed with status ' + xhr.status);
            }
        };
        xhr.onerror = function() {
            console.error('Request failed');
        };
        xhr.send();
        return remote;
    }
// 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) {
    var toReturn = title;
    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;
}