ThisVid.com Improved

Infinite scroll (optional). Preview for private videos. Filter: duration, public/private, include/exclude terms. Check access to private vids. Mass friend request button. Sorts messages. Download button 📼

Versión del día 18/05/2025. Echa un vistazo a la versión más reciente.

// ==UserScript==
// @name         ThisVid.com Improved
// @namespace    http://tampermonkey.net/
// @version      5.2.1
// @license      MIT
// @description  Infinite scroll (optional). Preview for private videos. Filter: duration, public/private, include/exclude terms. Check access to private vids.  Mass friend request button. Sorts messages. Download button 📼
// @author       smartacephale
// @supportURL   https://github.com/smartacephale/sleazy-fork
// @match        https://*.thisvid.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=thisvid.com
// @grant        GM_addStyle
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/billy-herrington-utils.umd.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/jabroni-outfit.umd.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/lskdb.umd.js
// @run-at       document-idle
// ==/UserScript==
/* globals $ */

const {
    Tick,
    parseDom,
    fetchWith,
    fetchHtml,
    timeToSeconds,
    parseCSSUrl,
    circularShift,
    range,
    listenEvents,
    replaceElementTag,
    sanitizeStr,
    chunks,
    downloader,
    AsyncPool,
    computeAsyncOneAtTime,
    InfiniteScroller,
    DataManager
  } = window.bhutils;
  Object.assign(unsafeWindow, { bhutils: window.bhutils });
  const {
    JabroniOutfitStore,
    defaultStateWithDurationAndPrivacy,
    JabroniOutfitUI,
    defaultSchemeWithPrivateFilter,
  } = window.jabronioutfit;
  const { LSKDB } = window.lskdb;

  const SponsaaLogo = `
    Kono bangumi ha(wa) goran no suponsaa no teikyou de okurishimasu⣿⣿⣿⣿
    ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
    ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⡟⣟⢻⢛⢟⠿⢿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
    ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣿⣾⣾⣵⣧⣷⢽⢮⢧⢿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
    ⣿⣿⣿⣿⣿⣿⣯⣭⣧⣯⣮⣧⣯⣧⣯⡮⣵⣱⢕⣕⢕⣕⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
    ⣿⣿⣿⣿⣿⡫⡻⣝⢯⡻⣝⡟⣟⢽⡫⡟⣏⢏⡏⡝⡭⡹⡩⣻⣿⣿⣿⣿⠟⠟⢟⡟⠟⠻⠛⠟⠻⠻⣿⣿⣿⡟⠟⠻⠛⠟⠻⠻⣿⣿
    ⣿⣿⣿⣿⡿⣻⣿⣿⣿⡿⣿⣿⡿⣿⣿⢿⢿⡻⢾⠽⡺⡞⣗⠷⣿⣿⣿⡏⠀⠀⠀⣣⣤⡄⠀⠠⣄⡆⠫⠋⠻⢕⣤⡄⠀ ⢀⣤⣔⣿⣿
    ⣿⣿⣿⣿⣷⣷⣷⣾⣶⣯⣶⣶⣷⣷⣾⣷⣳⣵⣧⣳⡵⣕⣮⣞⣾⣿⡟⠄⢀⣦⠀⢘⣽⡇⠀⠨⣿⡌⠀⠣⣠⠹⠿⡭⠀ ⠐⣿⣿⣿⣿
    ⣿⣿⣿⣿⣕⣵⣱⣫⣳⡯⣯⣫⣯⣞⣮⣎⣮⣪⣢⣣⣝⣜⡜⣜⣾⣿⠃⠀⠀⠑⠀⠀⢺⡇⠀ ⢘⣾⠀⢄⢄⠘⠀⢘⢎⠀⢈⣿⣿⣿⣿
    ⣿⣿⣿⣿⣿⣙⣛⣛⢻⢛⢟⢟⣛⢻⢹⣙⢳⢹⢚⢕⣓⡓⡏⣗⣿⣓⣀⣀⣿⣿⣮⢀⣀⣇⣀⣐⣿⣔⣀⢁⢀⣀⣀⣅⣀⡠⣿⣿⣿⣿
    ⣿⣿⣿⣿⣿⣾⡞⣞⢷⡻⡯⡷⣗⢯⢷⢞⢷⢻⢞⢷⡳⣻⣺⣿⣿⣿⣿⣿⣿⣿⣿⣿⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
    ⣿⣿⣿⣿⣿⣿⣷⣵⡵⣼⢼⢼⡴⣵⢵⡵⣵⢵⡵⣵⣪⣾⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
    ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣮⣧⣫⣪⡪⡣⣫⣪⣣⣯⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
    ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿`;

  GM_addStyle(`
  .haveNoAccess { background: linear-gradient(to bottom, #b50000 0%, #2c2c2c 100%) red !important; }
  .haveAccess { background: linear-gradient(to bottom, #4e9299 0%, #2c2c2c 100%) green !important; }
  .success { background: linear-gradient(#2f6eb34f, #66666647) !important; }
  .failure { background: linear-gradient(rgba(179, 47, 47, 0.31), rgba(102, 102, 102, 0.28)) !important; }
  .friend-button { background: radial-gradient(#5ccbf4, #e1ccb1) !important; }
  .friendProfile { background: radial-gradient(circle, rgb(28, 42, 50) 48%, rgb(0, 0, 0) 100%) !important; }
  `);

  class THISVID_RULES {
    delay = 350;

    constructor() {
      const { href, pathname } = window.location;

      this._PAGINATION_ALLOWED = [
        /\.com\/$/,
        /\/(categories|tags?)\//,
        /\/?q=.*/,
        /\/(\w+-)?(rated|popular|private|newest|winners|updates)\/(\d+\/)?$/,
        /\/members\/\d+\/\w+_videos\//,
        /\/playlist\/\d+\//,
        /\/my_(\w+)_videos\//,
        /\/my_wall\/#\w+/,
      ].some((r) => r.test(href));

      this.IS_MEMBER_PAGE = /^\/members\/\d+\/$/.test(pathname);
      this.IS_WATCHLATER_KIND = /^\/my_(\w+)_videos\//.test(pathname);
      this.IS_MESSAGES_PAGE = /^\/my_messages\//.test(pathname);
      this.IS_PLAYLIST = /^\/playlist\/\d+\//.test(pathname);
      this.IS_VIDEO_PAGE = /^\/videos\//.test(pathname);

      this.PAGE_HAS_VIDEO = this.GET_THUMBS(document).length > 0;

      this.paginationElement = document.querySelector('.pagination');
      this.paginationLast = this.getPaginationLast();
      this.paginationOffset = this.getPaginationOffset();
      this.paginationUrlGenerator = this.getPaginationGenerator();

      this.CONTAINER = Array.from(document.querySelectorAll('.thumbs-items')).pop();

      this.MY_ID = document.querySelector('[target="_self"]')?.href.match(/\/(\d+)\//)[1] || null;
      this.LOGGED_IN = !!this.MY_ID;
      this.IS_MY_MEMBER_PAGE = this.LOGGED_IN && !!document.querySelector('.my-avatar');
      this.IS_OTHER_MEMBER_PAGE = !this.IS_MY_MEMBER_PAGE && this.IS_MEMBER_PAGE;
      this.IS_MEMBER_FRIEND =
        this.IS_OTHER_MEMBER_PAGE &&
        document.querySelector('.case-left')?.innerText.includes('is in your friends');

      if (this.IS_MEMBER_FRIEND) {
        document.querySelector('.profile').classList.add('friendProfile');
      }

      // playlist page add link to video
      if (this.IS_PLAYLIST) {
        const videoUrl = this.PLAYLIST_THUMB_URL(pathname);
        const desc = document.querySelector('.tools-left > li:nth-child(4) > .title-description');
        const link = replaceElementTag(desc, 'a');
        link.href = videoUrl;
      }
    }

    getPaginationOffset() {
      return this.IS_PLAYLIST ? 1 : parseInt(location.pathname.match(/\/(\d+)\/?$/)?.[1]) || 1;
    }

    getPaginationLast(doc) {
      const e = doc || document;
      return parseInt(e.querySelector('.pagination-next')?.previousElementSibling?.innerText) || 1;
    }

    GET_THUMBS(html) {
      if (this.IS_WATCHLATER_KIND) {
        return Array.from(html.querySelectorAll('.thumb-holder'));
      }
      let thumbs = Array.from(html.querySelectorAll('.tumbpu[title]'));
      if (thumbs.length === 0 && html?.classList?.contains('tumbpu')) thumbs = [html];
      return thumbs.filter((thumb) => !thumb?.parentElement.classList.contains('thumbs-photo'));
    }

    PLAYLIST_THUMB_URL(src) {
      return src.replace(/playlist\/\d+\/video/, () => 'videos');
    }

    THUMB_URL(thumb) {
      if (this.IS_WATCHLATER_KIND) {
        return thumb.firstElementChild.href;
      }
      let url = thumb.getAttribute('href');
      if (this.IS_PLAYLIST) url = this.PLAYLIST_THUMB_URL(url);
      return url;
    }

    getPaginationGenerator(proxyLocation) {
      const url = new URL(proxyLocation || window.location);

      if (url.pathname === '/') url.pathname = '/latest-updates/';
      if (!/\/(\d+)\/?$/.test(url.pathname)) url.pathname = `${url.pathname}${this.paginationOffset}/`;

      const paginationUrlGenerator = (n) => {
        if (this.IS_PLAYLIST) {
          url.search = `mode=async&function=get_block&block_id=playlist_view_playlist_view&sort_by=added2fav_date&from=${n}&_=${Date.now()}`;
        } else {
          url.pathname = url.pathname.replace(/\/\d+\/$/, `/${n}/`);
        }
        return url.href;
      };

      return paginationUrlGenerator;
    }

    THUMB_IMG_DATA(thumb) {
      const img = thumb.querySelector('img');
      const privateThumb = thumb.querySelector('.private');
      let imgSrc = img?.getAttribute('data-original');
      if (privateThumb) {
        imgSrc = parseCSSUrl(privateThumb.style.background);
        privateThumb.removeAttribute('style');
      }
      const count = img.getAttribute('data-cnt');
      if (!count || count === '6') img.removeAttribute('data-cnt');
      img.classList.remove('lazy-load');
      img.classList.add('tracking');

      if (this.IS_PLAYLIST) {
        img.onmouseover = img.onmouseout = null;
        img.removeAttribute('onmouseover');
        img.removeAttribute('onmouseout');
      }

      return { img, imgSrc };
    }

    THUMB_DATA(thumb) {
      const title = sanitizeStr(thumb.querySelector('.title').innerText);
      const duration = timeToSeconds(thumb.querySelector('.thumb > .duration').textContent);
      return { title, duration };
    }

    IS_PRIVATE(thumb) {
      return !thumb.querySelector('.private');
    }
  }

  const RULES = new THISVID_RULES();

  //====================================================================================================

  function friend(id, message = '') {
    return fetchWith(FRIEND_REQUEST_URL(id, message));
  }

  const FRIEND_REQUEST_URL = (id, message = '') =>
    `https://thisvid.com/members/${id}/?action=add_to_friends_complete&function=get_block&block_id=member_profile_view_view_profile&format=json&mode=async&message=${message}`;

  const USERS_PER_PAGE = 24;

  async function getMemberFriends(memberId, start, end) {
    const { friendsCount } = await getMemberData(memberId);
    const offset = Math.ceil(friendsCount / USERS_PER_PAGE);
    const pages = range(offset)
      .slice(start, end)
      .map((o) => `https://thisvid.com/members/${memberId}/friends/${o}/`);
    const pagesFetched = pages.map((p) => fetchHtml(p));
    const friends = (await Promise.all(pagesFetched)).flatMap(getMembers);
    return friends;
  }

  function getMembers(el) {
    const friendsList = el.querySelector('#list_members_friends_items');
    return Array.from(friendsList?.querySelectorAll('.tumbpu') || [])
      .map((e) => e.href.match(/\d+/)?.[0])
      .filter((_) => _);
  }

  async function friendMemberFriends(orientationFilter) {
    const memberId = window.location.pathname.match(/\d+/)[0];
    friend(memberId);
    const friends = await getMemberFriends(memberId);
    const spool = new AsyncPool(60);
    friends
      .map((fid) => {
        if (!orientationFilter) return () => friend(fid);
        return () =>
          getMemberData(fid).then(async ({ orientation, uploadedPrivate }) => {
            if (
              uploadedPrivate > 0 &&
              (orientation === orientationFilter ||
                (orientationFilter === 'Straight' && orientation === 'Lesbian'))
            ) {
              await friend(fid);
            }
          });
      })
      .forEach((f) => spool.push(f));
    await spool.run();
  }

  function initFriendship() {
    GM_addStyle(
      '.buttons {display: flex; flex-wrap: wrap} .buttons button, .buttons a {align-self: center; padding: 4px; margin: 5px;}',
    );

    const buttonAll = parseDom(
      '<button style="background: radial-gradient(red, blueviolet);">friend everyone</button>',
    );
    const buttonStraightOnly = parseDom(
      '<button style="background: radial-gradient(red, #a18cb5);">friend straights</button>',
    );
    const buttonGayOnly = parseDom(
      '<button style="background: radial-gradient(red, #46baff);">friend gays</button>',
    );
    const buttonBisexualOnly = parseDom(
      '<button style="background: radial-gradient(red, #4ebaaf);">friend bisexuals</button>',
    );

    document
      .querySelector('.buttons')
      .append(buttonAll, buttonStraightOnly, buttonGayOnly, buttonBisexualOnly);

    buttonAll.addEventListener('click', (e) => handleClick(e), { once: true });
    buttonStraightOnly.addEventListener('click', (e) => handleClick(e, 'Straight'), { once: true });
    buttonGayOnly.addEventListener('click', (e) => handleClick(e, 'Gay'), { once: true });
    buttonBisexualOnly.addEventListener('click', (e) => handleClick(e, 'Bisexual'), { once: true });

    function handleClick(e, orientationFilter) {
      const button = e.target;
      button.style.background = 'radial-gradient(#ff6114, #5babc4)';
      button.innerText = 'processing requests';
      friendMemberFriends(orientationFilter).then(() => {
        button.style.background = 'radial-gradient(blue, lightgreen)';
        button.innerText = 'friend requests sent';
      });
    }
  }

  //====================================================================================================

  async function sendMessage(uid, message = 'add me pls') {
    const url = new URL(
      `https://thisvid.com/members/${uid}/?action=send_message_complete&function=get_block&block_id=member_profile_view_view_profile&format=json&mode=async`,
    );
    url.searchParams.append('message', message);
    await fetch(url.href);
  }

  //====================================================================================================

  async function getMemberData(id) {
    const url = id.includes('member') ? id : `/members/${id}/`;
    const doc = await fetchHtml(url);
    const data = {};

    doc.querySelectorAll('.profile span').forEach((s) => {
      if (s.innerText.includes('Name:')) {
        data.name = s.firstElementChild.innerText.trim();
      }
      if (s.innerText.includes('Orientation:')) {
        data.orientation = s.firstElementChild.innerText.trim();
      }
      if (s.innerText.includes('Videos uploaded:')) {
        data.uploadedPublic = parseInt(s.children[0].innerText);
        data.uploadedPrivate = parseInt(s.children[1].innerText);
      }
    });

    data.friendsCount =
      parseInt(
        doc.querySelector('#list_members_friends')?.firstElementChild.innerText.match(/\d+/g).pop(),
      ) || 0;

    return data;
  }

  //====================================================================================================

  unsafeWindow.requestPrivateAccess = (e, memberid) => {
    e.preventDefault();
    friend(memberid, '');
    e.target.innerText = e.target.innerText.replace('🚑', '🍆');
  };

  async function checkPrivateVideoAccess(url) {
    const html = await fetchHtml(url);
    const holder = html.querySelector('.video-holder > p');

    const access = !holder;

    const uploaderEl = holder ? holder.querySelector('a') : html.querySelector('a.author');
    const uploaderURL = uploaderEl.href.match(/\d+/).at(-1);
    const uploaderName = uploaderEl.innerText;

    return {
      access,
      uploaderURL,
      uploaderName,
    };
  }

  const uploadersNotInFriendlist = new Set();

  async function requestAccess() {
    const checkAccess = async (thumb) => {;
      const { access, uploaderURL } = await checkPrivateVideoAccess(thumb.querySelector('a').href);

      if (!access) {
        thumb.classList.add('haveNoAccess');

        if (!uploadersNotInFriendlist.has(uploaderURL)) friend(uploaderURL);
      } else {
        thumb.classList.add('haveAccess');
      }
    };

    const f = [];
    document.querySelectorAll('.tumbpu:has(.private), .thumb-holder:has(.private)').forEach((thumb) => {
      if (!thumb.classList.contains('haveNoAccess') && !thumb.classList.contains('haveAccess')) {
        f.push(() => checkAccess(thumb));
      }
    });
    computeAsyncOneAtTime(f);
  }

  //====================================================================================================

  const createDownloadButton = () =>
    downloader({
      append: '',
      after: '.share_btn',
      button: '<li><a href="#" style="text-decoration: none;font-size: 2rem;">📼</a></li>',
      cbBefore: () => $('.fp-ui').click(),
    });

  //====================================================================================================

  class PreviewAnimation {
    constructor(element, delay = 750) {
      $('img[alt!="Private"]').off();
      this.tick = new Tick(delay);
      listenEvents(element, ['mouseover', 'touchstart'], this.animatePreview);
    }

    ITERATE_PREVIEW_IMG = (img) => {
      const count = parseInt(img.getAttribute('data-cnt')) || 6;
      img.src = img
        .getAttribute('src')
        .replace(/(\d+)(?=\.jpg$)/, (_, n) => `${circularShift(parseInt(n), count)}`);
    };

    animatePreview = (e) => {
      const { target: el, type } = e;
      if (!el.classList.contains('tracking') || !el.getAttribute('src')) return;
      this.tick.stop();
      if (type === 'mouseover' || type === 'touchstart') {
        const orig = el.getAttribute('src');
        this.tick.start(
          () => this.ITERATE_PREVIEW_IMG(el),
          () => {
            el.src = orig;
          },
        );
        el.addEventListener(
          type === 'mouseover' ? 'mouseleave' : 'touchend',
          () => this.tick.stop(),
          { once: true },
        );
      }
    };
  }

  //====================================================================================================

  function highlightMessages() {
    for (const member of document.querySelectorAll('.user-avatar > a')) {
      getMemberData(member.href).then(({ uploadedPublic, uploadedPrivate }) => {
        if (uploadedPrivate > 0) {
          const success = !member.parentElement.nextElementSibling.innerText.includes('declined');
          member.parentElement.parentElement.classList.add(success ? 'success' : 'failure');
        }
        member.parentElement.parentElement.querySelector('.user-comment p').innerText +=
          `  |  videos: ${uploadedPublic} public, ${uploadedPrivate} private`;
      });
    }
  }

  //====================================================================================================

  const lskdb = new LSKDB();

  async function getMemberVideos(id, type = 'private') {
    const { uploadedPrivate, uploadedPublic, name } = await getMemberData(id);
    const videosCount = type === 'private' ? uploadedPrivate : uploadedPublic;
    const paginationLast = Math.ceil(videosCount / 48);

    const iteratable_url = RULES.getPaginationGenerator(
      new URL(`https://thisvid.com/members/${id}/${type}_videos/`),
    );
    const memberVideosGenerator = InfiniteScroller.createPaginationGenerator(
      0,
      paginationLast,
      iteratable_url,
    );
    return { name, videosCount, memberVideosGenerator };
  }

  async function getMembersVideos(id, friendsCount, memberGeneratorCallback, type = 'private') {
    let skipFlag = false;
    let skipCount = 1;
    let minVideosCount = 1;

    const skipCurrentMember = (n = 1) => {
      skipFlag = true;
      skipCount = n;
    };
    const filterVideosCount = (n = 1) => {
      minVideosCount = n;
    };

    let membersIds = await getMemberFriends(id, 0, 1);
    getMemberFriends(id, 1).then((r) => {
      membersIds = membersIds.concat(r);
    });

    async function* pageGenerator() {
      let currentGenerator = null;
      for (let c = 0; c < friendsCount - 1; c++) {
        if (lskdb.hasKey(membersIds[c])) continue;

        if (!currentGenerator) {
          const { memberVideosGenerator, name, videosCount } = await getMemberVideos(
            membersIds[c],
            type,
          );

          if (memberVideosGenerator && videosCount >= minVideosCount) {
            currentGenerator = memberVideosGenerator;
            memberGeneratorCallback(name, videosCount, membersIds[c]);
          } else continue;
        }

        const {
          value: { url } = {},
          done,
        } = await currentGenerator.next();

        if (done || skipFlag) {
          c += skipCount - 1;
          skipCount = 1;
          currentGenerator = null;
          skipFlag = false;
        } else {
          yield { url, offset: c };
        }
      }
    }

    return {
      pageGenerator: () => pageGenerator(membersIds, type),
      skipCurrentMember,
      filterVideosCount,
    };
  }

  function createPrivateFeedButton() {
    const container = document.querySelectorAll('.sidebar ul')[1];
    const buttonPrv = parseDom(
      `<li><a href="https://thisvid.com/my_wall/#private_feed" class="selective"><i class="ico-arrow"></i>My Friends Private Videos</a></li>`,
    );
    const buttonPub = parseDom(
      `<li><a href="https://thisvid.com/my_wall/#public_feed" class="selective"><i class="ico-arrow"></i>My Friends Public Videos</a></li>`,
    );
    container.append(buttonPub, buttonPrv);
  }

  async function createPrivateFeed() {
    createPrivateFeedButton();
    if (!window.location.hash.includes('feed')) return;
    const isPubKey = window.location.hash === '#public_feed';

    const container = parseDom('<div class="thumbs-items"></div>');
    const ignored = parseDom('<div class="ignored"><h2>IGNORED:</h2></div>');

    Object.assign(defaultSchemeWithPrivateFilter, {
      controlsSkip: [
        { type: 'button', innerText: 'skip 10', callback: async () => skip(10) },
        { type: 'button', innerText: 'skip 100', callback: async () => skip(100) },
        { type: 'button', innerText: 'skip 1000', callback: async () => skip(1000) },
      ],
      controlsFilter: [
        { type: 'button', innerText: 'filter >10', callback: async () => filterVidsCount(10) },
        { type: 'button', innerText: 'filter >25', callback: async () => filterVidsCount(25) },
        { type: 'button', innerText: 'filter >100', callback: async () => filterVidsCount(100) },
      ],
    });

    const containerParent = document.querySelector('.main > .container > .content');
    containerParent.innerHTML = '';
    containerParent.nextElementSibling.remove();
    containerParent.append(container);
    container.before(ignored);
    GM_addStyle(`.content { width: auto; }
   .member-videos, .ignored { background: #b3b3b324; min-height: 3rem; margin: 1rem 0px; color: #fff; font-size: 1.24rem; display: flex; flex-wrap: wrap; justify-content: center;
     padding: 10px; width: 100%; }
   .member-videos * {  padding: 5px; margin: 4px; }
   .member-videos h2 a { font-size: 1.24rem; margin: 0; padding: 0; display: inline; }
   .ignored * {  padding: 4px; margin: 5px; }
   .thumbs-items { display: flex; flex-wrap: wrap; }`);

    RULES.intersectionObservable = document.querySelector('.footer');
    RULES.CONTAINER = container;

    const { friendsCount } = await getMemberData(RULES.MY_ID);

    RULES.paginationLast = friendsCount;

    const { pageGenerator, skipCurrentMember, filterVideosCount } = await getMembersVideos(
      RULES.MY_ID,
      friendsCount,
      (name, videosCount, id) => {
        container.append(
          parseDom(`
       <div class="member-videos" id="mem-${id}">
         <h2><a href="/members/${id}/">${name}</a> ${videosCount} videos</h2>
         <button onClick="hideMemberVideos(event)">ignore 🗡</button>
         <button onClick="hideMemberVideos(event, false)">skip</button>
       </div>`),
        );
      },
      isPubKey ? 'public' : 'private',
    );

    RULES.alternativeGenerator = pageGenerator;

    const ignoredMembers = lskdb.getAllKeys();
    ignoredMembers.forEach((im) => {
      document
        .querySelector('.ignored')
        .append(parseDom(`<button id="#ir-${im}" onClick="unignore(event)">${im} 🗡</button>`));
    });

    const skip = (n) => {
      skipCurrentMember(n);
      document.querySelector('.thumbs-items').innerHTML = '';
    };

    unsafeWindow.hideMemberVideos = (e, ignore = true) => {
      let id = e.target.parentElement.id;
      if (!document.querySelector(`#${id} ~ div`)) {
        skipCurrentMember();
      }
      const box = document.getElementById(id);
      const toDelete = [box];
      let curr = box.nextElementSibling;
      while (curr?.classList.contains('tumbpu')) {
        toDelete.push(curr);
        curr = curr.nextElementSibling;
      }
      toDelete.forEach((e) => e.remove());
      id = id.slice(4);
      if (ignore) {
        document
          .querySelector('.ignored')
          .append(parseDom(`<button id="irm-${id}" onClick="unignore(event)">${id} X</button>`));
        lskdb.setKey(id);
      }
    };

    unsafeWindow.unignore = (e) => {
      const id = e.target.id.slice(4);
      lskdb.removeKey(id);
      e.target.remove();
    };

    const filterVidsCount = (count) => filterVideosCount(count);

    initInfiniteScroll();
  }

  //====================================================================================================

  async function clearMessages() {
    const sortMsgs = (doc) => {
      doc.querySelectorAll('.entry').forEach((e) => {
        const id = e.querySelector('input[name="delete[]"]').value;
        const msg = e.querySelector('.user-comment').innerText;
        if (/has confirmed|declined your|has removed/g.test(msg)) deleteMsg(id);
      });
    };

    const deleteMsg = (id) => {
      const url = `https://thisvid.com/my_messages/inbox/?mode=async&format=json&action=delete&function=get_block&block_id=list_messages_my_conversation_messages&delete[]=${id}`;
      fetch(url).then((res) => console.log(url, res?.status));
    };

    await Promise.all(
      Array.from({ length: RULES.paginationLast }, (_, i) =>
        fetchHtml(`https://thisvid.com/my_messages/inbox/${i + 1}/`).then((html) => sortMsgs(html)),
      ),
    );
  }

  function clearMessagesButton() {
    const btn = parseDom('<button>clear messages</button>');
    btn.addEventListener('click', clearMessages);
    document.querySelector('.headline').append(btn);
  }

  //====================================================================================================

  function initInfiniteScroll() {
    const iscroller = new InfiniteScroller({
      enabled: state.infiniteScrollEnabled,
      handleHtmlCallback: handleLoadedHTML,
      ...RULES
    }).onScroll((i) => {
      stateLocale.pagIndexLast = i.paginationLast;
      stateLocale.pagIndexCur = i.paginationOffset;
    });

    store.subscribe(() => { iscroller.enabled = state.infiniteScrollEnabled; });

    return iscroller;
  }

  function route() {
    console.log(SponsaaLogo);

    if (RULES.LOGGED_IN) {
      defaultSchemeWithPrivateFilter.privateFilter.push({
        type: 'button',
        innerText: 'request access 🔓',
        callback: requestAccess,
      });
    }

    if (RULES.IS_MY_MEMBER_PAGE) {
      createPrivateFeed();
      RULES.PAGE_HAS_VIDEO = true;
    }

    if (RULES.IS_MESSAGES_PAGE) {
      clearMessagesButton();
      highlightMessages();
    }

    if (RULES.IS_VIDEO_PAGE) {
      const holder = document.querySelector('.video-holder > p');
      if (holder) {
        const uploader = document.querySelector('a.author').href.match(/\d+/).at(-1);
        holder.parentElement.append(
          parseDom(
            `<button onclick="requestPrivateAccess(event, ${uploader}); this.onclick=null;">Friend Request</button>`,
          ),
        );
      }
      createDownloadButton();
    }

    if (!RULES.PAGE_HAS_VIDEO) return;

    const containers = Array.from(
      RULES.IS_WATCHLATER_KIND
        ? [RULES.CONTAINER]
        : document.querySelectorAll('.thumbs-items:not(.thumbs-members)'),
    );

    if (containers.length > 1 && !RULES.IS_MEMBER_PAGE) RULES.CONTAINER = containers[0];
    containers.forEach((c) => {
      handleLoadedHTML(c, RULES.IS_MEMBER_PAGE ? c : RULES.CONTAINER, true);
    });

    new PreviewAnimation(document.body);
    new JabroniOutfitUI(store, defaultSchemeWithPrivateFilter);

    if (RULES.IS_OTHER_MEMBER_PAGE) {
      initFriendship();
    }

    if (RULES._PAGINATION_ALLOWED) {
      stateLocale.pagIndexLast = RULES.paginationLast;
      if (!RULES.paginationElement) return;
      initInfiniteScroll();
    }
  }

  //====================================================================================================

  const store = new JabroniOutfitStore(defaultStateWithDurationAndPrivacy);
  const { state, stateLocale } = store;
  const { applyFilters, handleLoadedHTML } = new DataManager(RULES, state);
  store.subscribe(applyFilters);

  route();