PornoLab.net Thumbnail ExpanderEX

Automatically unfolds spoilers and replaces thumbnails with full-sized images constrained to the viewport height while blocking thumbnails linking to adware.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         PornoLab.net Thumbnail ExpanderEX
// @namespace    http://pornolab.net/
// @version      1.1.1
// @description  Automatically unfolds spoilers and replaces thumbnails with full-sized images constrained to the viewport height while blocking thumbnails linking to adware.
// @author       Anonymous
// @license      GPL-3.0-or-later
// @include      http://pornolab.net/forum/viewtopic.php*
// @include      https://pornolab.net/forum/viewtopic.php*
// @grant        GM_xmlhttpRequest
// ==/UserScript==

(function() {
  const autoUnfold = true;
  const autoPreload = true;
  const maxImgWidth = '1200px';
  const maxImgHeight = (Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0) - 50) + 'px';
  const blockedHosts = ['piccash.net', 'picclick.ru', 'pic4cash.ru', 'picspeed.ru', 'picforall.ru', 'freescreens.ru'];
  const disableImageReplacementInsideFoldsWithTitle = ['show'];

  function sleep(ms) {
    return new Promise(res => setTimeout(res, ms));
  }

  class Ticker {
    #last = new Date();
    tick() { this.#last = new Date(); }
    async wrap(cb, ms) {
      this.tick();
      await cb();
      await this.after(ms);
    }
    async after(ms) {
      const passed = new Date() - this.#last;
      if (passed > ms) { return; }
      await sleep(ms - passed);
    }
  }

  const qhandler = new class {
    #running = false
    async run(queue) {
      if (this.#running) { return; }
      const ticker = new Ticker();

      this.#running = true
      for (let i = 0; i < queue.length; i++) {
        while (!document.hasFocus()) { await sleep(200); }

        try { await ticker.wrap(queue[i], 500); }
        catch (err) { console.error(err); i--; await ticker.after(1000); }
      }
      this.#running = false
    }
  };

  const queue = Array.from(document.querySelectorAll('.sp-wrap')).map((post) => {
    const links = Array.from(post.querySelectorAll('var.postImg'));
    if (!links.length) return;

    if (autoUnfold) {
      post.querySelectorAll('.sp-head').forEach(header => header.classList.add('unfolded'));
    }

    if (autoPreload || autoUnfold) {
      post.querySelectorAll('.sp-body').forEach(body => {
        if (autoPreload) body.classList.add('inited');
        if (autoUnfold) body.style.display = 'block';
      })
    }

    const foldTitle = post.querySelector('.sp-head')?.textContent;
    const disableReplace = disableImageReplacementInsideFoldsWithTitle.includes(foldTitle);

    const queue = [];
    for (const link of links) {
      const url = link.title;
      const parentHref = link.parentNode?.href;

      if (!parentHref || disableReplace) {
        updateImageUrl(link, url);
        continue;
      }

      if (isBlocked(url)) {
        updateImageUrl(link, null);
        continue;
      }

      const updater = getImageUpdater(url, parentHref);
      updater ? queue.push(() => updater(link)) : updateImageUrl(link, url);
    }

    return queue;
  }).flat().filter(e => e);

  qhandler.run(queue);

  async function req(url, method, responseType = undefined) {
    return new Promise((res, rej) => {
      GM_xmlhttpRequest({
        method,
        url,
        responseType,
        onerr: rej,
        onload: res
      });
    });
  }

  function isBlocked(url) {
    return blockedHosts.some(host => url.includes(host));
  }

  function getImageUpdater(url, parentHref) {
    if (url.includes('fastpic.org')) {
      return link => handleFastpic(link, parentHref);
    } else if (url.includes('imagebam.com')) {
      return link => handleImageBam(link, parentHref);
    } else if (url.includes('imagevenue.com')) {
      return link => handleImageVenue(link, parentHref);
    } else if (url.includes('imgbox.com')) {
      return link => updateImageUrl(link, url.replace('thumbs', 'images').replace('_t', '_o'));
    } else if (url.includes('imgdrive.net')) {
      return link => handleImgDrive(link, url.replace('small', 'big'));
    } else if (parentHref.includes('turboimagehost.com')) {
      return link => handleTurboimagehost(link, parentHref);
    }
    return null;
  }

  async function fastpicUrl(link, url) {
    return await req(url, 'GET').then(response => {
      const match = response.responseText.match(/https?:\/\/i[0-9]{0,3}\.fastpic\.org\/big\/.+\.(?:jpe?g|png)\?.+?"/);
      console.log(match);
      if (![302, 200].includes(response.status)) throw 429;
      if (match) updateImageUrl(link, match[0].slice(0, -1));
      else updateImageUrl(link, url);
    });
  }

  async function fastpicIsDirectUrl(_, url) {
    return await req(url, 'HEAD').then(response => {
      const responseType = response.responseHeaders.split('\n').find(e => e.startsWith('content-type'))?.split(' ')?.[1]?.replace(';', '');
      console.log(responseType);
      return responseType !== 'text/html';
    });
  }

  async function handleFastpic(link, parentHref) {
    const url = parentHref || link.title;
    return await fastpicIsDirectUrl(link, url).then(c => !c ? fastpicUrl(link, url) : updateImageUrl(link, url));
  }

  async function handleImageBam(link, parentHref) {
    return await req(parentHref, 'GET').then(r => {
      const match = r.responseText.match(/<img src="(.+?)"[^>]+class="main-image/i);
      if (match) updateImageUrl(link, match[1]);
    });
  }

  async function handleImageVenue(link, parentHref) {
    return await req(parentHref, 'GET').then(r => {
      const match = r.responseText.match(/https?:\/\/cdn-images\.imagevenue\.com\/[a-z0-9]{2}\/[a-z0-9]{2}\/[a-z0-9]{2}\/.+?_o\.(?:jp.?g|png)"/);
      if (match) updateImageUrl(link, match[0].slice(0, -1));
    });
  }

  async function handleImgDrive(link, url) {
    return await req(url, 'GET', 'blob').then(r => {
      const reader = new FileReader();
      reader.onload = () => updateImageUrl(link, reader.result);
      reader.readAsDataURL(r.response);
    });
  }

  async function handleTurboimagehost(link, url) {
    return await req(url, 'GET').then(r => {
      const parser = new DOMParser();
      const doc = parser.parseFromString(r.responseText, 'text/html');
      const src = doc.body.querySelector('img#imageid')?.src
      if (src) updateImageUrl(link, src)
    });
  }

  function updateImageUrl(node, url) {
    node.title = url;
    if (url && autoPreload) {
      node.innerHTML = `<img src="${url}" style="max-width:${maxImgWidth}; max-height:${maxImgHeight};"/>`;
    }
  }
})()