// ==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();
})();