Sleazy Fork is available in English.

iStripper downloader

Replace small images by their full size and add a download button to download zip of all images + cover

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 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         iStripper downloader
// @description  Replace small images by their full size and add a download button to download zip of all images + cover
// @version      1.0.0
// @author       BreatFR
// @namespace    http://usercssjs.breat.fr/i/istripper
// @match        *://*.istripper.com/*
// @require      https://cdn.jsdelivr.net/npm/[email protected]/umd/index.min.js
// @copyright    2025, BreatFR (https://breat.fr)
// @icon         https://www.istripper.com/favicons/istripper/apple-icon-120x120.png
// @license      AGPL-3.0-or-later; https://www.gnu.org/licenses/agpl-3.0.txt
// @grant        GM_xmlhttpRequest
// @grant        GM_download
// @run-at       document-end
// ==/UserScript==

(function() {
  'use strict';

  const style = document.createElement('style');
  style.textContent = `
    a.box:hover div.img.icard { background-position: initial; }
    [src*="https://www.istripper.com/free/sets"] { transition: transform .2s ease-in-out; }
    a.box:hover [src*="https://www.istripper.com/free/sets"] { transform: scale(1.1); transition: transform .2s ease-in-out; }

    .istripper-download-btn {
      align-items: center;
      background-color: rgba(24, 24, 24, .2);
      border: none;
      border-radius: .5em;
      color: #fff;
      cursor: pointer;
      display: flex;
      flex-direction: column;
      font-family: poppins, cursive;
      font-size: 1.15rem;
      gap: 1em;
      justify-content: center;
      line-height: 1.5;
      padding: .5em 1em;
      position: absolute;
      transition: background-color .3s ease, box-shadow .3s ease;
      z-index: 9999;
    }
    .istripper-download-btn:hover {
      background-color: rgba(255, 80, 80, .85);
      box-shadow: 0 0 2em rgba(255, 80, 80, .85);
    }
    @keyframes spinLoop { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
    .istripper-btn-icon.spin { animation: spinLoop 1s linear infinite; }
    @keyframes pulseLoop {
      0% { transform: scale(1); opacity: 1; }
      50% { transform: scale(1.1); opacity: 0.7; }
      100% { transform: scale(1); opacity: 1; }
    }
    .istripper-btn-icon.pulse { animation: pulseLoop 1.8s ease-in-out infinite; }
    .istripper-btn-icon { font-size: 3em; line-height: 1em; }

    #top {
      aspect-ratio: 1 / 1;
      background: transparent;
      border: none;
      bottom: 1em;
      cursor: pointer;
      font-size: 1.2em;
      left: 1em;
      position: fixed;
      z-index: 9999;
    }
  `;
  document.head.appendChild(style);

  function updateImagesAndOverlays() {
    const images = document.querySelectorAll('img.illustration');
    images.forEach(img => {
      if (!img.src.includes("_card_full")) img.src = img.src.replace(".jpg", "_card_full.jpg");
    });

    const divCollection = document.querySelectorAll('.collection-label');
    divCollection.forEach(div => { div.style.pointerEvents = 'none'; });

    const divsWithOverlay = document.querySelectorAll('div[data-overlay]');
    divsWithOverlay.forEach(div => { div.removeAttribute('data-overlay'); });

    const overlayImages = document.querySelectorAll('img.overlay');
    overlayImages.forEach(img => { img.parentNode && img.parentNode.removeChild(img); });

    const icardDivs = document.querySelectorAll('div.img.icard');
    icardDivs.forEach(div => {
      const backgroundImageURL = div.style.backgroundImage;
      if (backgroundImageURL && !backgroundImageURL.includes("_card_full")) {
        div.style.alignItems = 'center';
        div.style.backgroundPosition = 'center center';
        div.style.backgroundSize = '0';
        div.style.display = 'flex';
        div.style.justifyContent = 'center';
      }
    });

    const icardImg = document.querySelectorAll('div.img.icard > img');
    icardImg.forEach(img => {
      if (!img.src.includes("_card_full") && img.classList.contains("hidden")) {
        if (img.src.endsWith(".jpg")) {
          img.src = img.src.replace(".jpg", "_card_full.jpg");
          img.removeAttribute('class');
          img.style.height = "242px";
          img.style.opacity = '1';
          img.style.pointerEvents = 'auto';
          img.style.width = "auto";
        }
      }
    });

    const icardOverlayDivs = document.querySelectorAll('div.icard-overlay');
    icardOverlayDivs.forEach(div => {
      div.style.pointerEvents = 'none';
      div.style.position = 'absolute';
      div.style.top = '0';
      div.style.left = '0';
      div.removeAttribute('onmouseover');
    });
  }

  updateImagesAndOverlays();
  setInterval(updateImagesAndOverlays, 500);

  // ===== Helpers UI =====
  function setButtonContent(btn, icon, label) {
    let iconEl = btn.querySelector('.istripper-btn-icon');
    let labelEl = btn.querySelector('.istripper-btn-label');

    if (!iconEl) {
      iconEl = document.createElement('div');
      iconEl.className = 'istripper-btn-icon';
      btn.appendChild(iconEl);
    }
    if (!labelEl) {
      labelEl = document.createElement('div');
      labelEl.className = 'istripper-btn-label';
      btn.appendChild(labelEl);
    }

    iconEl.textContent = icon;
    labelEl.textContent = label;
  }

  function setIconAnimation(btn, type) {
    const icon = btn.querySelector('.istripper-btn-icon');
    if (!icon) return;
    icon.classList.remove('spin', 'pulse');
    void icon.offsetWidth;
    if (type) icon.classList.add(type);
  }

  function updateIconWithAnimation(btn, icon, label, animationClass) {
    setButtonContent(btn, icon, label);
    requestAnimationFrame(() => setIconAnimation(btn, animationClass));
  }

  function resetDownloadButton(btn) {
    btn.disabled = false;
    updateIconWithAnimation(btn, '📦', 'Download pack + cover', null);
  }

  function sanitizeZipName(name) {
    return (name || "istripper_pack").trim().replace(/[\\/:*?"<>|]/g, "_");
  }

  // ===== Binary download via GM (avoids CORS issues) =====
  function gmGetArrayBuffer(url) {
    return new Promise((resolve, reject) => {
      GM_xmlhttpRequest({
        method: "GET",
        url,
        responseType: "arraybuffer",
        onload: (res) => {
          if (res.status >= 200 && res.status < 300 && res.response) resolve(res.response);
          else reject(new Error(`HTTP ${res.status} for ${url}`));
        },
        onerror: () => reject(new Error(`Network error for ${url}`)),
      });
    });
  }

  async function gmGetUint8(url) {
    const ab = await gmGetArrayBuffer(url);
    return new Uint8Array(ab);
  }

  // ===== ZIP logic: unzip -> flatten photos/ -> add cover -> rezip =====
  function stripPhotosFolder(entries) {
    const out = {};
    for (const [path, data] of Object.entries(entries)) {
      if (!data || !data.length) continue;
      out[path.replace(/^photos\//i, "")] = data;
    }
    return out;
  }

  function ensureUniqueName(map, desired) {
    if (!map[desired]) return desired;
    const dot = desired.lastIndexOf(".");
    const base = dot >= 0 ? desired.slice(0, dot) : desired;
    const ext  = dot >= 0 ? desired.slice(dot) : "";
    let i = 2;
    while (map[`${base}_${i}${ext}`]) i++;
    return `${base}_${i}${ext}`;
  }

  function getOfficialZipUrl() {
    const a = document.querySelector('a > .bt-download')?.closest('a');
    if (a?.href && /\/fileaccess\/zip\/[a-z]\d+(\b|\/|$)/i.test(a.href)) return a.href;

    for (const link of document.querySelectorAll('a[href*="/fileaccess/zip/"]')) {
      if (/\/fileaccess\/zip\/[a-z]\d+(\b|\/|$)/i.test(link.href)) return link.href;
    }
    return null;
  }

  async function rebuildZipFlatWithCover({ zipUrl, coverUrl, zipNameBase, btn }) {
    updateIconWithAnimation(btn, '🌀', 'Downloading official ZIP...', 'spin');
    const zipUint8 = await gmGetUint8(zipUrl);

    updateIconWithAnimation(btn, '📦', 'Unzipping...', 'pulse');
    const entries = await new Promise((resolve, reject) => {
      fflate.unzip(zipUint8, (err, data) => (err ? reject(err) : resolve(data)));
    });

    const flat = stripPhotosFolder(entries);

    updateIconWithAnimation(btn, '🖼️', 'Downloading cover...', 'pulse');
    const coverUint8 = await gmGetUint8(coverUrl);
    const coverName = ensureUniqueName(flat, 'cover.jpg');
    flat[coverName] = coverUint8;

    updateIconWithAnimation(btn, '📦', `Creating ZIP (${Object.keys(flat).length} files)...`, null);
    const newZip = await new Promise((resolve, reject) => {
      fflate.zip(flat, { level: 0 }, (err, data) => (err ? reject(err) : resolve(data)));
    });

    const zipName = sanitizeZipName(zipNameBase) + ".zip";
    const blobUrl = URL.createObjectURL(new Blob([newZip], { type: "application/zip" }));

    updateIconWithAnimation(btn, '📥', 'Saving ZIP...', 'pulse');
    GM_download({
      url: blobUrl,
      name: zipName,
      saveAs: true,
      onload: () => setTimeout(() => URL.revokeObjectURL(blobUrl), 30_000),
      onerror: (err) => {
        URL.revokeObjectURL(blobUrl);
        console.error('[iStripper Collector] ❌ GM_download failed:', err);
        alert('ZIP download failed.');
      }
    });
  }

  // ===== Download button injection =====
  function addDownloadButton() {
    const urlMatch = location.href.match(/^https:\/\/www\.istripper\.com\/[^\/]*\/?models\/[^\/]+\/[^\/]+$/);
    const isPackPage = urlMatch && document.querySelector('img.illustration') && document.querySelectorAll('a.picture').length > 0;
    const hasDownloadLink = !!document.querySelector('a > .bt-download');
    if (!isPackPage || !hasDownloadLink) return;

    if (document.querySelector('.istripper-download-btn')) return;

    console.log('[iStripper Collector] Injecting download button');
    const btn = document.createElement('button');
    btn.className = 'istripper-download-btn';
    btn.innerHTML = `
      <div class="istripper-btn-icon">📦</div>
      <div class="istripper-btn-label">Download pack + cover</div>
    `;

    const blackBox = document.querySelector('div.cta.showDetail');
    const wrapper = document.querySelector('div[style="background: #f6f6f6;"]');

    if (blackBox && wrapper) {
      wrapper.style.position = 'relative';
      wrapper.appendChild(btn);

      requestAnimationFrame(() => {
        const boxRect = blackBox.getBoundingClientRect();
        const wrapperRect = wrapper.getBoundingClientRect();
        const btnRect = btn.getBoundingClientRect();

        const top = boxRect.top - wrapperRect.top - btnRect.height - 32;
        const left = boxRect.left - wrapperRect.left + (boxRect.width / 2) - (btnRect.width / 2);

        btn.style.top = `${top}px`;
        btn.style.left = `${left}px`;
      });
    } else {
      console.warn('[iStripper Collector] Positioning fallback');
      document.body.appendChild(btn);
    }

    btn.addEventListener('click', async () => {
      btn.disabled = true;

      try {
        const zipUrl = getOfficialZipUrl();
        if (!zipUrl) throw new Error('Official ZIP link not found.');

        const coverImg = document.querySelector('img.illustration');
        if (!coverImg?.src) throw new Error('Cover image not found.');

        const coverUrl = coverImg.src.includes('_card_full')
          ? coverImg.src.replace(/\.jpg(\?.*)?$/i, '.jpg$1')
          : coverImg.src.replace(/\.jpg(\?.*)?$/i, '_card_full.jpg$1');

        const h2 = document.querySelector('h2.mdlnav');
        const zipNameBase = h2 ? h2.textContent : 'istripper_pack';

        await rebuildZipFlatWithCover({ zipUrl, coverUrl, zipNameBase, btn });

        updateIconWithAnimation(btn, '✅', 'ZIP ready', null);
      } catch (e) {
        console.error('[iStripper Collector] ❌ Failed:', e);
        alert(`Failed: ${e?.message || e}`);

        // (plus besoin de remettre "📦 Download..." ici, le finally reset tout)
        updateIconWithAnimation(btn, '❌', 'Failed', null);
      } finally {
        // Laisse le user voir "ZIP ready" (ou "Failed") 2s, puis reset état initial
        setTimeout(() => resetDownloadButton(btn), 2000);
      }
    });
  }

  addDownloadButton();
})();