LegalPorno Enhancer

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

Version vom 04.02.2020. Aktuellste Version

// ==UserScript==
// @name        LegalPorno Enhancer
// @namespace   https://www.legalporno.com/forum/viewtopic.php?f=96&t=24238
// @description Adds extra functionality to LegalPorno and Pornbox. Easily filter out unwanted categories. Also adds tags on top of every scene.
// @match       http*://*.legalporno.com/*
// @match       http*://legalporno.com/*
// @exclude     http*://*legalporno.com/forum/*
// @exclude     http*://account.legalporno.com/*
// @match       https://*pornbox.com/*
// @version     1.2.4
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_addStyle
// @run-at      document-start
// ==/UserScript==

const is_lp = !!window.location.href.match(/legalporno/);

let glo_filters = {
  studios: {
    'Gonzo.com': true,
    'Giorgio Grandi': true,
    'Interracial Vision': true,
    'American Anal': true,
    "Giorgio's Lab": true,
    Focus: true,
    Assablanca: true,
    'Mr Anal': true,
    'Porn World': true,
    'Girls Gone Wild': true
  },
  categories: {
    Piss: true,
    'Piss Drink': true,
    Fisting: true,
    Prolapse: true,
    Shemale: true
  }
};

const studio_colors = {
  'Gonzo.com': { bg: '#cd0000', text: '#fff' },
  'Giorgio Grandi': { bg: '#693180', text: '#fff' },
  'Interracial Vision': { bg: '#242424', text: '#e6e6e6' },
  'American Anal': { bg: '#b45219', text: '#fffa78' },
  "Giorgio's Lab": { bg: '#206d04', text: '#fff' }
};

// Uncomment the following line to reset global filters
// GM_setValue('user_filters', JSON.stringify(glo_filters));

validateUserFilters = filters => {
  if (!filters.studios || !filters.categories) return false;
  if (
    Object.keys(filters.studios).length !==
    Object.keys(glo_filters.studios).length
  )
    return false;
  if (
    Object.keys(filters.categories).length !==
    Object.keys(glo_filters.categories).length
  )
    return false;
  if (
    Object.keys(filters.categories)
      .sort()
      .some(
        (val, idx) => val !== Object.keys(glo_filters.categories).sort()[idx]
      )
  )
    return false;
  if (
    Object.keys(filters.studios)
      .sort()
      .some((val, idx) => val !== Object.keys(glo_filters.studios).sort()[idx])
  )
    return false;
  return true;
};

try {
  const user_config = GM_getValue('user_filters');
  if (!user_config)
    throw new Error('No user filters found. Using default ones.');
  user_filters = JSON.parse(user_config);
  if (!validateUserFilters(user_filters))
    throw new Error('Invalid user settings.');
  glo_filters = user_filters;
} catch (e) {}

const studios = {
  Interracial: 'Interracial Vision',
  'Hard Porn World': 'Porn World'
};

const icon_categories = {
  interracial: 'IR',
  'double anal (DAP)': 'DAP',
  fisting: 'Fisting',
  prolapse: 'Prolapse',
  shemale: 'Shemale',
  'triple anal (TAP)': 'TAP',
  piss: 'Piss',
  squirting: 'Squirt',
  '3+ on 1': '3+on1',
  'double vaginal (DPP)': 'DPP',
  'first time': '1st',
  'triple penetration': 'TP',
  '0% pussy': '0% Pussy',
  'cum swallowing': 'Cum Swallow',
  'piss drinking': 'Piss Drink',
  'anal creampies': 'Anal Creampie',
  '1 on 1': '1on1',
  milf: 'Milf',
  'facial cumshot': 'Facial'
};

GM_addStyle(`
.hiddenScene {
  display: none;
}

.hide_element {
  display: none !important;
}

.thumbnail-duration {
  top: 0 !important;
  right: 0 !important;
  left: unset !important;
  bottom: unset !important;
  margin: 5px !important;
}

.enhanced_categories_container {
  width: 100%;
  display: flex;
  flex-wrap: wrap-reverse;
  position: absolute;
  bottom:0; left: 0;
  z-index: 3;
  padding: 3px 5px;
}

.enchanced_icon {
  border-radius: 3px;
  background-color: #616161;
  color: #eee;
  opacity: 0.9;
  padding: 2px 7px;
  margin-right: 2px;
  margin-top: 2px;
  font-size: 13px;
}

.enhanced--studio {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 3;
  display: flex;
  margin: 4px 6px;
}

.enhanced--studio {
  background-color: #a76523c9;
  border-radius: 3px;
  padding: 2px 7px;
  color: white;
  text-shadow: 1px 1px #00000080;
  font-size: 12px;
  margin-right: 4px;
}

.enhanced_views_label {
  position: absolute;
  top: 28px;
  right: 5px;
  font-size: 11px;
  display: flex;
}

.enhanced--views {
  background-color: rgba(102,102,102,0.6);
  padding: 2px 6px;
  border-radius: 3px;
  color: #fff;
  display: flex;
  height: 100%;
  align-items: center;
  justify-content: center;
}

.enhanced--views > i {
  margin-right: 5px;
}

.item__img:hover > div {
  display: none;
}

.item__img-labels-bottom {
  top: 3px;
  right: 3px;
  left: unset;
  bottom: unset;
  display: flex
}

.img-label {
  margin-top: unset;
  margin-left: 2px;
}

.item__img-labels {
  display: flex;
  flex-direction: row-reverse;
  left: unset;
  right: 3px;
  top: 23px;
}

/* Filter Css Begin */
.filters_container_lp {
  margin-left: -15px;
  margin-right: -15px;
}

.filters_container--main {
  width: 100%;
  display: flex;
  justify-content: center;
  background: linear-gradient(
    0deg,
    #babcbc 0%,
    #dfe1e1 2%,
    #dfe1e1 98%,
    #babcbc 100%
  );
}

.filters_card {
  margin: 0 25px;
  padding-bottom: 15px;
  display: flex;
  flex-direction: column;
  align-items: center;
  position: relative;
}

.filters_selections {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
}

.filters--divider {
  width: 1px;
  background-color: rgb(0, 0, 0, 0.25);
  height: 50px;
  margin-top: 15px;
  align-self: center;
}

.filters--selection {
  padding: 1px 10px 0 0;
  min-width: 140px;
  display: flex;
  align-items: center;
}

.filters--selection > label,
.filters--selection > input[type="checkbox"] {
  margin: 0 2px 0 0;
  font-weight: bold;
}

.fitlers--header {
  padding: 5px 0 5px 0;
  align-self: center;
  font-weight: bold;
}

.enhanced_toggle {
  background: -webkit-linear-gradient(
    top,
    #a9a9a9 0%,
    #d97575 5%,
    #563434 100%
  ) !important;
  // height: 100%;
  border: 1px solid #722424 !important;
  color: white;
  padding: 7px 11px;
  font-weight: 700;
  border: none !important;
  }

.enhanced_toggle_pb {
  height: 100%;
  padding: 7px 11px;
  border-radius: 300px;
  position: absolute;
  margin-left: 4px;
}

.enhanced_toggle_lp {
  border-radius: 200px;
  margin-left: 4px;
}

.enhanced_toggle:hover {
  cursor: pointer;
}

.enhanced_toggle > i {
  font-size: 12px;
}
/* Filter Css End */

.enhanced--sort {
  display: grid;
  grid-template-columns: 1fr 1fr;
  justify-items: center;
  grid-gap: 2px;
  position: absolute;

  top: -4px;
  right: 210px;
}

.enhanced--sort--navbar {
  right: 25px;
  top: 6px;
}

.enhanced--sort > span {
  grid-column-end: span 2;
  color: #fff;
  
}

.enhanced--sort > button {
  width: 70px;
  height: 23px;
  justify-content: center;
  align-items: center;
  text-align: center;
  padding: 0;
  margin: 0;
}

.page-section {
  position: relative !important;
}
`);

const getSceneInfo = scene => {
  const info = {
    studio: null,
    categories: null,
    date: null,
    views: null
  };

  info.studio = scene.querySelector('.rating-studio-name, a[href^="#studio/"]');
  info.studio = info.studio ? info.studio.innerText : null;
  if (studios[info.studio]) {
    info.studio = studios[info.studio];
  }
  info.categories = [...scene.querySelectorAll('a[href*="niche/"]')]
    .filter(cat => cat.innerText && icon_categories[cat.innerText])
    .map(cat => icon_categories[cat.innerText])
    .sort();
  // info.categories = info.categories.filter(
  //   cat => !(cat === 'Piss' && info.categories.includes('Piss Drink'))
  // );
  info.categories = Array.from(new Set(info.categories));

  const views = scene.querySelector('.rating-views');
  const date = scene.querySelector('.glyphicon-calendar');

  if (views) {
    info.views = parseInt(views.innerText.replace('VIEWS:', '').trim(), 10);
  }
  if (date) {
    try {
      info.date = new Date(
        date.parentElement.innerText.replace('RELEASE:', '').trim()
      );
    } catch (e) {}
  }

  return info;
};

const formatViews = n => {
  if (n >= 1e3 && n < 1e6) return +(n / 1e3).toFixed(0) + 'k';
  if (n >= 1e6 && n < 1e9) return +(n / 1e6).toFixed(0) + 'm';
  if (n >= 1e9 && n < 1e12) return +(n / 1e9).toFixed(0) + 'b';
  // if (n >= 1e12) return +(n / 1e12).toFixed(1) + "T";
  return n;
};

const enhanceScene = scene => {
  // Remove icons from thumnail. ("4k" and "new")
  const icons = scene.querySelectorAll('.icon');
  for (const icon of icons) {
    icon.classList.add('hide_element');
  }

  const { studio, categories, views, date } = getSceneInfo(scene);

  if (studio) {
    const studioLabel = document.createElement('div');
    studioLabel.classList.add('enhanced--studio');
    studioLabel.innerHTML = studio;

    if (studio_colors[studio]) {
      studioLabel.style = `
      background-color: ${studio_colors[studio]['bg']}; color: ${studio_colors[studio]['text']};
      `;
    }

    if (views) {
      const viewsLabel = document.createElement('div');
      viewsLabel.innerHTML = `
      <div class='enhanced--views'><i class='fa fa-eye'></i>${formatViews(
        views
      )}</div>`;
      viewsLabel.classList.add('enhanced_views_label');
      if (scene.querySelector('.thumbnail-image')) {
        scene.querySelector('.thumbnail-image').appendChild(viewsLabel);
      }
    }

    // studioLabel.classList.add('enhanced_title_tags');
    scene
      .querySelector('.thumbnail-image, .item__img')
      .appendChild(studioLabel);
  }
  if (categories.length) {
    const cat_div = document.createElement('div');
    cat_div.classList.add('enhanced_categories_container');
    for (const cat of categories) {
      const icon = document.createElement('div');
      icon.classList.add('enchanced_icon');
      icon.innerHTML = cat;
      cat_div.appendChild(icon);
    }

    if (scene.querySelector('.thumbnail-image, .item__img')) {
      scene.querySelector('.thumbnail-image, .item__img').appendChild(cat_div);
    }
  }
};

const is_scene_filtered = scene => {
  const { studio, categories, views, date } = getSceneInfo(scene);

  for (const key of Object.keys(glo_filters.studios)) {
    if (studio && studio.includes(key) && !glo_filters.studios[key])
      return true;
  }

  for (const key of Object.keys(glo_filters.categories)) {
    if (categories && categories.includes(key) && !glo_filters.categories[key])
      return true;
  }

  return false;
};

const filterScene = scene => {
  if (is_scene_filtered(scene)) {
    scene.classList.add('hiddenScene');
  }
};

const updateFilters = () => {
  GM_setValue('user_filters', JSON.stringify(glo_filters));
  const scenes = document.querySelectorAll('.thumbnail, .block-item');
  for (const scene of scenes) {
    if (scene.classList.contains('hiddenScene'))
      scene.classList.remove('hiddenScene');
    filterScene(scene);
  }
};

const createFilterElement = () => {
  const { studios, categories } = glo_filters;

  const studioHtml = Object.keys(studios).reduce(
    (acc, studio) =>
      (acc += `<div class="filters--selection">
      <input type="checkbox" name="studios" id="${studio}"
      ${studios[studio] ? 'checked="checked"' : ''}
      />
      <label for="${studio}">${studio}</label>
    </div>`),
    ''
  );

  const categoriesHtml = Object.keys(categories).reduce(
    (acc, cat) =>
      (acc += `<div class="filters--selection">
      <input type="checkbox" name="categories" id="${cat}"
      ${categories[cat] ? 'checked="checked"' : ''}
      />
      <label for="${cat}">${cat}</label>
    </div>`),
    ''
  );

  const form = document.createElement('form');
  form.classList.add('filters_container', 'hide_element');
  if (is_lp) form.classList.add('filters_container_lp');
  form.innerHTML = `<div class="filters_container--main">
    <div class="filters_card">
      <div class="fitlers--header">Studios:</div>
      <div class="filters_selections">
        ${studioHtml}
      </div>
    </div>
    <!--  -->
    <div class="filters--divider"></div>
    <!--  -->
    <div class="filters_card">
      <div class="fitlers--header">Categories:</div>
      <div class="filters_selections">
        ${categoriesHtml}
      </div>
    </div>
    </div>`;

  form.addEventListener('change', e => {
    glo_filters[e.target.name][e.target.id] = e.target.checked;
    updateFilters();
  });

  const lp_header = document.querySelector('.header');
  const pb_header = document.querySelector('#wrap-container');

  if (lp_header) lp_header.insertBefore(form, lp_header.childNodes[2]);
  if (pb_header) pb_header.insertBefore(form, pb_header.childNodes[0]);
  createFilterToggle();
};

const createFilterToggle = () => {
  const toggleFilterBtn = document.createElement('button');
  toggleFilterBtn.classList.add('enhanced_toggle');
  toggleFilterBtn.classList.add(`enhanced_toggle_${is_lp ? 'lp' : 'pb'}`);

  toggleFilterBtn.innerHTML = `<i class="fa fa-filter"></i>`;

  toggleFilterBtn.addEventListener('click', () => {
    const filters_container = document.querySelector('.filters_container');
    if (filters_container) {
      filters_container.classList.toggle('hide_element');
    }
  });

  const search_container = document.querySelector(
    '.nav-search-container, .header-block:nth-of-type(3)'
  );
  is_lp ? (search_container.style = 'display: flex;') : null;
  search_container.appendChild(toggleFilterBtn);
};

const callback = mutationsList => {
  for (let mutation of mutationsList) {
    for (const node of mutation.addedNodes) {
      if (node.nodeType === 1) {
        if (node.nodeName === 'DIV') {
          if (
            node.classList.contains('block-item') ||
            node.classList.contains('thumbnail')
          ) {
            enhanceScene(node);
            filterScene(node);
          }
        }
      }
    }
  }
};

const config = { childList: true, subtree: true, attributes: true };
const observer = new MutationObserver(callback);
observer.observe(window.document, config);

const sortScenes = ({ date = false, views = false, asc = true } = {}) => {
  const node = document.querySelector('.thumbnails');
  [...node.children]
    .sort((a, b) => {
      const infoA = getSceneInfo(a);
      const infoB = getSceneInfo(b);

      let order = -1;

      if (date) {
        order = infoA.date > infoB.date ? -1 : 1;
      } else if (views) {
        order = infoA.views > infoB.views ? -1 : 1;
      } else {
        order = 0;
      }

      if (a.classList.contains('button--load-more')) return 0;
      if (b.classList.contains('button--load-more')) return 0;

      return !asc ? order : order * -1;
    })
    .map(scene => {
      node.appendChild(scene);
      node.appendChild(document.createTextNode(' '));
    });
};

const sortBtns = () => {
  const sortDiv = document.createElement('div');
  sortDiv.classList.add('enhanced--sort');
  sortDiv.innerHTML = `
  <span>Sort By:</span>
  <button asc="false">Date</button>
  <button asc="false">Views</button>
  `;

  sortDiv.addEventListener('click', e => {
    if (e.target.tagName === 'BUTTON') {
      const asc = e.target.getAttribute('asc') === 'true';
      e.target.setAttribute('asc', !asc);
      const sortObj = { asc };
      sortObj[e.target.innerText.toLowerCase()] = true;
      sortScenes(sortObj);
    }
  });

  const navbar = document.querySelectorAll('.navbar > .container-fluid');
  if (navbar.length) {
    navbar[navbar.length - 1].appendChild(sortDiv);
    sortDiv.classList.add('enhanced--sort--navbar');
    return;
  }

  const cont = document.querySelector('.page-section');
  if (cont) {
    sortDiv.children[0].style = 'color: #000;';
    cont.appendChild(sortDiv);
  }
};

document.addEventListener('DOMContentLoaded', () => {
  if (!is_lp && !document.querySelector('.nav-search-container')) return;
  createFilterElement();
  sortBtns();

  const nodes = document.querySelectorAll('.block-item, .thumbnail');
  [...nodes].map(node => {
    enhanceScene(node);
    filterScene(node);
  });
});