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