Analvids/LegalPorno Enhancer

Adds extra functionality to Analvids (formerly known as LegalPorno) and Pornbox. Easily filter out unwanted categories. Also adds tags on top of every scene.

От 22.12.2021. Виж последната версия.

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

const is_lp = !!window.location.href.match(/(analvids|pornworld)/);

let glo_filters = {
  studios: {
    'Giorgio Grandi': true,
    'Gonzo.com': true,
    'NRX-Studio': true,
    "Giorgio's Lab": true,
    'Yummy estudio': true,
    'Busted T-Girls': true,
    'VK Studio': true,
    'Black in White': true,
    'N&F studio': true,
    XfreaX: true,
    'Interracial Vision': true,
    'Angelo Godshack Original': true,
    'Sineplex SOS': true,
    'Sineplex CZ': true,
    'PISSING E ANAL FANTASY': true,
    'LATIN TEENS productions': true,
    'Natasha Teen Productions': true,
    'Mambo Perv': true,
    'American Anal': true,
    'Rock Corp': true,
    'TheWonderToys Training Studio': true,
    'Vira Gold Films': true,
    'Anal Maniacs by Lady Dee': true,
    'Eden does': true,
    'Pineapples Studio': true,
    'Porn World': false,
    'Show Everything': false,
  },
  categories: {
    Piss: true,
    'Piss Drink': true,
    Fisting: true,
    Prolapse: true,
    Trans: true,
  },
};

const studio_colors = {

  'Gonzo.com': { bg: '#e90000a5', text: '#ffeeee' },
  'Giorgio Grandi': { bg: '#2313d3a5', text: '#EEE' },
  'Interracial Vision': { bg: '#242424a5', text: '#e6e6e6' },
  'American Anal': { bg: '#0C5326a5', text: '#d1f3e8' },
  "Giorgio's Lab": { bg: '#3DB8DCa5', text: '#EEEEEE' },
  'NRX-Studio': { bg: '#24f6d0a5', text: '#424d4b' },
  'N&F studio': { bg: '#1091c4a5', text: '#EEEEEE' },
  'Yummy estudio': { bg: '#0a11e4a5', text: '#ffff25' },
  'Busted T-Girls': { bg: '#BDF619a5', text: '#3485A6' },
  'VK Studio': { bg: '#d7fbaaa5', text: '#3a531b' },
  'Black in White': { bg: '#EEEEEEa5', text: '#000000' },
  'N&F studio': { bg: '#8D15CAa5', text: '#EEEEEE' },
  XfreaX: { bg: '#93076Aa5', text: '#EEEEEE' },
  'Angelo Godshack Original': { bg: '#6ba2d4a5', text: '#EEEEEE' },
  'Sineplex SOS': { bg: '#9C44EEa5', text: '#EEEEEE' },
  'Sineplex CZ': { bg: '#8A0EB3a5', text: '#EEEEEE' },
  'PISSING E ANAL FANTASY': { bg: '#ff5e00a5', text: '#fbf275' },
  'LATIN TEENS productions': { bg: '#1AFB54a5', text: '#555555' },
  'Natasha Teen Productions': { bg: '#EE1788a5', text: '#EEEEEE' },
  'Mambo Perv': { bg: '#C729A5a5', text: '#EEEEEE' },
  'Rock Corp': { bg: '#6D1644a5', text: '#83C650' },
  'TheWonderToys Training Studio': { bg: '#ACEACFa5', text: '#5F546A' },
  'Vira Gold Films': { bg: '#F7AF7Ca5', text: '#662a35' },
  'Anal Maniacs by Lady Dee': { bg: '#4B7039a5', text: '#EEEEEE' },
  'Eden does': { bg: '#378B03a5', text: '#EEEEEE' },
  'Pineapples Studio': { bg: '#674ABAa5', text: '#EEEEEE' },
  'Porn World': { bg: '#C1015Aa5', text: '#E2E316' },
};

// 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',
  transsexual: 'Trans',
  trans: 'Trans',
  '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 !important;
}

.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;
  font-family: sans-serif;
  font-weight: 600;
  font-size: 0.98em;
  background-color: #a76523c9;
  border-radius: 3px;
  padding: 2px 7px;
  color: white;
  // text-shadow: 1px 1px #00000080;
  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(3, 1fr);
  margin-bottom: 10px;
}

.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^="/application/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, .thumbnail-price');
  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 isStudioFiltered = studio => {
  for (const key of Object.keys(glo_filters.studios)) {
    if (studio && studio.includes(key) && glo_filters.studios[key])
      return false;
  }
  return true;
};

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

  if (!glo_filters.studios['Show Everything']) {
    if (isStudioFiltered(studio)) 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 (isSceneFiltered(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 getStudiosCheckboxes = () => {
  return [
    ...document.querySelectorAll(
      'input[name="studios"]:not(input[id="Show Everything"])'
    ),
  ];
};

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"' : ''}
      ${
        studios['Show Everything'] && studio !== 'Show Everything'
          ? 'disabled=true"'
          : ''
      }
      />
      <label for="${studio}"
      ${studio == 'Show Everything' ? 'style= "color:#f83600;"' : ''}>
      ${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>
          <input type="button" value="Select All">
          <input type="button" value="Clear">
        </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('click', e => {
    if (!e.target.type) return;

    if (e.target.type == 'button') {
      if (e.target.value === 'Select All') {
        getStudiosCheckboxes().forEach(n => {
          n.checked = true;
        });

        Object.keys(glo_filters.studios).forEach(studio => {
          if (studio === 'Show Everything') return;
          glo_filters.studios[studio] = true;
        });
      }
      if (e.target.value === 'Clear') {
        getStudiosCheckboxes().forEach(n => {
          n.checked = false;
        });
        Object.keys(glo_filters.studios).forEach(studio => {
          glo_filters.studios[studio] = false;
        });
      }
      // glo_filters[e.target.name][e.target.id] = e.target.checked;
    }

    if (e.target.type == 'checkbox') {
      glo_filters[e.target.name][e.target.id] = e.target.checked;
    }

    if (e.target.id === 'Show Everything') {
      getStudiosCheckboxes().forEach(el => {
        el.disabled = !el.disabled;
      });
    }

    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);
  });
});