Sleazy Fork is available in English.

Literotica Downloader

Single page HTML download for Literotica with improved readability

  1. // ==UserScript==
  2. // @name Literotica Downloader
  3. // @description Single page HTML download for Literotica with improved readability
  4. // @namespace literotica_downloader
  5. // @include https://tags.literotica.com/*
  6. // @include https://*.tags.literotica.com/*
  7. // @include https://www.literotica.com/new/stories*
  8. // @include https://literotica.com/c/*
  9. // @include https://**.literotica.com/c/*
  10. // @include https://www.literotica.com/c/*
  11. // @include https://www.literotica.com/authors/**/works/stories*
  12. // @require https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js
  13. // @require https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js
  14. // @version 5.10
  15. // @grant GM_info
  16. // @grant GM_registerMenuCommand
  17. // @grant GM.registerMenuCommand
  18. // @grant GM_unregisterMenuCommand
  19. // @grant GM_openInTab
  20. // @grant GM_getValue
  21. // @grant GM.getValue
  22. // @grant GM_setValue
  23. // @grant GM.setValue
  24. // @grant GM_notification
  25. // @grant GM.notification
  26. // @author Improved by a random redditor and @nylonmachete, originally by Patrick Kolodziejczyk and fixed by i23234234
  27. // ==/UserScript==
  28. /* jshint esversion: 8 */
  29. // Those valuse can be modifyed by the icon GreaseMonkey > User script commands > Toggle ...
  30. let GMsetValue, GMgetValue, GMregisterMenuCommand, GMunregisterMenuCommand, GMnotification, GMopenInTab;
  31. var options= {
  32. 'isNightMode' : true,
  33. 'isNumberChapterInFilename': true,
  34. 'isUsernameInFilename' : true,
  35. 'isDescriptionInFilename' : false,
  36. 'isNoteInFilename' : false,
  37. 'isCategoryInFilename' : false,
  38. 'isBookmark' : false,
  39. 'fontsize': "2.2"
  40. };
  41.  
  42. // Creating style for a download icon
  43. var downloadTooltip = 'Download as html';
  44. var bookmarkTooltip = 'Check as read';
  45. var bookmarkCheckTooltip = 'Un-check as read';
  46. var iconDonwload = '<i class="bi bi-arrow-down-circle" style="cursor: pointer;cursor: hand;margin-right:5px; font-size:14px"></i>';
  47. var iconBookmark = '<i class="bi bi-bookmark" style="cursor: pointer;cursor: hand;margin-right:5px; font-size:14px"></i>';
  48. var iconBookmarkCheck = '<i class="bi bi-bookmark-check-fill" style="cursor: pointer;cursor: hand;margin-right:5px; font-size:14px"></i>';
  49. var PREFIX_URL_PAGE = 'https://www.literotica.com';
  50. /**
  51. * Style HTML downloaded
  52. */
  53. var chapterStyle = '_style="line-height: 1.4em;" ';
  54. var descriptionStyle = ' style="line-height: 1.2em;" ';
  55.  
  56. /*
  57. * Constante for Filename
  58. */
  59. var SEPARATOR = '_';
  60. var PREFIX_NOTE = 'RATING_';
  61. var POSTFIX_NUMER_CHAPTER ='_Part_Series';
  62.  
  63. /*
  64. * Constant For logging
  65. */
  66. var PREFIX_LOG='[Literotica Downloader]';
  67.  
  68. function calculateBodyStyle(){
  69. var toReturn = " style=\"";
  70. if(options.isNightMode){
  71. toReturn +="background-color:#333333; color: #EEEEEE;";
  72. }
  73. toReturn +="font-family: Helvetica,Arial,sans-serif; width: 50%; margin: 0 auto; line-height: 1.5em;";
  74. toReturn +="font-size:"+options.fontsize+"em; padding: 50px 0 50px 0;";
  75. toReturn +="\"";
  76. return toReturn;
  77. }
  78.  
  79.  
  80. // Create link elements
  81. const link1 = document.createElement('link');
  82. link1.href = 'https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css';
  83. link1.rel = 'stylesheet';
  84. link1.type = 'text/css';
  85.  
  86. const link2 = document.createElement('link');
  87. link2.href = 'https://cdn.jsdelivr.net/npm/bootstrap-icons@1.4.1/font/bootstrap-icons.css';
  88. link2.rel = 'stylesheet';
  89.  
  90. const styleElement = document.createElement('style');
  91.  
  92. // Set the type attribute
  93. styleElement.type = 'text/css';
  94.  
  95. // Set the CSS rules
  96. styleElement.textContent = '.ldText{background-color: lightgray;width: 3em;},.ldChapter,.ldSerie {margin-left: auto;} ._works_wrapper_29o2p_1 ._works_item__series_expanded_parts_29o2p_40{margin-left: 0px !important} .lbIconOnStoryCardComponent {float:left}';
  97.  
  98. // Append link elements to the head
  99. document.head.appendChild(link1);
  100. document.head.appendChild(link2);
  101. document.head.appendChild(styleElement);
  102.  
  103. console.log("Literotica Downloader start addEventListener");
  104.  
  105. window.addEventListener("load", function() {
  106. 'use strict';
  107. setTimeout(function() {
  108. const GMinfo = GM_info;
  109. const handlerInfo = GMinfo.scriptHandler;
  110. const isGM = Boolean(handlerInfo.toLowerCase() === 'greasemonkey');
  111. var panelOptionDownloader= `
  112. <div id="literoticaDownloaderOption" title="Options Literotica Downloader" >
  113. Please Configure the options :
  114. <fieldset>
  115. <legend>Interface: </legend>
  116. <div>
  117. <input type="checkbox" name="isBookmark" id="isBookmark" class="ldCheckbox">
  118. <span>Bookmark (locale storage)</span>
  119. </div>
  120. <div>Please refresh the page for thoses options</div>
  121. </fieldset>
  122. <fieldset>
  123. <legend>Reading: </legend>
  124. <div>
  125. <input type="checkbox" name="isNightMode" id="isNightMode" class="ldCheckbox">
  126. <span>Night Mode</span>
  127. </div>
  128. </fieldset>
  129. <fieldset>
  130. <legend>Filename: </legend>
  131. <div>
  132. <input type="checkbox" name="isNumberChapterInFilename" id="isNumberChapterInFilename" class="ldCheckbox">
  133. <span>Number of chapters in filename (for series)</span>
  134. </div>
  135. <div>
  136. <input type="checkbox" name="isUsernameInFilename" id="isUsernameInFilename" class="ldCheckbox">
  137. <span>Username in filename</span>
  138. </div>
  139. <div>
  140. <input type="checkbox" name="isDescriptionInFilename" id="isDescriptionInFilename" class="ldCheckbox">
  141. <span>Description in filename (Not on series)</span>
  142. </div>
  143. <div>
  144. <input type="checkbox" name="isNoteInFilename" id="isNoteInFilename" class="ldCheckbox">
  145. <span>Rating in filename (Not on series)</span>
  146. </div>
  147. <div>
  148. <input type="checkbox" name="isCategoryInFilename" id="isCategoryInFilename" class="ldCheckbox">
  149. <span>Category in filename (Not on series)</span>
  150. </div>
  151. <div>
  152. <span>FontSize (in em with . for decimal)</span>
  153. <input type="text" name="fontsize" id="fontsize" class="ldText">
  154. </div>
  155. </fieldset>
  156. </div>
  157. `;
  158. document.body.insertAdjacentHTML('beforeend',panelOptionDownloader);
  159. var ldCheckboxes = document.querySelectorAll('.ldCheckbox');
  160.  
  161. ldCheckboxes.forEach(function(checkbox) {
  162. checkbox.addEventListener('change', async function() {
  163. console.log(this.id);
  164. GMsetValue(this.id, this.checked);
  165. console.log(PREFIX_LOG+'new value =>' + await GMgetValue(this.id));
  166. options[this.id] = this.checked;
  167. });
  168. });
  169. var ldInputTexts = document.querySelectorAll('.ldText');
  170.  
  171. ldInputTexts.forEach(function(checkbox) {
  172. checkbox.addEventListener('change', async function() {
  173. console.log(this.id);
  174. GMsetValue(this.id, this.value);
  175. console.log('new value =>' + await GMgetValue(this.id));
  176. options[this.id] = this.value;
  177. console.log(options);
  178. });
  179. });
  180. console.log(PREFIX_LOG+"Start init dialog option");
  181. $( "#literoticaDownloaderOption" ).dialog({ autoOpen: false, width: 450 });
  182. console.log(PREFIX_LOG+"Snd init dialog option");
  183. if (isGM) {
  184. GMsetValue = GM.setValue;
  185. GMgetValue = GM.getValue;
  186. GMregisterMenuCommand = GM.registerMenuCommand;
  187. GMunregisterMenuCommand = function() {};
  188. GMnotification = GM.notification;
  189. console.log(PREFIX_LOG+"It's GM !");
  190. } else {
  191. console.log(PREFIX_LOG+"Other than GM...");
  192.  
  193. GMsetValue = GM_setValue;
  194. GMgetValue = GM_getValue;
  195. GMregisterMenuCommand = GM_registerMenuCommand;
  196. GMunregisterMenuCommand = GM_unregisterMenuCommand;
  197. }
  198. initOptions();
  199.  
  200. // Open dialog when the menu command is clicked
  201. GMregisterMenuCommand("Literotica Downloader Options", function() {
  202. $( "#literoticaDownloaderOption" ).dialog( "open" );
  203. });
  204.  
  205. // Dialog setup
  206. var dialog = document.getElementById('literoticaDownloaderOption');
  207. dialog.style.display = 'none'; // Initially hide the dialog
  208.  
  209. /*
  210. * Download Button for Chapter
  211. */
  212. addDownloadChapterButtonTo(document);
  213. /*
  214. * Download Button for Series
  215. */
  216. addDownloadSeriesButtonTo(document);
  217.  
  218. /*
  219. * Download Button for individual Story
  220. */
  221. addDownloadIndividualStoryButtonTo(document);
  222. /*
  223. * Section download on tags
  224. */
  225. document.addEventListener('DOMNodeInserted', function(e) {
  226. if (e.target!= null && e.target.classList != null && e.target.classList.contains('ai_gJ')) {
  227. var target = e.target;
  228.  
  229. var ai_iGElements = target.querySelectorAll('.ai_iG');
  230. if(ai_iGElements){
  231. ai_iGElements.forEach(function(element) {
  232. element.insertAdjacentHTML('afterbegin', '<span class="ldDownloadTag lbIconOnStoryCardComponent">' + iconDonwload + '</span>');
  233. });
  234.  
  235. target.querySelectorAll('.ldDownloadTag.lbIconOnStoryCardComponent')
  236. .forEach(function(tag) {
  237. tag.addEventListener('click', function() {
  238. getABookForStoryOnStoryCardComponent(tag.parentNode.parentNode);
  239. });
  240. });
  241. }
  242. }
  243. });
  244. /*
  245. * Section download on category
  246. */
  247. console.log(PREFIX_LOG+"init category");
  248. document.querySelectorAll('.b-slb-item').forEach(function(element) {
  249. element.insertAdjacentHTML('afterbegin', '<span class="ldChapter lbIconOnStoryCardComponent">' + iconDonwload + '</span>');
  250. });
  251. document.querySelectorAll(".ldChapter.lbIconOnStoryCardComponent").forEach(function(tag) {
  252. tag.addEventListener('click', function(tag) {
  253. getABookForStoryItemCategory(tag.target.parentNode.parentNode);
  254. });
  255. });
  256. }, 250); // 1000 milliseconds = 1 second
  257. });
  258. function addDownloadSeriesButtonTo(parentElement){
  259. const titlesSeries = parentElement.querySelectorAll("[class*='_works_item__series_expanded_header_card'] [class*='_works_item__title'] [class*='_item_title']");
  260. titlesSeries.forEach(element => {
  261. // Create a new element to prepend
  262. const newElement = document.createElement('div');
  263. newElement.textContent = 'Prepended Element'; // Change this to whatever content you want
  264. var idIcon = Math.floor(Math.random() * 1000);
  265. // Prepend the new element before the selected element
  266. element.insertAdjacentHTML('beforebegin', '<span id="' + idIcon + '" class="ldSerie"> ' + iconDonwload + '</span>');
  267. element.classList.add("lbButtonAdded");
  268. });
  269.  
  270. parentElement.querySelectorAll('.ldSerie').forEach(element => {
  271. element.addEventListener('click', function() {getABookForSerieDiv(element.parentNode.parentNode)});
  272. });
  273. }
  274. function addDownloadChapterButtonTo(parentElement){
  275. const titlesChapter = parentElement.querySelectorAll("[class*='_series_parts__item_'] [class*='_works_item__title'] [class*='item_title']");
  276. titlesChapter.forEach(element => {
  277. // Create a new element to prepend
  278. const newElement = document.createElement('div');
  279. newElement.textContent = 'Prepended Element'; // Change this to whatever content you want
  280. var idIcon = Math.floor(Math.random() * 1000);
  281. // Prepend the new element before the selected element
  282. element.insertAdjacentHTML('beforebegin', '<span id="' + idIcon + '" class="ldChapter"> ' + iconDonwload + '</span>');
  283. element.classList.add("lbButtonAdded");
  284. });
  285.  
  286. parentElement.querySelectorAll('.ldChapter').forEach(element => {
  287. element.addEventListener('click', function() {getABookForStoryDiv(element.parentNode)});
  288. });
  289. }
  290. function addDownloadIndividualStoryButtonTo(parentElement){
  291. const titlesChapter = parentElement.querySelectorAll("[class*='_works_item__title'] [class*='item_title']:not(.lbButtonAdded)");
  292. titlesChapter.forEach(element => {
  293. // Create a new element to prepend
  294. const newElement = document.createElement('div');
  295. newElement.textContent = 'Prepended Element'; // Change this to whatever content you want
  296. var idIcon = Math.floor(Math.random() * 1000);
  297. // Prepend the new element before the selected element
  298. element.insertAdjacentHTML('beforebegin', '<span id="' + idIcon + '" class="ldChapter"> ' + iconDonwload + '</span>');
  299. element.classList.add("lbButtonAdded");
  300. });
  301.  
  302. parentElement.querySelectorAll('.ldChapter').forEach(element => {
  303. element.addEventListener('click', function() {getABookForStoryDiv(element.parentNode)});
  304. });
  305. }
  306.  
  307. async function initOptions(){
  308. // Init of all options
  309. for(const element of Object.getOwnPropertyNames(options)){
  310. await initCheckBox(element);
  311. await initInputText(element);
  312. }
  313. }
  314. async function initCheckBox(element, index, array) {
  315. options[element] = await GMgetValue(element,options[element]);
  316. $("#"+element)[0].checked= options[element];
  317. return true;
  318. }
  319. async function initInputText(element, index, array) {
  320. options[element] = await GMgetValue(element,options[element]);
  321. $("#"+element)[0].value= options[element];
  322. console.log(PREFIX_LOG+element+"-> "+options[element]);
  323. return true;
  324. }
  325.  
  326. function getABookForStoryItemCategory(cardComponent) {
  327. console.log(PREFIX_LOG+"In getABookForStoryItemCategory");
  328. console.log(cardComponent);
  329. var link = cardComponent.querySelector('h3 a').getAttribute('href');
  330. var storyId = extractStoryId(link);
  331. var jsonStory = getJsonStoryByStoryId(extractStoryId(link));
  332. var title = jsonStory.submission.title;
  333. var note = jsonStory.submission.rate_all;
  334. var chapterTitle = jsonStory.submission.title;
  335. var description = jsonStory.submission.description;
  336. var category = jsonStory.submission.category_info.pageUrl;
  337. var author = jsonStory.submission.author.username;
  338. getABookForStory(storyId, title, note, chapterTitle, description, category, author);
  339. }
  340.  
  341. function getABookForStoryOnStoryCardComponent(cardComponent) {
  342. console.log(PREFIX_LOG+"In getABookForStoryOnStoryCardComponent");
  343. console.log(cardComponent);
  344. var link = cardComponent.querySelector('.ai_ii').getAttribute('href');
  345. var storyId = extractStoryId(link);
  346. var jsonStory = getJsonStoryByStoryId(extractStoryId(link));
  347. var title = jsonStory.submission.title;
  348. var note = jsonStory.submission.rate_all;
  349. var chapterTitle = jsonStory.submission.title;
  350. var description = jsonStory.submission.description;
  351. var category = jsonStory.submission.category_info.pageUrl;
  352. var author = jsonStory.submission.author.username;
  353. getABookForStory(storyId, title, note, chapterTitle, description, category, author);
  354. }
  355.  
  356. function openFullViewForDownloadSerie(){
  357. var moreExist= false;
  358. document.querySelectorAll("[class*='_show_more']").forEach(element => {
  359. if(element.innerText.trim().startsWith('View Full')){
  360. moreExist = true;
  361. console.log(PREFIX_LOG+"Opening "+element);
  362. element.click();
  363. }
  364. });
  365. if(moreExist){
  366. setTimeout(function() {
  367. openFullViewForDownloadSerie();
  368. }, 250);
  369. }
  370. }
  371. function extractSerieId(url) {
  372. // Split the URL by slashes
  373. const parts = url.split('/');
  374. // Get the last part of the array
  375. const lastPart = parts[parts.length - 1];
  376. return lastPart;
  377. }
  378. function getSerieLinkFormDiv(serieDiv){
  379. return serieDiv.querySelectorAll("[class*='_item_title_']")[0].href;
  380. }
  381. function getABookForSerieDiv(serieDiv) {
  382. console.log(PREFIX_LOG+"Processing series");
  383. console.log(serieDiv);
  384. openFullViewForDownloadSerie();
  385. var link = getSerieLinkFormDiv(serieDiv);
  386. var serieId = extractSerieId(link);
  387. var jsonSerie = getJsonSerieBySerieId(serieId).data;
  388. var title = jsonSerie.title;
  389. // Get the X Part Series
  390. var descriptionSeries = ""
  391. var author = jsonSerie.user.username;
  392. var numberChapter = jsonSerie.work_count;
  393. alert("Starting building file for " + title + " of " + author + " with "+numberChapter+".\nPlease wait...");
  394.  
  395. var book = '<html>\n<head>\n<meta content="text/html; charset=UTF-8" http-equiv="Content-Type">\n';
  396. book += '<title>' + title + '</title>';
  397. book += '<meta content="' +author+ '" name="author">';
  398. book += '</head>\n<body ' + calculateBodyStyle() + ' >';
  399.  
  400. function addChapter(itemChapter) {
  401. console.log(PREFIX_LOG+"addChapter"+itemChapter);
  402. var storyId = itemChapter.url;
  403. var jsonStory = getJsonStoryByStoryId(storyId);
  404. var chapterTitle = jsonStory.submission.title;
  405. var description = jsonStory.submission.description;
  406. book += '<h1 class=\'chapter\'' + chapterStyle + '>' + chapterTitle + '</h1>';
  407. book += '<h2 class=\'chapter\'' + descriptionStyle + '>' + description + '</h2>';
  408. book += getContentOfStory(storyId);
  409. }
  410. console.log(serieDiv.nextElementSibling);
  411. var allChapter = serieDiv.nextElementSibling.querySelectorAll("[class*='_series_parts__item_'][class*='_works_item_']")
  412. var lastChapter = allChapter[allChapter.length - 1];
  413. console.log(lastChapter);
  414. var linkLastChapter = getChapterLinkFormDiv(lastChapter);
  415. console.log(PREFIX_LOG+"linkLastChapter => "+linkLastChapter);
  416. var storyId = extractStoryId(linkLastChapter);
  417. var jsonStory = getJsonStoryByStoryId(storyId);
  418. if(jsonStory.submission.series.items != null){
  419. jsonStory.submission.series.items.forEach(element => {
  420. console.log(element);
  421. // Your logic to perform on each element
  422. addChapter(element);
  423. });
  424. }else{
  425. console.log(PREFIX_LOG+"Last Chapter doesn't have series informations");
  426. console.log(PREFIX_LOG+"Falback on previous one");
  427. console.log(allChapter);
  428. lastChapter = allChapter[allChapter.length - 2];
  429. console.log(lastChapter);
  430. linkLastChapter = getChapterLinkFormDiv(lastChapter);
  431. console.log(PREFIX_LOG+"linkLastChapter => "+linkLastChapter);
  432. storyId = extractStoryId(linkLastChapter);
  433. jsonStory = getJsonStoryByStoryId(storyId);
  434. jsonStory.submission.series.items.forEach(element => {
  435. console.log(element);
  436. // Your logic to perform on each element
  437. addChapter(element);
  438. });
  439. }
  440. saveTextAsFile(book, buildFilename(title, author, descriptionSeries, null, null, numberChapter));
  441. }
  442. function getChapterLinkFormDiv(storyDiv){
  443. return storyDiv.querySelectorAll("[class*='_item_title_']")[0].href;
  444. }
  445.  
  446. function getABookForStoryDiv(storyDiv){
  447. console.log(PREFIX_LOG+"Processing getABookForStoryDiv");
  448. console.log(storyDiv);
  449. var link = getChapterLinkFormDiv(storyDiv);
  450. var storyId = extractStoryId(link);
  451. var jsonStory = getJsonStoryByStoryId(extractStoryId(link));
  452. var title = jsonStory.submission.title;
  453. var note = jsonStory.submission.rate_all;
  454. var chapterTitle = jsonStory.submission.title;
  455. var description = jsonStory.submission.description;
  456. var category = jsonStory.submission.category_info.pageUrl;
  457. var author = jsonStory.submission.author.username;
  458. getABookForStory(storyId, title, note, chapterTitle, description, category, author);
  459. }
  460.  
  461. function getABookForStory(storyId, title, note, chapterTitle, description, category, author) {
  462. var book = '<html>\n<head>\n<meta content="text/html; charset=UTF-8" http-equiv="Content-Type">\n';
  463. console.log(PREFIX_LOG+'title' + title);
  464. book += '<title>' + title + '</title>';
  465. book += '<meta content="' + author + '" name="author">';
  466. book += '</head>\n<body ' + calculateBodyStyle() + ' >';
  467. if(chapterTitle != null){
  468. book += '<h1 class=\'chapter\'' + chapterStyle + '>' + chapterTitle + '</h1>';
  469. }
  470. if(description != null){
  471. book += '<h2 class=\'chapter\'' + descriptionStyle + '>' + description + '</h2>';
  472. }
  473. book += getContentOfStory(storyId);
  474.  
  475. saveTextAsFile(book, buildFilename(title, author, description, note, category, null));
  476. }
  477.  
  478. function extractStoryId(url) {
  479. // Split the URL by slashes
  480. const parts = url.split('/');
  481. // Get the last part of the array
  482. const lastPart = parts[parts.length - 1];
  483. return lastPart;
  484. }
  485. function getContentOfStoryOfPage(storyId, current_page) {
  486. var apiURL = "https://literotica.com/api/3/stories/"+storyId+'?params=%7B"contentPage"%3A'+current_page+'%7D';
  487. var xhr = new XMLHttpRequest();
  488. var toReturn ="[Page "+current_page+" not Found]";
  489. xhr.open('GET', apiURL, false); // false makes the request synchronous
  490. xhr.onload = function() {
  491. if (xhr.status >= 200 && xhr.status < 300) {
  492. const response = JSON.parse(xhr.responseText);
  493. toReturn = response.pageText;
  494. } else {
  495. console.error(PREFIX_LOG+'Request failed with status ' + xhr.status);
  496. }
  497. };
  498. xhr.onerror = function() {
  499. console.error(PREFIX_LOG+'Request failed');
  500. };
  501. xhr.send();
  502. return toReturn;
  503. }
  504.  
  505. function getJsonSerieBySerieId(serieId){
  506. var apiURL = "https://literotica.com/api/3/series/"+serieId;
  507. console.log(PREFIX_LOG+"Fetching on API -> " + apiURL);
  508. var toReturn;
  509. var xhr = new XMLHttpRequest();
  510. xhr.open('GET', apiURL, false); // false makes the request synchronous
  511. xhr.onload = function() {
  512. if (xhr.status >= 200 && xhr.status < 300) {
  513. toReturn = JSON.parse(xhr.responseText);
  514. console.log(toReturn);
  515. } else {
  516. console.error(PREFIX_LOG+'Request failed with status ' + xhr.status);
  517. }
  518. };
  519. xhr.onerror = function() {
  520. console.error(PREFIX_LOG+'Request failed');
  521. };
  522. xhr.send();
  523. return toReturn;
  524. }
  525. function getJsonStoryByStoryId(storyId){
  526. var apiURL = "https://literotica.com/api/3/stories/"+storyId;
  527. console.log(PREFIX_LOG+"Fetching on API -> " + apiURL);
  528. var toReturn;
  529. var xhr = new XMLHttpRequest();
  530. xhr.open('GET', apiURL, false); // false makes the request synchronous
  531. xhr.onload = function() {
  532. if (xhr.status >= 200 && xhr.status < 300) {
  533. toReturn = JSON.parse(xhr.responseText);
  534. console.log(toReturn);
  535. } else {
  536. console.error(PREFIX_LOG+'Request failed with status ' + xhr.status);
  537. }
  538. };
  539. xhr.onerror = function() {
  540. console.error(PREFIX_LOG+'Request failed');
  541. };
  542. xhr.send();
  543. return toReturn;
  544. }
  545.  
  546. function convertTextToHTML(text) {
  547. // Replace CRLF with <br> tags
  548. var htmlText = text.replace(/\r\n/g, "<br>");
  549. // Replace LF with <br> tags
  550. htmlText = htmlText.replace(/\n/g, "<br>");
  551. // Replace CR with <br> tags
  552. htmlText = htmlText.replace(/\r/g, "<br>");
  553. return htmlText;
  554. }
  555.  
  556. // Function parsing all pages to get the storie based
  557. function getContentOfStory(storyId) {
  558. console.log(PREFIX_LOG+"Processing -> " + storyId);
  559. var apiURL = "https://literotica.com/api/3/stories/"+storyId;
  560. console.log(PREFIX_LOG+"Fetching on API -> " + apiURL);
  561. var toReturn;
  562. var xhr = new XMLHttpRequest();
  563. xhr.open('GET', apiURL, false); // false makes the request synchronous
  564. xhr.onload = function() {
  565. if (xhr.status >= 200 && xhr.status < 300) {
  566. const response = JSON.parse(xhr.responseText);
  567. toReturn = response.pageText;
  568. var pageCount = response.meta.pages_count;
  569. console.log(PREFIX_LOG+"pageCount => "+pageCount);
  570. if (pageCount > 0) {
  571. for (let currentPage = 2; currentPage <= pageCount; currentPage++) {
  572. const result = getContentOfStoryOfPage(storyId, currentPage); // Call the given function
  573. toReturn += result; // Concatenate the result to toReturn
  574. }
  575. }
  576. } else {
  577. console.error(PREFIX_LOG+'Request failed with status ' + xhr.status);
  578. }
  579. };
  580. xhr.onerror = function() {
  581. console.error(PREFIX_LOG+'Request failed');
  582. };
  583. xhr.send();
  584. toReturn = convertTextToHTML(toReturn);
  585. return toReturn;
  586. }
  587. // Function used to return content as a file for the user.
  588. function saveTextAsFile(textToWrite, fileNameToSaveAs) {
  589. var textFileAsBlob = new Blob([textToWrite], {
  590. type: 'text/javascript'
  591. });
  592. var downloadLink = document.createElement('a');
  593. downloadLink.download = fileNameToSaveAs;
  594. downloadLink.innerHTML = 'Download File';
  595. // Firefox requires the link to be added to the DOM
  596. // before it can be clicked.
  597. downloadLink.href = window.URL.createObjectURL(textFileAsBlob);
  598. //downloadLink.onclick = destroyClickedElement;
  599. downloadLink.style.display = 'none';
  600. document.body.appendChild(downloadLink);
  601. downloadLink.click();
  602. }
  603.  
  604. function buildFilename(title, author, description, note, category, numberChapter) {
  605. var toReturn = title;
  606. if (options.isNumberChapterInFilename) {
  607. toReturn = toReturn + SEPARATOR + numberChapter + POSTFIX_NUMER_CHAPTER;
  608. }
  609. if (options.isUsernameInFilename) {
  610. toReturn = toReturn + SEPARATOR + author;
  611. }
  612. if (options.isDescriptionInFilename) {
  613. if (description != null && description != "") {
  614. toReturn = toReturn + SEPARATOR + description.replace(/[^\w\s]/gi, '');
  615. }
  616. }
  617. if (options.isNoteInFilename) {
  618. if (note != null && note != "") {
  619. toReturn = toReturn + SEPARATOR + PREFIX_NOTE + note;
  620. }
  621. }
  622. if (options.isCategoryInFilename) {
  623. if (category != null && category != "") {
  624. toReturn = toReturn + SEPARATOR + category;
  625. }
  626. }
  627. // Add file extension;
  628. toReturn = toReturn + '.html';
  629. return toReturn;
  630. }
  631.  
  632. function hasNodeClassStartingWith(node, startclass) {
  633. const classList = node.classList;
  634.  
  635. for (let i = 0; i < classList.length; i++) {
  636. if (classList[i].startsWith(startclass)) {
  637. return true;
  638. }
  639. }
  640.  
  641. return false;
  642. }
  643.  
  644. // Callback function to be called when an element is added to the DOM
  645. function handleElementAdded(mutationsList, observer) {
  646. for(var mutation of mutationsList) {
  647. if (mutation.type === 'childList') {
  648. // Check if nodes were added
  649. mutation.addedNodes.forEach(function(node) {
  650. if (node.nodeType === 1 &&
  651. ( hasNodeClassStartingWith(node,'_series_parts__item')
  652. || hasNodeClassStartingWith(node,'_series_parts__wrapper')
  653. || hasNodeClassStartingWith(node,'_works_wrapper_')
  654. || hasNodeClassStartingWith(node,'_works_item_')
  655. )
  656. ) {
  657. //console.log(PREFIX_LOG+'Element added:', node);
  658. addDownloadChapterButtonTo(node);
  659. addDownloadSeriesButtonTo(node);
  660. addDownloadIndividualStoryButtonTo(node);
  661. // Call your function here or do any other necessary action
  662. }
  663. });
  664. }
  665. }
  666. }
  667.  
  668. // Create a MutationObserver instance
  669. var observer = new MutationObserver(handleElementAdded);
  670.  
  671. // Define the options for the observer (in this case, we're observing changes to the child list)
  672. var observerConfig = {
  673. childList: true,
  674. subtree: true // This option allows us to observe changes within the entire subtree of the target node
  675. };
  676.  
  677. // Start observing the target node (in this example, we're observing the entire document)
  678. observer.observe(document, observerConfig);