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.

// ==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};"/>`;
    }
  }
})()