Literotica Downloader

Single page HTML download for Literotica with improved readability

As of 2021-05-01. See the latest version.

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 or Violentmonkey 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 Downloader
// @description Single page HTML download for Literotica with improved readability
// @namespace   literotica_downloader
// @include     https://www.literotica.com/stories/memberpage.php*
// @include     https://tags.literotica.com/*
// @include			https://www.literotica.com/c/*
// @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     3.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 ...
console.log("Init Literotica Downloader");
var options= {
	'isNightMode' : true,
	'isUsernameInFilename' : true,
	'isDescriptionInFilename' : false,
	'isNoteInFilename' : false,
	'isCategoryInFilename' : false,
  'isLargeTable' : false,
  'isBookmark' : false
};
// @grant
var SEPARATOR = '_';
var PREFIX_NOTE = 'RATING_';
var cssSelectorSerie ='.ser-ttl td:nth-child(1)';
var cssSelectorSingleStory ='.root-story td:nth-child(1)';
var cssSelectorChapter ='.sl td:nth-child(1)';
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;" ';
// 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>';

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;
}

var headerToAdd =  `<link href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css" rel="stylesheet" type="text/css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css">'
  '<style type="text/css">.lbIconOnStoryCardComponent {float:left}</style>
 `;
$("head").append (
	// loading CSS JqueryUI for options menu
    headerToAdd
);
let GMsetValue, GMgetValue, GMregisterMenuCommand, GMunregisterMenuCommand, GMnotification, GMopenInTab;

async function onReadyDoc() {
    'use strict';

    /* Perfectly Compatible For Greasemonkey4.0+, TamperMonkey, ViolentMonkey * F9y4ng * 20210209 */


    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>Interface: </legend>
			<div>
      	<input type="checkbox" name="isLargeTable" id="isLargeTable" class="ldCheckbox">
    	  <span>Large Story Table</span>
   	</div>
<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>
`;
  $('body').append(panelOptionDownloader);
  // Handle of change of value by user.
  $('.ldCheckbox').change(async function onChangeCheckBox(){

    console.log(this.id);
    GMsetValue(this.id, this.checked);
    console.log('new value =>'+ await GMgetValue(this.id));
    options[this.id] = this.checked;
      console.log("to"+await GM.getValue('isLargeTable'))
  });
  $( "#literoticaDownloaderOption" ).dialog({ autoOpen: false, width: 450 });

    if (isGM) {
        GMsetValue = GM.setValue;
        GMgetValue = GM.getValue;
        GMregisterMenuCommand = GM.registerMenuCommand;
        GMunregisterMenuCommand = () => {};
        GMnotification = GM.notification;
        console.log("It's GM !");
    } else {
        console.log("Other");

        GMsetValue = GM_setValue;
        GMgetValue = GM_getValue;
        GMregisterMenuCommand = GM_registerMenuCommand;
        GMunregisterMenuCommand = GM_unregisterMenuCommand;
    }
  // Globale registry of the options menu.
  GMregisterMenuCommand("Literotica Downloader Options", () => {
    $( "#literoticaDownloaderOption" ).dialog( "open" );
  });
    	async function initCheckBox(element, index, array) {
		options[element] = await GMgetValue(element,options[element]);
		$("#"+element)[0].checked= options[element];
        console.log(element+"-> "+options[element]);

			return true;
		}
  // Init of all options
	for(const element of  Object.getOwnPropertyNames(options)){
    await initCheckBox(element)
  }
  console.log("end Init");
  console.log(options);

    // 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 parsing all pages to get the storie based
    function getContentOfStoie(baseURL) {
        console.log("Fetching " + baseURL);
        var remote;
        $.ajax({
            url: baseURL,
            type: 'GET',
            async: false,
            crossDomain: true,
            success: function(data) {
                if ($(data).find('a.l_bJ.l_bL').size() > 0) {
                    remote = $(data).find('.panel.article.aa_eQ .aa_ht').html() + getContentOfStoie($(data).find('a.l_bJ.l_bL')[0].href);
                } else {
                    remote = $(data).find('.panel.article.aa_eQ .aa_ht').html();
                }
            }
        });
        return remote;
    }

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

        function addChapter(element, index, array) {
            if ($(this).find('a').size() > 0) {
                var chapeterTitle = $($(this).find('td a')[0]).text();
                var description = $($(this).find('td')[1]).text();
                book += '<h1 class=\'chapter\'' + chapterStyle + '>' + chapeterTitle + '</h1>';
                book += '<h2 class=\'chapter\'' + descriptionStyle + '>' + description + '</h2>';
                var link = $($(this).find('a')[0]);
                book += getContentOfStoie(link.attr('href'));
            }
        }
        myDiv.nextUntil('.ser-ttl,.root-story').each(addChapter);
        saveTextAsFile(book, buildFilename(title, getAuthor(), descriptionSeries, ));
    }
  	function getAuthor(){
      return $('.contactheader').text();
    }
  	function getTitleSerieFormDiv(myDiv){
      return $.trim(myDiv.text().split(':')[0]);
    }
  	function getDescriptionSerieFormDiv(myDiv){
      return $.trim(myDiv.text().split(':')[1]);
    }
		function getTitleChapterFormDiv(myDiv){
      return $.trim($(myDiv).text().split('(')[0]);
    }
  	function getNoteChapterFormDiv(myDiv){
      return $.trim($($(myDiv)).text().split('(')[1]).replace(")", "");
    }
  	function isSerie(myDiv){
      return $(myDiv.parent('tr')).hasClass('ser-ttl');
    }

  	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 getABookForStoryDiv(myDiv) {
        var title = getTitleChapterFormDiv(myDiv);
        var note =  getNoteChapterFormDiv(myDiv);
        var chapterTitle = null;
      var description = null;
      var category =null;
        if ($(myDiv).find('a').size() >= 0) {
            chapterTitle = $($(myDiv.parent()).find('td a')[0]).text();
            description = $($(myDiv.parent()).find('td')[1]).text();
            category = $($(myDiv.parent()).find('td')[2]).text();
        }
      	var link = $($(myDiv).find('a')[0]).attr('href');
      console.log("chapterTitle:"+chapterTitle)
      	getABookForStory(link, title, note, chapterTitle, description, category, getAuthor());
    }
  /*****
   *
   * Section download on author
   *
   ****/

    $(cssSelectorSerie).prepend('<span class="ldSerie"> ' + iconDonwload +'</span>');
    $('.ldSerie').click(function() {
        getABookForSerieDiv($(this).parent().parent());
    });
    var idIcon = Math.floor(Math.random() * 1000);
    $(cssSelectorSingleStory+','+cssSelectorChapter).prepend('<span id="' + idIcon + '" class="ldChapter"> ' + iconDonwload + '</span>');
    $('.ldChapter').click(function() {
        getABookForStoryDiv($(this).parent());
    });
  function getTitleChapterFormStoryCardComponent(cardComponent){
    return $(cardComponent).find('.ai_ii').text();
  }
  function getNoteChapterFormStoryCardComponent(cardComponent){
     return $(cardComponent).find('.K_H').text();
  }
  function getAuthorChapterFormStoryCardComponent(cardComponent){
     return $(cardComponent).find('[typeof="Person"] .ai_im').text();
  }
  function getABookForStoryOnStoryCardComponent(cardComponent) {
    console.log(cardComponent);
    var title = getTitleChapterFormStoryCardComponent(cardComponent);
    var note =  getNoteChapterFormStoryCardComponent(cardComponent);
    var chapterTitle = title;
    var description = $(cardComponent).find('.ai_ij').text();
    var category = $(cardComponent).find('.ai_im').text();
    var link = $($(cardComponent).find('.ai_ii')[0]).attr('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);
  }
  
  /*****
   *
   * Section download on category
   *
   ****/
    var idIcon = Math.floor(Math.random() * 1000);
    $(".b-slb-item").prepend('<span id="' + idIcon + '" class="ldChapter lbIconOnStoryCardComponent"> ' + iconDonwload + '</span>');
    $('.ldChapter').click(function() {
        getABookForStoryItemCategory($(this).parent());
    });

  function getTitleChapterFormStoryOnItemCategory(cardComponent){
    return $(cardComponent).find('h3 a').text();
  }
  function getNoteChapterFormStoryOnItemCategory(cardComponent){
     return $(cardComponent).find('.b-slib-rating').text();
  }
  function getAuthorChapterFormStoryOnItemCategory(cardComponent){
     return $(cardComponent).find('.b-user-info-name').text();
  }
  function getABookForStoryItemCategory(cardComponent) {
    console.log(cardComponent);
    var title = getTitleChapterFormStoryOnItemCategory(cardComponent);
    var note =  getNoteChapterFormStoryOnItemCategory(cardComponent);
    var chapterTitle = title;
    var description = $(cardComponent).find('.b-slib-description')[0].childNodes[0].nodeValue;
    description = description.substring(0,description.length-4)
    var category = $('h1').text().replace(' Stories Hub', "");
    var link = $($(cardComponent).find('h3 a')[0]).attr('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);
  }

  /*****
   *
   * Section download on tags
   *
   ****/
  $(document).on('DOMNodeInserted', function(e) {
    if ( $(e.target).hasClass('ai_gJ') ) {
      // Créé une instance de l'observateur lié à la fonction de callback
       $(e.target).find('.ai_iG').prepend('<span class="ldDownloadTag lbIconOnStoryCardComponent"> ' + iconDonwload +'</span>');
      $('.ldDownloadTag.lbIconOnStoryCardComponent').unbind( "click" ).click(function() {
        getABookForStoryOnStoryCardComponent($(this).parent().parent());
    	});
    }
	});


  /****
   *
   * Section Bookmark
   *
   ****/
  function isStoryCardComponent(myDiv){
    return $(myDiv).hasClass('ai_gJ');
  }
  function isItemCategory(myDiv){
    return $(myDiv).hasClass("b-slb-item");
  }

  	function getBookmarkKey(myDiv){
      var key = "LD_BOOKMARK_";
      if(isStoryCardComponent(myDiv)){
         return key+getAuthorChapterFormStoryCardComponent(myDiv)+"_chapter_"+getTitleChapterFormStoryCardComponent(myDiv);
      }
      if(isItemCategory(myDiv)){
         return key+getAuthorChapterFormStoryOnItemCategory(myDiv)+"_chapter_"+getTitleChapterFormStoryOnItemCategory(myDiv);
      }
      var key = key+getAuthor();

      if(isSerie(myDiv)){
      	key =key+"_serie_"+getTitleSerieFormDiv(myDiv);
      }else {
        key =key+"_chapter_"+getTitleChapterFormDiv(myDiv);
      }

      return key;
    }
  	async function toogleBookmark(toToogle){
      var key = getBookmarkKey($(toToogle));
      var oldValue = await GMgetValue(key);
      if(oldValue != "1") {
      		await GMsetValue(key,"1");
    	}else {
        	await GMsetValue(key,null);
      }
      await updateBookmark($(toToogle));

  	}
  	async function updateBookmark(toToogle){
      console.log("in updateBookmark");
      $(toToogle).find('.ldbookMark').remove();
      var key = getBookmarkKey(toToogle);
      console.log(key);
      var value = await GMgetValue(key);
      if(options.isBookmark){
        if(isStoryCardComponent(toToogle)){
          if(value!=null){
            $(toToogle).find('.ai_iG').prepend('<span class="ldbookMark lbIconOnStoryCardComponent"> ' + iconBookmarkCheck +'</span>');
          }else {
            $(toToogle).find('.ai_iG').prepend('<span class="ldbookMark lbIconOnStoryCardComponent"> ' + iconBookmark +'</span>');
          }
          $(toToogle).find('.ldbookMark').click(function() {
       			toogleBookmark($(this).parent().parent());
    			});
        }else if(isItemCategory(toToogle)){
				if(value!=null){
            $(toToogle).prepend('<span class="ldbookMark lbIconOnStoryCardComponent"> ' + iconBookmarkCheck +'</span>');
          }else {
            $(toToogle).prepend('<span class="ldbookMark lbIconOnStoryCardComponent"> ' + iconBookmark +'</span>');
          }
          $(toToogle).find('.ldbookMark').click(function() {
       			toogleBookmark($(this).parent());
    			});
				}else {
          if(value!=null){
            $(toToogle).prepend('<span class="ldbookMark"> ' + iconBookmarkCheck +'</span>');
          }else {
            $(toToogle).prepend('<span class="ldbookMark"> ' + iconBookmark +'</span>');
          }
          $(toToogle).find('.ldbookMark').click(function() {
       			toogleBookmark($(this).parent());
    			});
        }

    	}
  	}
  $(cssSelectorSingleStory+','+cssSelectorChapter+','+cssSelectorSerie).prepend('<span class="ldbookMark"></span>');
  function initBookmark(index,element , array) {
    updateBookmark($(element).parent());
  }
  /*****
   *
   * Section Bookmark on category
   *
   ****/
  $(".b-slb-item").prepend('<span class="ldbookMark"></span>');
  function initBookmark(index,element , array) {
    updateBookmark($(element).parent());
  }
  /*****
   *
   * Section Bookmark on tags
   *
   ****/
  async function insertBookBackOnDOMNodeInserted(e) {
    if ( $(e.target).hasClass('ai_gJ') ) {
      // Créé une instance de l'observateur lié à la fonction de callback
       $(e.target).find('.ai_iG').prepend('<span class="ldbookMark">toto</span>'); 
       await updateBookmark($(e.target));
    }
	}
  $(document).on('DOMNodeInserted',(e)=> insertBookBackOnDOMNodeInserted(e));
  /****
   *
   * Section Large Table
   *
   ****/
  function updateLargeTable(){
  	// Enlarge table
    console.log(options);
    if(options.isLargeTable== true){
      console.log('Enlarged table')
      $('div[style*=width], table[width="733"], img[width="733"], table[width="760"]').each(function(index, value) {if($(value).width()==733 || $(value).width()==760){$(value).width("100%")}})
    }
  }
	updateLargeTable();
  $('.ldbookMark').each(initBookmark);

}
$(document).ready(onReadyDoc());