LegalPorno Enhancer

Adds extra functionality to LegalPorno and Pornbox. Easily filter out unwanted categories. Also adds tags on top of every scene.

As of 02/04/2020. See the latest version.

  1. // ==UserScript==
  2. // @name LegalPorno Enhancer
  3. // @namespace https://www.legalporno.com/forum/viewtopic.php?f=96&t=24238
  4. // @description Adds extra functionality to LegalPorno and Pornbox. Easily filter out unwanted categories. Also adds tags on top of every scene.
  5. // @match http*://*.legalporno.com/*
  6. // @match http*://legalporno.com/*
  7. // @exclude http*://*legalporno.com/forum/*
  8. // @exclude http*://account.legalporno.com/*
  9. // @match https://*pornbox.com/*
  10. // @version 1.2.7
  11. // @grant GM_getValue
  12. // @grant GM_setValue
  13. // @grant GM_addStyle
  14. // @run-at document-start
  15. // ==/UserScript==
  16.  
  17. const is_lp = !!window.location.href.match(/legalporno/);
  18.  
  19. let glo_filters = {
  20. studios: {
  21. 'Gonzo.com': true,
  22. 'Giorgio Grandi': true,
  23. 'Interracial Vision': true,
  24. 'American Anal': true,
  25. "Giorgio's Lab": true,
  26. Focus: true,
  27. Assablanca: true,
  28. 'Mr Anal': true,
  29. 'Porn World': true,
  30. 'Girls Gone Wild': true,
  31. 'Kinky Sex': true,
  32. 'Outside the Studio': true
  33. },
  34. categories: {
  35. Piss: true,
  36. 'Piss Drink': true,
  37. Fisting: true,
  38. Prolapse: true,
  39. Shemale: true
  40. }
  41. };
  42.  
  43. const studio_colors = {
  44. 'Gonzo.com': { bg: '#cd0000', text: '#fff' },
  45. 'Giorgio Grandi': { bg: '#693180', text: '#fff' },
  46. 'Interracial Vision': { bg: '#242424', text: '#e6e6e6' },
  47. 'American Anal': { bg: '#b45219', text: '#fffa78' },
  48. "Giorgio's Lab": { bg: '#206d04', text: '#fff' },
  49. 'Kinky Sex': { bg: '#000', text: '#fff' }
  50. };
  51.  
  52. // Uncomment the following line to reset global filters
  53. // GM_setValue('user_filters', JSON.stringify(glo_filters));
  54.  
  55. validateUserFilters = filters => {
  56. if (!filters.studios || !filters.categories) return false;
  57. if (
  58. Object.keys(filters.studios).length !==
  59. Object.keys(glo_filters.studios).length
  60. )
  61. return false;
  62. if (
  63. Object.keys(filters.categories).length !==
  64. Object.keys(glo_filters.categories).length
  65. )
  66. return false;
  67. if (
  68. Object.keys(filters.categories)
  69. .sort()
  70. .some(
  71. (val, idx) => val !== Object.keys(glo_filters.categories).sort()[idx]
  72. )
  73. )
  74. return false;
  75. if (
  76. Object.keys(filters.studios)
  77. .sort()
  78. .some((val, idx) => val !== Object.keys(glo_filters.studios).sort()[idx])
  79. )
  80. return false;
  81. return true;
  82. };
  83.  
  84. try {
  85. const user_config = GM_getValue('user_filters');
  86. if (!user_config)
  87. throw new Error('No user filters found. Using default ones.');
  88. user_filters = JSON.parse(user_config);
  89. if (!validateUserFilters(user_filters))
  90. throw new Error('Invalid user settings.');
  91. glo_filters = user_filters;
  92. } catch (e) {}
  93.  
  94. const studios = {
  95. Interracial: 'Interracial Vision',
  96. 'Hard Porn World': 'Porn World'
  97. };
  98.  
  99. const icon_categories = {
  100. interracial: 'IR',
  101. 'double anal (DAP)': 'DAP',
  102. fisting: 'Fisting',
  103. prolapse: 'Prolapse',
  104. transsexual: 'Shemale',
  105. trans: 'Shemale',
  106. 'triple anal (TAP)': 'TAP',
  107. piss: 'Piss',
  108. squirting: 'Squirt',
  109. '3+ on 1': '3+on1',
  110. 'double vaginal (DPP)': 'DPP',
  111. 'first time': '1st',
  112. 'triple penetration': 'TP',
  113. '0% pussy': '0% Pussy',
  114. 'cum swallowing': 'Cum Swallow',
  115. 'piss drinking': 'Piss Drink',
  116. 'anal creampies': 'Anal Creampie',
  117. '1 on 1': '1on1',
  118. milf: 'Milf',
  119. 'facial cumshot': 'Facial'
  120. };
  121.  
  122. GM_addStyle(`
  123. .hiddenScene {
  124. display: none !important;
  125. }
  126.  
  127. .hide_element {
  128. display: none !important;
  129. }
  130.  
  131. .thumbnail-duration {
  132. top: 0 !important;
  133. right: 0 !important;
  134. left: unset !important;
  135. bottom: unset !important;
  136. margin: 5px !important;
  137. }
  138.  
  139. .enhanced_categories_container {
  140. width: 100%;
  141. display: flex;
  142. flex-wrap: wrap-reverse;
  143. position: absolute;
  144. bottom:0; left: 0;
  145. z-index: 3;
  146. padding: 3px 5px;
  147. }
  148.  
  149. .enchanced_icon {
  150. border-radius: 3px;
  151. background-color: #616161;
  152. color: #eee;
  153. opacity: 0.9;
  154. padding: 2px 7px;
  155. margin-right: 2px;
  156. margin-top: 2px;
  157. font-size: 13px;
  158. }
  159.  
  160. .enhanced--studio {
  161. position: absolute;
  162. top: 0;
  163. left: 0;
  164. z-index: 3;
  165. display: flex;
  166. margin: 4px 6px;
  167. }
  168.  
  169. .enhanced--studio {
  170. background-color: #a76523c9;
  171. border-radius: 3px;
  172. padding: 2px 7px;
  173. color: white;
  174. text-shadow: 1px 1px #00000080;
  175. font-size: 12px;
  176. margin-right: 4px;
  177. }
  178.  
  179. .enhanced_views_label {
  180. position: absolute;
  181. top: 28px;
  182. right: 5px;
  183. font-size: 11px;
  184. display: flex;
  185. }
  186.  
  187. .enhanced--views {
  188. background-color: rgba(102,102,102,0.6);
  189. padding: 2px 6px;
  190. border-radius: 3px;
  191. color: #fff;
  192. display: flex;
  193. height: 100%;
  194. align-items: center;
  195. justify-content: center;
  196. }
  197.  
  198. .enhanced--views > i {
  199. margin-right: 5px;
  200. }
  201.  
  202. .item__img:hover > div {
  203. display: none;
  204. }
  205.  
  206. .item__img-labels-bottom {
  207. top: 3px;
  208. right: 3px;
  209. left: unset;
  210. bottom: unset;
  211. display: flex
  212. }
  213.  
  214. .img-label {
  215. margin-top: unset;
  216. margin-left: 2px;
  217. }
  218.  
  219. .item__img-labels {
  220. display: flex;
  221. flex-direction: row-reverse;
  222. left: unset;
  223. right: 3px;
  224. top: 23px;
  225. }
  226.  
  227. /* Filter Css Begin */
  228. .filters_container_lp {
  229. margin-left: -15px;
  230. margin-right: -15px;
  231. }
  232.  
  233. .filters_container--main {
  234. width: 100%;
  235. display: flex;
  236. justify-content: center;
  237. background: linear-gradient(
  238. 0deg,
  239. #babcbc 0%,
  240. #dfe1e1 2%,
  241. #dfe1e1 98%,
  242. #babcbc 100%
  243. );
  244. }
  245.  
  246. .filters_card {
  247. margin: 0 25px;
  248. padding-bottom: 15px;
  249. display: flex;
  250. flex-direction: column;
  251. align-items: center;
  252. position: relative;
  253. }
  254.  
  255. .filters_selections {
  256. display: grid;
  257. grid-template-columns: repeat(2, 1fr);
  258. }
  259.  
  260. .filters--divider {
  261. width: 1px;
  262. background-color: rgb(0, 0, 0, 0.25);
  263. height: 50px;
  264. margin-top: 15px;
  265. align-self: center;
  266. }
  267.  
  268. .filters--selection {
  269. padding: 1px 10px 0 0;
  270. min-width: 140px;
  271. display: flex;
  272. align-items: center;
  273. }
  274.  
  275. .filters--selection > label,
  276. .filters--selection > input[type="checkbox"] {
  277. margin: 0 2px 0 0;
  278. font-weight: bold;
  279. }
  280.  
  281. .fitlers--header {
  282. padding: 5px 0 5px 0;
  283. align-self: center;
  284. font-weight: bold;
  285. }
  286.  
  287. .enhanced_toggle {
  288. background: -webkit-linear-gradient(
  289. top,
  290. #a9a9a9 0%,
  291. #d97575 5%,
  292. #563434 100%
  293. ) !important;
  294. // height: 100%;
  295. border: 1px solid #722424 !important;
  296. color: white;
  297. padding: 7px 11px;
  298. font-weight: 700;
  299. border: none !important;
  300. }
  301.  
  302. .enhanced_toggle_pb {
  303. height: 100%;
  304. padding: 7px 11px;
  305. border-radius: 300px;
  306. position: absolute;
  307. margin-left: 4px;
  308. }
  309.  
  310. .enhanced_toggle_lp {
  311. border-radius: 200px;
  312. margin-left: 4px;
  313. }
  314.  
  315. .enhanced_toggle:hover {
  316. cursor: pointer;
  317. }
  318.  
  319. .enhanced_toggle > i {
  320. font-size: 12px;
  321. }
  322. /* Filter Css End */
  323.  
  324. .enhanced--sort {
  325. display: grid;
  326. grid-template-columns: 1fr 1fr;
  327. justify-items: center;
  328. grid-gap: 2px;
  329. position: absolute;
  330.  
  331. top: -4px;
  332. right: 210px;
  333. }
  334.  
  335. .enhanced--sort--navbar {
  336. right: 25px;
  337. top: 6px;
  338. }
  339.  
  340. .enhanced--sort > span {
  341. grid-column-end: span 2;
  342. color: #fff;
  343. }
  344.  
  345. .enhanced--sort > button {
  346. width: 70px;
  347. height: 23px;
  348. justify-content: center;
  349. align-items: center;
  350. text-align: center;
  351. padding: 0;
  352. margin: 0;
  353. }
  354.  
  355. .page-section {
  356. position: relative !important;
  357. }
  358. `);
  359.  
  360. const getSceneInfo = scene => {
  361. const info = {
  362. studio: null,
  363. categories: null,
  364. date: null,
  365. views: null
  366. };
  367.  
  368. info.studio = scene.querySelector('.rating-studio-name, a[href^="#studio/"]');
  369. info.studio = info.studio ? info.studio.innerText : null;
  370. if (studios[info.studio]) {
  371. info.studio = studios[info.studio];
  372. }
  373. info.categories = [...scene.querySelectorAll('a[href*="niche/"]')]
  374. .filter(cat => cat.innerText && icon_categories[cat.innerText])
  375. .map(cat => icon_categories[cat.innerText])
  376. .sort();
  377. // info.categories = info.categories.filter(
  378. // cat => !(cat === 'Piss' && info.categories.includes('Piss Drink'))
  379. // );
  380. info.categories = Array.from(new Set(info.categories));
  381.  
  382. const views = scene.querySelector('.rating-views');
  383. const date = scene.querySelector('.glyphicon-calendar');
  384.  
  385. if (views) {
  386. info.views = parseInt(views.innerText.replace('VIEWS:', '').trim(), 10);
  387. }
  388. if (date) {
  389. try {
  390. info.date = new Date(
  391. date.parentElement.innerText.replace('RELEASE:', '').trim()
  392. );
  393. } catch (e) {}
  394. }
  395.  
  396. return info;
  397. };
  398.  
  399. const formatViews = n => {
  400. if (n >= 1e3 && n < 1e6) return +(n / 1e3).toFixed(0) + 'k';
  401. if (n >= 1e6 && n < 1e9) return +(n / 1e6).toFixed(0) + 'm';
  402. if (n >= 1e9 && n < 1e12) return +(n / 1e9).toFixed(0) + 'b';
  403. // if (n >= 1e12) return +(n / 1e12).toFixed(1) + "T";
  404. return n;
  405. };
  406.  
  407. const enhanceScene = scene => {
  408. // Remove icons from thumnail. ("4k" and "new")
  409. const icons = scene.querySelectorAll('.icon');
  410. for (const icon of icons) {
  411. icon.classList.add('hide_element');
  412. }
  413.  
  414. const { studio, categories, views, date } = getSceneInfo(scene);
  415.  
  416. if (studio) {
  417. const studioLabel = document.createElement('div');
  418. studioLabel.classList.add('enhanced--studio');
  419. studioLabel.innerHTML = studio;
  420.  
  421. if (studio_colors[studio]) {
  422. studioLabel.style = `
  423. background-color: ${studio_colors[studio]['bg']}; color: ${studio_colors[studio]['text']};
  424. `;
  425. }
  426.  
  427. if (views) {
  428. const viewsLabel = document.createElement('div');
  429. viewsLabel.innerHTML = `
  430. <div class='enhanced--views'><i class='fa fa-eye'></i>${formatViews(
  431. views
  432. )}</div>`;
  433. viewsLabel.classList.add('enhanced_views_label');
  434. if (scene.querySelector('.thumbnail-image')) {
  435. scene.querySelector('.thumbnail-image').appendChild(viewsLabel);
  436. }
  437. }
  438.  
  439. // studioLabel.classList.add('enhanced_title_tags');
  440. scene
  441. .querySelector('.thumbnail-image, .item__img')
  442. .appendChild(studioLabel);
  443. }
  444. if (categories.length) {
  445. const cat_div = document.createElement('div');
  446. cat_div.classList.add('enhanced_categories_container');
  447. for (const cat of categories) {
  448. const icon = document.createElement('div');
  449. icon.classList.add('enchanced_icon');
  450. icon.innerHTML = cat;
  451. cat_div.appendChild(icon);
  452. }
  453.  
  454. if (scene.querySelector('.thumbnail-image, .item__img')) {
  455. scene.querySelector('.thumbnail-image, .item__img').appendChild(cat_div);
  456. }
  457. }
  458. };
  459.  
  460. const isSceneFiltered = scene => {
  461. const { studio, categories, views, date } = getSceneInfo(scene);
  462.  
  463. for (const key of Object.keys(glo_filters.studios)) {
  464. if (studio && studio.includes(key) && !glo_filters.studios[key])
  465. return true;
  466. }
  467.  
  468. for (const key of Object.keys(glo_filters.categories)) {
  469. if (categories && categories.includes(key) && !glo_filters.categories[key])
  470. return true;
  471. }
  472.  
  473. return false;
  474. };
  475.  
  476. const filterScene = scene => {
  477. if (isSceneFiltered(scene)) {
  478. scene.classList.add('hiddenScene');
  479. }
  480. };
  481.  
  482. const updateFilters = () => {
  483. GM_setValue('user_filters', JSON.stringify(glo_filters));
  484. const scenes = document.querySelectorAll('.thumbnail, .block-item');
  485. for (const scene of scenes) {
  486. if (scene.classList.contains('hiddenScene'))
  487. scene.classList.remove('hiddenScene');
  488. filterScene(scene);
  489. }
  490. };
  491.  
  492. const createFilterElement = () => {
  493. const { studios, categories } = glo_filters;
  494.  
  495. const studioHtml = Object.keys(studios).reduce(
  496. (acc, studio) =>
  497. (acc += `<div class="filters--selection">
  498. <input type="checkbox" name="studios" id="${studio}"
  499. ${studios[studio] ? 'checked="checked"' : ''}
  500. />
  501. <label for="${studio}">${studio}</label>
  502. </div>`),
  503. ''
  504. );
  505.  
  506. const categoriesHtml = Object.keys(categories).reduce(
  507. (acc, cat) =>
  508. (acc += `<div class="filters--selection">
  509. <input type="checkbox" name="categories" id="${cat}"
  510. ${categories[cat] ? 'checked="checked"' : ''}
  511. />
  512. <label for="${cat}">${cat}</label>
  513. </div>`),
  514. ''
  515. );
  516.  
  517. const form = document.createElement('form');
  518. form.classList.add('filters_container', 'hide_element');
  519. if (is_lp) form.classList.add('filters_container_lp');
  520. form.innerHTML = `<div class="filters_container--main">
  521. <div class="filters_card">
  522. <div class="fitlers--header">Studios:</div>
  523. <div class="filters_selections">
  524. ${studioHtml}
  525. </div>
  526. </div>
  527. <!-- -->
  528. <div class="filters--divider"></div>
  529. <!-- -->
  530. <div class="filters_card">
  531. <div class="fitlers--header">Categories:</div>
  532. <div class="filters_selections">
  533. ${categoriesHtml}
  534. </div>
  535. </div>
  536. </div>`;
  537.  
  538. form.addEventListener('change', e => {
  539. glo_filters[e.target.name][e.target.id] = e.target.checked;
  540. updateFilters();
  541. });
  542.  
  543. const lp_header = document.querySelector('.header');
  544. const pb_header = document.querySelector('#wrap-container');
  545.  
  546. if (lp_header) lp_header.insertBefore(form, lp_header.childNodes[2]);
  547. if (pb_header) pb_header.insertBefore(form, pb_header.childNodes[0]);
  548. createFilterToggle();
  549. };
  550.  
  551. const createFilterToggle = () => {
  552. const toggleFilterBtn = document.createElement('button');
  553. toggleFilterBtn.classList.add('enhanced_toggle');
  554. toggleFilterBtn.classList.add(`enhanced_toggle_${is_lp ? 'lp' : 'pb'}`);
  555.  
  556. toggleFilterBtn.innerHTML = `<i class="fa fa-filter"></i>`;
  557.  
  558. toggleFilterBtn.addEventListener('click', () => {
  559. const filters_container = document.querySelector('.filters_container');
  560. if (filters_container) {
  561. filters_container.classList.toggle('hide_element');
  562. }
  563. });
  564.  
  565. const search_container = document.querySelector(
  566. '.nav-search-container, .header-block:nth-of-type(3)'
  567. );
  568. is_lp ? (search_container.style = 'display: flex;') : null;
  569. search_container.appendChild(toggleFilterBtn);
  570. };
  571.  
  572. const callback = mutationsList => {
  573. for (let mutation of mutationsList) {
  574. for (const node of mutation.addedNodes) {
  575. if (node.nodeType === 1) {
  576. if (node.nodeName === 'DIV') {
  577. if (
  578. node.classList.contains('block-item') ||
  579. node.classList.contains('thumbnail')
  580. ) {
  581. enhanceScene(node);
  582. filterScene(node);
  583. }
  584. }
  585. }
  586. }
  587. }
  588. };
  589.  
  590. const config = { childList: true, subtree: true, attributes: true };
  591. const observer = new MutationObserver(callback);
  592. observer.observe(window.document, config);
  593.  
  594. const sortScenes = ({ date = false, views = false, asc = true } = {}) => {
  595. const node = document.querySelector('.thumbnails');
  596. [...node.children]
  597. .sort((a, b) => {
  598. const infoA = getSceneInfo(a);
  599. const infoB = getSceneInfo(b);
  600.  
  601. let order = -1;
  602.  
  603. if (date) {
  604. order = infoA.date > infoB.date ? -1 : 1;
  605. } else if (views) {
  606. order = infoA.views > infoB.views ? -1 : 1;
  607. } else {
  608. order = 0;
  609. }
  610.  
  611. if (a.classList.contains('button--load-more')) return 0;
  612. if (b.classList.contains('button--load-more')) return 0;
  613.  
  614. return !asc ? order : order * -1;
  615. })
  616. .map(scene => {
  617. node.appendChild(scene);
  618. node.appendChild(document.createTextNode(' '));
  619. });
  620. };
  621.  
  622. const sortBtns = () => {
  623. const sortDiv = document.createElement('div');
  624. sortDiv.classList.add('enhanced--sort');
  625. sortDiv.innerHTML = `
  626. <span>Sort By:</span>
  627. <button asc="false">Date</button>
  628. <button asc="false">Views</button>
  629. `;
  630.  
  631. sortDiv.addEventListener('click', e => {
  632. if (e.target.tagName === 'BUTTON') {
  633. const asc = e.target.getAttribute('asc') === 'true';
  634. e.target.setAttribute('asc', !asc);
  635. const sortObj = { asc };
  636. sortObj[e.target.innerText.toLowerCase()] = true;
  637. sortScenes(sortObj);
  638. }
  639. });
  640.  
  641. const navbar = document.querySelectorAll('.navbar > .container-fluid');
  642. if (navbar.length) {
  643. navbar[navbar.length - 1].appendChild(sortDiv);
  644. sortDiv.classList.add('enhanced--sort--navbar');
  645. return;
  646. }
  647.  
  648. const cont = document.querySelector('.page-section');
  649. if (cont) {
  650. sortDiv.children[0].style = 'color: #000;';
  651. cont.appendChild(sortDiv);
  652. }
  653. };
  654.  
  655. document.addEventListener('DOMContentLoaded', () => {
  656. if (!is_lp && !document.querySelector('.nav-search-container')) return;
  657. createFilterElement();
  658. sortBtns();
  659.  
  660. const nodes = document.querySelectorAll('.block-item, .thumbnail');
  661. [...nodes].map(node => {
  662. enhanceScene(node);
  663. filterScene(node);
  664. });
  665. });