Literotica Downloader

Single page HTML download for Literotica with improved readability

Versión del día 01/05/2021. Echa un vistazo a la versión más reciente.

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