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.

< Feedback on PornoLab.net Thumbnail ExpanderEX

Review: Good - script works

§
Posted: 16-03-2025

imagevenue thumbs just disappear and there's an error after

url example: /forum/viewtopic.php?t=1777687

§
Posted: 16-03-2025

same for fastpic images on url: /forum/viewtopic.php?t=2073585 they were already fullsize, so why touch em??

§
Posted: 18-03-2025

same for imgbox thumbs here, they just disappear /forum/viewtopic.php?t=2821140

§
Posted: 18-03-2025
Edited: 18-03-2025

The error on the screen seems unrelated, but even just with this script alone the result seems to be the same:

§
Posted: 18-03-2025

edit: - ignore previous posts here, here's a summary and analysis, I have found 3 different problems with the script:

1) - it's pic4all which disappear on the 1st link now and imagevnue ones are just left as small thumbs: /forum/viewtopic.php?t=1777687 problem: "blacklisted" hosts and their thumbs just disappear - that shouldn't happen

2) /forum/viewtopic.php?t=2821140 - here there are a lot of photos then video screens, but to see video screens you have to wait almost forever for the photos to load first

  • it would be nice if the script didn't load anything for collapsed spoileres - e.g. I want to see only the video screens but I have to wait like 10+ minutes for the photos to load

3) the problem that the script touches already fullsized images is there as well - it makes them disappear and wait for the very long queue to reappear again, why? : /forum/viewtopic.php?t=2073585

  • it shouldn't do anything with non-thumbs
§
Posted: 07-05-2025

Lost account

Although I wouldn't bother myself with proof i appreciate your valuable attention. But must inform you that it was uploaded from my alt account, which I currently have no access to because I made the mistake of registering it with a temporary email and logged out without ensuring I could log back in. Didn't tried to recover it either cause i have zero legitimate proof that it's mine besides it was last logged in quite a while ago and has only this script which is solely purpose for it's existing. Now i just decide to not care about it associating with me.

So despite i think that cooler name is impossible for such masterpiece someone must republish it under his name and sadly with different title too.

On the list.

  1. That's because i mindlessly rewrote some functional which was in original script. Integrity is important even if it break things i wasn't aware about.
  2. I dont know better way besides manually enabling folds processing by keywords which is done by enableImageReplacementInsideFoldsWithTitleContains or combining with previous but adding gui action to it. Returning at the beginning of map with slightly modifying disableReplace logic will do the thing leaving non matching folds untouched.
  3. That was becase all topics i tested had url in the parentHref so it was prioritized over title which was actually url in this case. Unless it's /big/ it's still thumbnail.

Refined script

Which partially fixes your issues. I wish i could do fold but not today greasyfork not today.

// ==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 = '80vw';
  const maxImgHeight = '100vh';
  const blockedHosts = ['piccash.net', 'picclick.ru', 'pic4cash.ru', 'picspeed.ru', 'picforall.ru', 'freescreens.ru'];
  const enableImageReplacementInsideFoldsWithTitleContains = ['скрытый текст', 'Коврики', 'Примеры фото', 'Скринлист', 'Mb', 'Gb', 'Примеры', 'Скриншоты'];

  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 foldTitle = post.querySelector('.sp-head')?.textContent;
    const disable = !enableImageReplacementInsideFoldsWithTitleContains.some(e => foldTitle.includes(e));
    if(disable) return;

    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 queue = [];
    for (const link of links) {
      const url = link.title;
      const parentHref = link.parentNode?.href;

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

      if (isBlocked(url)) {
        updateImageUrl(link, url);
        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|webp)\?.+?"/);
      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) {
    if(url.includes('/big/')) return Promise.resolve(true);
    return await req(url, 'HEAD').then(response => {
      return !response.responseHeaders.replaceAll(' ', '').toLowerCase().includes('content-type:text/html');
    });
  }

  async function handleFastpic(link, parentHref) {
    const url = (parentHref ? (new URL(parentHref).pathname !== '/' ? parentHref : undefined) : undefined) || 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};"/>`;
    }
  }
})()
§
Posted: 07-05-2025

Thank you for the update

I dont know better way

My current easy solution is adding spoilers to the queue in reverse order.

But a good way would be to make a task which runs ~every second and checks which spoilers are open and which are not - and processes images only in opened spoilers ignoring still closed spoilers

§
Posted: 07-05-2025

Or a button next to a spoiler which opens that spoiler and processes the images inside

Post reply

Sign in to post a reply.