Literotica Downloader

Single page HTML download for Literotica with improved readability

As of 2021-04-25. 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*
// @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     2.9
// @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==

// Those valuse can be modifyed by the icon GreaseMonkey > User script commands > Toggle ...
var options= {
	'isNightMode' : true,
	'isUsernameInFilename' : true,
	'isDescriptionInFilename' : false,
	'isNoteInFilename' : false,
	'isCategoryInFilename' : false
}

// @grant
var SEPARATOR = '_';
var PREFIX_NOTE = 'RATING_';
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 iconDonwload = ' style=\'background-image: url("https://marcoceppi.github.io/bootstrap-glyphicons/img/glyphicons-halflings.png");     background-position: -120px -24px;cursor: pointer;cursor: hand;    background-repeat: no-repeat;    display: inline-block;    height: 14px;    line-height: 14px;    vertical-align: text-bottom;    width: 14px;}\'';
var messagePleaseRefreshPage= 'For the modification to be taken into account.\nPlease refresh the page.';
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;
}
$("head").append (
	// loading CSS JqueryUI for options menu
  '<link href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css" rel="stylesheet" type="text/css">'
);
$(document).ready(function() {
    'use strict';
    const isdebug = false;
    const debug = isdebug ? console.log.bind(console) : () => {};

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

    let GMsetValue, GMgetValue, GMregisterMenuCommand, GMunregisterMenuCommand, GMnotification, GMopenInTab;
    const GMinfo = GM_info;
    const handlerInfo = GMinfo.scriptHandler;
    const isGM = Boolean(handlerInfo.toLowerCase() === 'greasemonkey');

    debug(`//-> CheckGM: ${isGM} >> ${handlerInfo}`);
		var panelOptionDownloader=  `
<div id="literoticaDownloaderOption" title="Options Literotica Downloader" >
   Please Configure the options :
	<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(function onChangeCheckBox(checkbox){
    GMsetValue(this.id, this.checked);
    options[this.id] = this.checked;
  });
  $( "#literoticaDownloaderOption" ).dialog({ autoOpen: false, width: 350 });

    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" );
  });
    	function initCheckBox(element, index, array) {
			if (isGM) {
				GMgetValue(element,  options[element]).then((myData) => {
					options[element] = myData;
					$("#"+element)[0].checked= options[element];
				});
			}else {
				options[element] = GMgetValue(element, options[element]);
				$("#"+element)[0].checked= options[element];
			}

			return true;
		}
  // Init of all options
  Object.getOwnPropertyNames(options).every(initCheckBox)

    // 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) {
                    console.log($(data).find('a.l_bJ.l_bL').size());
                    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 = $.trim(myDiv.text().split(':')[0]);
        // Get the X Part Series
        var descriptionSeries = $.trim(myDiv.text().split(':')[1]);
        var author = $('.contactheader').text();
        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, 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, author, descriptionSeries, ));
    }

    function getABookForStoryDiv(myDiv) {
        var title = $.trim($($(myDiv)).text().split('(')[0]);
        var note = $.trim($($(myDiv)).text().split('(')[1]).replace(")", "");
        if ($(myDiv).find('a').size() >= 0) {
            var chapterTitle = $($(myDiv.parent()).find('td a')[0]).text();
            var description = $($(myDiv.parent()).find('td')[1]).text();
            var category = $($(myDiv.parent()).find('td')[2]).text();
        }
        var author = $('.contactheader').text();
        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">';
        book += '<meta content="' + author + '" name="author">';
        if(options.isNightMode){
          book += '</head>\n<body ' + bodyStyleNight + ' >';
        }else  {
        	book += '</head>\n<body ' + bodyStyle + ' >';
        }
        book += '<h1 class=\'chapter\'' + chapterStyle + '>' + chapterTitle + '</h1>';
        book += '<h2 class=\'chapter\'' + descriptionStyle + '>' + description + '</h2>';
        var link = $($(myDiv).find('a')[0]);
        book += getContentOfStoie(link.attr('href'));

        saveTextAsFile(book, buildFilename(title, author, description, note, category));
    }
    $('.ser-ttl td:nth-child(1)').prepend('<span ' + iconDonwload + '></span>');
    $('.ser-ttl td:nth-child(1) span').click(function() {
        getABookForSerieDiv($(this).parent().parent());
    });
    var idIcon = Math.floor(Math.random() * 100);
    $('.root-story td:nth-child(1), .sl td:nth-child(1)').prepend('<span ' + iconDonwload + ' id="' + idIcon + '"></span>');
    $('.root-story td:nth-child(1), .sl td:nth-child(1) span').click(function() {
        getABookForStoryDiv($(this).parent());
    });
});