GPT gallery downloader (GPTGD)

Creates a button to download all images from galleries & favorite photos in a single .zip file. Large collections are split into multiple zip files.

Fra 22.09.2025. Se den seneste versjonen.

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 or Violentmonkey 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         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();
})();