您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Creates a button to download all images from galleries & favorite photos in a single .zip file. Large collections are split into multiple zip files.
当前为
// ==UserScript== // @name GPT gallery downloader (GPTGD) // @namespace _pc // @version 6.5 // @license MIT // @description Creates a button to download all images from galleries & favorite photos in a single .zip file. Large collections are split into multiple zip files. // @author verydelight // @match https://www.gayporntube.com/user/* // @match https://www.gayporntube.com/galleries/* // @connect gayporntube.com // @connect media-1-albums.gayporntube.com // @connect media-2-albums.gayporntube.com // @icon https://www.gayporntube.com/favicon.ico // @compatible Firefox Tampermonkey // @grant GM.xmlHttpRequest // @grant GM.download // @require https://update.greasyfork.org/scripts/473358/1237031/JSZip.js // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/FileSaver.min.js // ==/UserScript== (function() { 'use strict'; const enableDebugLog = false; // Set to true for detailed console logging console.log("GPTGD: Script is starting."); const profileHashPattern = /^#page.*-favorite_photos.*$/; const allImageUrls = new Set(); const baseUrl = window.location.href.split('#')[0]; let zip; let downloadStatus; let progressElement; let downloadButton; let totalImagesToDownload = 0; let imagesDownloadedOverall = 0; let mainPageRetryCount = 0; const maxRetries = 10; const CHUNK_SIZE = 50; const CHUNK_THRESHOLD = 75; const convertToFullSizeUrl = (url) => { const pattern1 = /\/thumbs/; const pattern2 = /\/main\/\d{1,4}x\d{1,4}/; return url.replace(pattern1, '').replace(pattern2, '/sources'); }; const extractAndStoreImages = (container) => { if (enableDebugLog) console.log("GPTGD: Extracting images from container:", container); const images = container.querySelectorAll('img'); if (enableDebugLog) console.log(`GPTGD: Found ${images.length} images.`); images.forEach((img) => { let imageUrl = img.src; if (!imageUrl || imageUrl.startsWith('data:')) { imageUrl = img.getAttribute('data-src'); } if (imageUrl) { const fullSizeUrl = convertToFullSizeUrl(imageUrl); allImageUrls.add(fullSizeUrl); if (enableDebugLog) console.log(`GPTGD: Added image URL to set: ${fullSizeUrl}`); } }); if (enableDebugLog) console.log(`GPTGD: Current total unique images: ${allImageUrls.size}`); }; const downloadFile = (url, filename, zipInstance) => { if (enableDebugLog) console.log(`GPTGD: Starting download for: ${url}`); return new Promise((resolve, reject) => { GM.xmlHttpRequest({ method: 'GET', responseType: 'blob', url: url, headers: { "Content-Type": "image/jpeg", "Accept": "image/jpeg" }, onload: (response) => { if (enableDebugLog) console.log(`GPTGD: Download successful for: ${filename}`); const blob = new Blob([response.response], { type: 'image/jpeg' }); zipInstance.file(filename, blob, { binary: true }); resolve(); }, onerror: (err) => { console.error("GPTGD: Error in fetching and downloading file: ", url, err); reject(err); } }); }); }; const scrapeAllPages = (totalPages, updateStatusCallback) => { if (enableDebugLog) console.log(`GPTGD: Starting multi-page scraping for ${totalPages} pages.`); return new Promise((resolve) => { let currentPage = 1; const processNextPage = () => { if (currentPage > totalPages) { if (enableDebugLog) console.log(`GPTGD: Finished scraping. Found a total of ${allImageUrls.size} unique images across all pages.`); resolve(); return; } updateStatusCallback(`Processing page ${currentPage}/${totalPages}...`); const iframe = document.createElement('iframe'); iframe.style.display = 'none'; iframe.src = `${baseUrl}#page${currentPage}-favorite_photos`; if (enableDebugLog) console.log(`GPTGD: Loading iframe for page ${currentPage}: ${iframe.src}`); iframe.onload = () => { if (enableDebugLog) console.log(`GPTGD: Iframe loaded for page ${currentPage}.`); let iframeRetryCount = 0; const maxIframeRetries = 10; const checkIframeContent = () => { try { const doc = iframe.contentDocument || iframe.contentWindow.document; const imageContainer = doc.querySelector('#custom_fav_albums_images_fav_albums_images'); if (imageContainer) { if (enableDebugLog) console.log(`GPTGD: Found image container in iframe for page ${currentPage}.`); extractAndStoreImages(imageContainer); if (enableDebugLog) console.log(`GPTGD: Scraped page ${currentPage}. Current images: ${allImageUrls.size}`); iframe.remove(); currentPage++; processNextPage(); } else if (iframeRetryCount < maxIframeRetries) { iframeRetryCount++; if (enableDebugLog) console.log(`GPTGD: Image container not found in iframe for page ${currentPage}. Retrying... (Attempt ${iframeRetryCount}/${maxIframeRetries})`); setTimeout(checkIframeContent, 500); } else { console.error(`GPTGD: Failed to find image container in iframe for page ${currentPage} after multiple retries.`); iframe.remove(); currentPage++; processNextPage(); } } catch (e) { console.error(`GPTGD: Error processing iframe content for page ${currentPage}:`, e); iframe.remove(); currentPage++; processNextPage(); } }; checkIframeContent(); }; document.body.appendChild(iframe); }; processNextPage(); }); }; const calculateTotalParts = (totalImages) => { if (totalImages <= CHUNK_THRESHOLD) { return 1; } let parts = 1; let imagesLeft = totalImages - CHUNK_SIZE; while (imagesLeft > 0) { if (imagesLeft > CHUNK_THRESHOLD) { parts++; imagesLeft -= CHUNK_SIZE; } else { parts++; imagesLeft = 0; } } return parts; }; const processAndZipChunk = async (urls, partNumber, totalParts, isSingleChunk) => { zip = new JSZip(); let downloadTime = 0; const totalInChunk = urls.length; const partText = isSingleChunk ? '' : `part ${partNumber}/${totalParts}: `; for (let i = 0; i < totalInChunk; i++) { const url = urls[i]; const fileName = url.split('/').pop(); const downloadStart = Date.now(); try { await downloadFile(url, fileName, zip); const downloadEnd = Date.now(); downloadTime += downloadEnd - downloadStart; imagesDownloadedOverall++; const overallProgressPercent = Math.round((imagesDownloadedOverall / totalImagesToDownload) * 100); progressElement.setAttribute("value", overallProgressPercent); const averageTimePerFile = downloadTime / (i + 1); const downloadEstimate = Math.round((averageTimePerFile * (totalImagesToDownload - imagesDownloadedOverall)) / 1000); const timeRemaining = downloadEstimate > 0 ? ` (ca.: ${formatTime(downloadEstimate)} remaining)` : ''; if (partText){ downloadStatus.textContent = `Downloading ${partText}image ${i + 1}/${totalInChunk} [${imagesDownloadedOverall}/${totalImagesToDownload}] ${timeRemaining}`; }else{ downloadStatus.textContent = `Downloading image ${i + 1}/${totalInChunk} ${timeRemaining}`; } } catch (e) { console.error("GPTGD: Failed to download image: ", url, e); downloadStatus.textContent = `Error downloading image ${imagesDownloadedOverall + 1} (${i + 1} of current chunk). Skipping...`; } } downloadStatus.textContent = isSingleChunk ? `Downloaded. Zipping files...` : `Part ${partNumber}/${totalParts} downloaded. Zipping files...`; let finalZipFileName = getZipFileName(); if (!isSingleChunk) { finalZipFileName += `_part${partNumber}`; } const content = await zip.generateAsync({ type: "blob", compression: "DEFLATE", compressionOptions: { level: 6 } }); saveAs(content, finalZipFileName); downloadStatus.textContent = isSingleChunk ? `Saved.` : `Part ${partNumber}/${totalParts} saved.`; if (enableDebugLog) console.log(`GPTGD: Part ${partNumber} download complete and file saved.`); }; const startDownload = async () => { if (enableDebugLog) console.log("GPTGD: Starting download process."); downloadStatus.textContent = "Starting to download images..."; progressElement.style.display = "block"; let imageUrls = Array.from(allImageUrls); totalImagesToDownload = imageUrls.length; imagesDownloadedOverall = 0; if (enableDebugLog) console.log(`GPTGD: Total images to download: ${totalImagesToDownload}`); const isSingleChunk = totalImagesToDownload <= CHUNK_THRESHOLD; const totalParts = calculateTotalParts(totalImagesToDownload); let partNumber = 1; while (imageUrls.length > 0) { let chunkSize; if (imageUrls.length > CHUNK_THRESHOLD) { chunkSize = CHUNK_SIZE; } else { chunkSize = imageUrls.length; } const chunk = imageUrls.splice(0, chunkSize); await processAndZipChunk(chunk, partNumber, totalParts, isSingleChunk); partNumber++; } downloadStatus.textContent = "All parts downloaded and saved!"; if (enableDebugLog) console.log("GPTGD: All downloads complete."); }; const getZipFileName = () => { if (window.location.href.includes('/galleries/')) { const zipFileName = document.querySelector('h1.title').innerText .replace(/[^a-zA-Z0-9-_ ]/g, '') .replace(/\s+/g, ' ') .substring(0, 245) .replace(/^_+|_+$/g, '') .trim(); return `GPT Gallery - ${zipFileName}`; } else { const userOrGallery = window.location.pathname.split('/')[2]; return `GPT Favorites - ${userOrGallery}`; } }; function formatTime(seconds) { const minutes = Math.floor(seconds / 60); const secs = seconds % 60; const formattedMinutes = minutes < 10 ? "0" + minutes : minutes; const formattedSeconds = secs < 10 ? "0" + secs : secs; return minutes > 0 ? `${formattedMinutes}:${formattedSeconds} minutes` : `${formattedSeconds} seconds`; } const createDownloadUI = (container) => { if (enableDebugLog) console.log("GPTGD: Creating download UI."); const existingButton = document.querySelector('.gptgd-bttn'); if (existingButton) existingButton.remove(); downloadStatus = document.createElement("div"); progressElement = document.createElement("progress"); downloadButton = document.createElement("button"); if (window.location.href.includes('/galleries/')) { downloadButton.textContent = "Download gallery"; } else if (window.location.href.includes('/user/')){ downloadButton.textContent = "Download favourites"; } downloadButton.type = "button"; downloadButton.classList.add('gptgd-bttn'); downloadButton.style.cssText = "padding: 10px 20px; font-size: 16px; font-weight: bold; color: white; background-color: #007BFF; border: none; border-radius: 5px; cursor: pointer; margin-top: 10px;"; progressElement.setAttribute("value", 0); progressElement.setAttribute("max", 100); progressElement.style.width = "100%"; progressElement.style.height = "20px"; progressElement.style.display = "none"; downloadStatus.style.cssText = "margin-top: 10px; font-style: italic;"; const h2 = container.querySelector('h2'); const h1 = container.querySelector('h1'); if (h2) { h2.insertAdjacentElement('afterend', downloadButton); downloadButton.insertAdjacentElement('afterend', downloadStatus); downloadStatus.insertAdjacentElement('afterend', progressElement); } else if (h1) { h1.insertAdjacentElement('afterend', downloadButton); downloadButton.insertAdjacentElement('afterend', downloadStatus); downloadStatus.insertAdjacentElement('afterend', progressElement); } else { container.prepend(downloadButton, downloadStatus, progressElement); } if (enableDebugLog) console.log("GPTGD: Download UI created successfully."); }; const handleDOMChanges = () => { if (enableDebugLog) console.log("GPTGD: handleDOMChanges called."); if (enableDebugLog) console.log(`GPTGD: Current URL is ${window.location.href}`); const isGalleryPage = window.location.href.includes('/galleries/'); const isProfilePage = window.location.href.includes('/user/') && window.location.hash.match(profileHashPattern); if (enableDebugLog) console.log(`GPTGD: isGalleryPage = ${isGalleryPage}, isProfilePage = ${isProfilePage}`); let container = null; if (isGalleryPage) { container = document.querySelector('#album_view_album_view'); } else if (isProfilePage) { container = document.querySelector('#custom_fav_albums_images_fav_albums_images'); } if (container) { mainPageRetryCount = 0; const buttonExists = document.querySelector('.gptgd-bttn'); if (!buttonExists) { if (enableDebugLog) console.log("GPTGD: Required container found and button does not exist. Creating UI."); allImageUrls.clear(); createDownloadUI(container); downloadButton.addEventListener("click", async () => { if (enableDebugLog) console.log("GPTGD: Download button clicked."); downloadButton.style.display = "none"; if (isGalleryPage) { if (enableDebugLog) console.log("GPTGD: Gallery download logic triggered."); downloadStatus.textContent = "Processing images on current page..."; const images = document.querySelectorAll('#album_view_album_view > #tab5 img'); if (enableDebugLog) console.log(`GPTGD: Found ${images.length} images on the gallery page.`); images.forEach((img) => { let imageUrl = img.getAttribute('data-src'); if (imageUrl) { const fullSizeUrl = convertToFullSizeUrl(imageUrl); allImageUrls.add(fullSizeUrl); } }); if (enableDebugLog) console.log(`GPTGD: Extracted ${allImageUrls.size} image URLs from the gallery page.`); startDownload(); } else if (isProfilePage) { if (enableDebugLog) console.log("GPTGD: Profile download logic triggered."); const paginationContainer = document.querySelector('#custom_fav_albums_images_fav_albums_images_pagination'); if (paginationContainer) { const nextButton = paginationContainer.querySelector('li.next'); const lastPageLi = nextButton?.previousElementSibling; const totalPages = parseInt(lastPageLi?.textContent.trim(), 10) || 1; if (enableDebugLog) console.log(`GPTGD: Pagination found. Total pages: ${totalPages}`); downloadStatus.textContent = `Processing page 1/${totalPages}...`; await scrapeAllPages(totalPages, (status) => { downloadStatus.textContent = status; }); } else { if (enableDebugLog) console.log("GPTGD: No pagination container found, assuming single page."); downloadStatus.textContent = `Processing images on current page...`; extractAndStoreImages(container); } startDownload(); } }); } else { if (enableDebugLog) console.log("GPTGD: Button already exists, no need to recreate UI."); } } else if (mainPageRetryCount < maxRetries) { mainPageRetryCount++; if (enableDebugLog) console.log(`GPTGD: Container not found. Retrying in 500ms... (Attempt ${mainPageRetryCount}/${maxRetries})`); setTimeout(handleDOMChanges, 500); } else { if (enableDebugLog) console.log("GPTGD: Failed to find required container after multiple retries."); const existingButton = document.querySelector('.gptgd-bttn'); if (existingButton) { if (enableDebugLog) console.log("GPTGD: Navigated away from a target page. Removing UI."); existingButton.remove(); if (downloadStatus) downloadStatus.remove(); if (progressElement) progressElement.remove(); } } }; if (enableDebugLog) console.log("GPTGD: Starting MutationObserver on the body."); const observer = new MutationObserver(handleDOMChanges); observer.observe(document.body, { childList: true, subtree: true }); handleDOMChanges(); })();