Pornolab Preview Light

affiche une preview sur la page de recherche pour les liens

Terá de instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Terá de instalar uma extensão como Tampermonkey para instalar este script.

Terá de instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Terá de instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Terá de instalar uma extensão como Tampermonkey para instalar este script.

Terá de instalar uma extensão de gestão de scripts de utilizador para instalar este script.

(Já tenho um gestor de scripts de utilizador, deixe-me instalá-lo!)

Terá de instalar uma extensão como Stylus para instalar este estilo.

Terá de instalar uma extensão como Stylus para instalar este estilo.

Terá de instalar uma extensão como Stylus para instalar este estilo.

Terá de instalar uma extensão de gestão de estilos de utilizador para instalar este estilo.

Terá de instalar uma extensão de gestão de estilos de utilizador para instalar este estilo.

Terá de instalar uma extensão de gestão de estilos de utilizador para instalar este estilo.

(Já tenho um gestor de estilos de utilizador, deixe-me instalá-lo!)

// ==UserScript==
// @name Pornolab Preview Light
// @version 3.4.0
// @description affiche une preview sur la page de recherche pour les liens
// @author seb-du17
// @license MIT
// @match *://pornolab.net/forum/tracker*
// @match *://pornolab.net/forum/viewforum*
// @match *://pornolab.net/forum/search*
// @run-at document-idle
// @grant none
// @namespace https://greasyfork.org/users/108513
// ==/UserScript==

/* jshint esversion: 11 */
/* global AbortController, IntersectionObserver, DOMParser, Image */

(function() {
'use strict';

// ========== Configuration ==========
const CONFIG = {
  MAX_CONCURRENT: 2,
  TIMEOUT: 10000,
  RETRY_DELAY: 3000,
  CACHE_SIZE: 200,
  DEBOUNCE: 100,
  IMG_WIDTH: 180,
  IMG_HEIGHT: 210,
  ROOT_MARGIN: '400px'
};

// ========== Cache LRU ==========
function createLRUCache(maxSize) {
  const cache = new Map();
  return {
    get: function(key) {
      if (!cache.has(key)) { return null; }
      const value = cache.get(key);
      cache.delete(key);
      cache.set(key, value);
      return value;
    },
    set: function(key, value) {
      if (cache.has(key)) {
        cache.delete(key);
      } else if (cache.size >= maxSize) {
        const firstKey = cache.keys().next().value;
        cache.delete(firstKey);
      }
      cache.set(key, value);
    },
    has: function(key) {
      return cache.has(key);
    }
  };
}

const cache = createLRUCache(CONFIG.CACHE_SIZE);
const failedUrls = new Set(); // urls ayant échoué une première fois
const requestQueue = [];
let activeRequests = 0;

// ========== CSS optimisé ==========
const style = document.createElement('style');
style.textContent = `
.pornolab-preview-wrapper {
  display: flex !important;
  align-items: flex-end !important;
  gap: 10px !important;
  width: 100% !important;
}
.pornolab-preview-wrapper-link {
  flex: 1 1 auto !important;
  min-width: 0 !important;
}
.pornolab-preview-img-container {
  width: ${CONFIG.IMG_WIDTH}px !important;
  height: ${CONFIG.IMG_HEIGHT}px !important;
  flex-shrink: 0 !important;
  overflow: hidden !important;
  border: 1px solid #444 !important;
  border-radius: 2px !important;
  content-visibility: auto !important;
}
.pornolab-preview-img-container img {
  width: 100% !important;
  height: 100% !important;
  object-fit: cover !important;
  display: block !important;
}
`;
document.head.appendChild(style);

// ========== Fetch avec timeout ==========
function fetchWithTimeout(url, timeout) {
  const controller = new AbortController();
  const timeoutId = setTimeout(function() {
    controller.abort();
  }, timeout);

  return fetch(url, {
    signal: controller.signal,
    priority: 'low'
  }).then(function(response) {
    clearTimeout(timeoutId);
    return response;
  }).catch(function(error) {
    clearTimeout(timeoutId);
    throw error;
  });
}

// ========== Extraction URL preview ==========
function extractPreviewUrl(doc) {
  const firstImg = doc.querySelector('.postImg');
  if (!firstImg) { return ''; }
  return (firstImg.title || firstImg.getAttribute('data-src') || firstImg.getAttribute('src') || '');
}

// ========== Queue de requêtes ==========
function processQueueItem(item) {
  const resolve = item.resolve;
  const url = item.url;

  fetchWithTimeout(url, CONFIG.TIMEOUT)
    .then(function(response) {
      if (!response.ok) { return ''; }
      return response.text();
    })
    .then(function(html) {
      if (!html) { return []; }
      const doc = new DOMParser().parseFromString(html, 'text/html');
      const u = extractPreviewUrl(doc);
      const urls = u ? [u] : [];
      cache.set(url, urls);
      return urls;
    })
    .then(resolve)
    .catch(function() {
      // Échec : marquer pour retry, ne pas mettre en cache
      failedUrls.add(url);
      resolve([]);
    })
    .finally(function() {
      activeRequests--;
      if (requestQueue.length) {
        processQueue();
      }
    });
}

function processQueue() {
  while (requestQueue.length && activeRequests < CONFIG.MAX_CONCURRENT) {
    const item = requestQueue.shift();
    activeRequests++;
    processQueueItem(item);
  }
}

function fetchPreviewUrls(url) {
  const cached = cache.get(url);
  if (cached !== null) {
    return Promise.resolve(cached);
  }

  return new Promise(function(resolve) {
    requestQueue.push({ resolve: resolve, url: url });
    processQueue();
  });
}

// ========== Retry silencieux ==========
function scheduleRetry(url, link) {
  setTimeout(function() {
    // Ne retenter que si toujours pas en cache
    if (cache.has(url)) { return; }
    failedUrls.delete(url);

    fetchPreviewUrls(url).then(function(urls) {
      if (!urls.length) { return; }
      // Le lien est peut-être déjà remplacé par un wrapper, vérifier
      if (link.parentNode) {
        const wrapper = createPreviewWrapper(urls, link);
        link.parentNode.replaceChild(wrapper, link);
      }
    });
  }, CONFIG.RETRY_DELAY);
}

// ========== Insert preview ==========
function createPreviewWrapper(urls, link) {
  const wrapper = document.createElement('div');
  wrapper.className = 'pornolab-preview-wrapper';

  const linkWrapper = document.createElement('div');
  linkWrapper.className = 'pornolab-preview-wrapper-link';
  linkWrapper.appendChild(link.cloneNode(true));

  const imgContainer = document.createElement('div');
  imgContainer.className = 'pornolab-preview-img-container';

  const img = new Image();
  img.src = urls[0];
  img.loading = 'lazy';
  img.decoding = 'async';
  img.onerror = function() {
    imgContainer.remove();
  };

  imgContainer.appendChild(img);

  wrapper.appendChild(linkWrapper);
  wrapper.appendChild(imgContainer);
  return wrapper;
}

function insertPreview(link) {
  if (link.dataset.previewDone) { return; }

  const href = link.getAttribute('href');
  if (!href || href.charAt(0) !== '.') { return; }

  link.dataset.previewDone = '1';

  const url = 'https://pornolab.net/forum' + href.slice(1);

  fetchPreviewUrls(url).then(function(urls) {
    if (!urls.length) {
      // Fetch raté : programmer un retry silencieux
      if (failedUrls.has(url)) {
        scheduleRetry(url, link);
      }
      return;
    }
    const wrapper = createPreviewWrapper(urls, link);
    if (link.parentNode) {
      link.parentNode.replaceChild(wrapper, link);
    }
  });
}

// ========== Process links avec IntersectionObserver ==========
const pendingLinks = new Set();
let debounceTimer;
let intersectionObserver;

function handleIntersection(entry) {
  if (entry.isIntersecting) {
    insertPreview(entry.target);
    intersectionObserver.unobserve(entry.target);
    pendingLinks.delete(entry.target);
  }
}

intersectionObserver = new IntersectionObserver(
  function(entries) {
    for (let i = 0; i < entries.length; i++) {
      handleIntersection(entries[i]);
    }
  },
  { rootMargin: CONFIG.ROOT_MARGIN }
);

function processLinks() {
  const selector = '.tLink:not([data-preview-done]), .tt-text:not([data-preview-done])';
  const links = document.querySelectorAll(selector);

  for (let i = 0; i < links.length; i++) {
    const link = links[i];
    if (!pendingLinks.has(link)) {
      pendingLinks.add(link);
      intersectionObserver.observe(link);
    }
  }
}

function debouncedProcessLinks() {
  clearTimeout(debounceTimer);
  debounceTimer = setTimeout(processLinks, CONFIG.DEBOUNCE);
}

// ========== MutationObserver ==========
const mutationObserver = new MutationObserver(debouncedProcessLinks);

function init() {
  processLinks();
  mutationObserver.observe(document.body, {
    childList: true,
    subtree: true
  });
}

if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', init);
} else {
  init();
}
})();