iStripper downloader

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

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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();
})();