Adds a convenience download button for owned image packs using the official ZIP link, includes the public full-size cover, and repacks the archive with a cleaner structure.
// ==UserScript== // @name iStripper downloader // @description Adds a convenience download button for owned image packs using the official ZIP link, includes the public full-size cover, and repacks the archive with a cleaner structure. // @version 1.0.2 // @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/v2/apple-touch-icon.png // @license AGPL-3.0-or-later; https://www.gnu.org/licenses/agpl-3.0.txt // @grant GM_download // @grant GM_xmlhttpRequest // @run-at document-end // // ==/UserScript== (function() { 'use strict'; const style = document.createElement('style'); style.textContent = ` .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-self: center; line-height: 1.5; margin-top: 1em; 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; } /* Notifications discrètes */ .istripper-toast { position: fixed; bottom: 20px; right: 20px; background: rgba(24, 24, 24, 0.95); color: #fff; padding: 12px 16px; border-radius: 6px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); z-index: 999999; font-family: Poppins, sans-serif; font-size: 1.2rem; max-width: 400px; word-break: break-word; opacity: 0; transform: translateY(20px); transition: all 0.3s ease; border-left: 4px solid #40c057; } .istripper-toast.show { opacity: 1; transform: translateY(0); } .istripper-toast.error { border-left-color: #fa5252; } .istripper-toast.info { border-left-color: #339af0; } `; document.head.appendChild(style); // 🔔 Fonction pour afficher une notification discrète function showNotification(message, type = 'success', duration = 2500) { const toast = document.createElement('div'); toast.className = 'istripper-toast' + (type === 'error' ? ' error' : type === 'info' ? ' info' : ''); toast.textContent = message; document.body.appendChild(toast); requestAnimationFrame(() => { toast.classList.add('show'); }); setTimeout(() => { toast.classList.remove('show'); setTimeout(() => toast.remove(), 300); }, duration); } function updateCoverImage() { const images = document.querySelectorAll('img.w-full.h-full.object-cover'); images.forEach(img => { if (!img.src.includes("_card_full")) img.src = img.src.replace("cardsoft_2x.jpg", "card_full.jpg"); }); } updateCoverImage(); setInterval(updateCoverImage, 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 link = document.querySelector('a[href^="https://www.istripper.com/fileaccess/zip/"]'); if (link?.href && /\/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 officialZipLink = document.querySelector('a[href^="https://www.istripper.com/fileaccess/zip/"]'); const coverImg = document.querySelector('img.w-full.h-full.object-cover'); const isPackPage = !!officialZipLink && !!coverImg; if (!isPackPage) 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 showRoot = document.querySelector('main .relative.w-full'); const blackBox = showRoot?.querySelector('div.relative.overflow-hidden.rounded-2xl[style*="rgba(210, 4, 45, 0.3)"]'); if (blackBox) { blackBox.style.overflow = 'visible'; blackBox.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 = blackBox?.querySelector('img[src*="card_full.jpg"]') || blackBox?.querySelector('img.w-full.h-full.object-cover'); if (!coverImg?.src) throw new Error('Cover image not found.'); const coverUrl = coverImg.src; const h1 = blackBox?.querySelector('h1'); const zipNameBase = h1 ? h1.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(); setInterval(addDownloadButton, 1000); // ==== Improve VR download by copying clean MP4 filename ==== function sanitizeFileNamePart(str) { return (str || "") .trim() .replace(/[\x00-\x1f\x80-\x9f]/g, "") .replace(/[\\/:*?"<>|]/g, "_") .replace(/\s+/g, " ") .slice(0, 180); } function getVrShowFileName() { const h1 = document.querySelector("h1"); const title = sanitizeFileNamePart(h1?.textContent || "iStripper VR"); const subtitle = h1?.parentElement?.querySelector("p.font-instrument-serif") || h1?.parentElement?.querySelector("p.italic") || [...document.querySelectorAll("h1 ~ p")] .find(p => p.textContent?.trim()); const subtitleText = sanitizeFileNamePart(subtitle?.textContent || ""); return subtitleText ? `${title} - ${subtitleText}.mp4` : `${title}.mp4`; } function patchVrLinks() { const links = document.querySelectorAll('a[href*="/fileaccess/vr180/"]'); links.forEach(link => { const filename = getVrShowFileName(); link.dataset.vrPatched = "1"; link.setAttribute("download", filename); link.download = filename; link.title = `Download as: ${filename}`; }); } async function copyTextToClipboard(text) { try { await navigator.clipboard.writeText(text); return true; } catch (e) { console.warn("[iStripper Collector] Clipboard API failed:", e); return false; } } patchVrLinks(); setInterval(patchVrLinks, 1000); document.addEventListener("click", async (event) => { const link = event.target.closest('a[href*="/fileaccess/vr180/"]'); if (!link) return; const filename = link.download || link.getAttribute("download") || getVrShowFileName(); const copied = await copyTextToClipboard(filename); console.log( copied ? "[iStripper Collector] VR filename copied:" : "[iStripper Collector] VR filename copy failed:", filename ); // 🔔 Notification discrète pour VR if (copied) { showNotification('✅ Title copied', 'success', 2000); } else { showNotification('❌ Copy failed', 'error', 2500); } }, true); })();