ExHentai Download Button with Batch Download Support

Adds download buttons (Original, Resample, H@H) to ExHentai/E-Hentai galleries with batch download support. Features progress tracking, error logging, and GP-aware downloading.

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         ExHentai Download Button with Batch Download Support
// @namespace    https://github.com/troyt-666/exhentai-utilities
// @version      1.3.0
// @description  Adds download buttons (Original, Resample, H@H) to ExHentai/E-Hentai galleries with batch download support. Features progress tracking, error logging, and GP-aware downloading.
// @author       Troy T
// @homepageURL  https://github.com/troyt-666/exhentai-utilities
// @supportURL   https://github.com/troyt-666/exhentai-utilities/issues
// @match        https://exhentai.org/
// @match        https://exhentai.org/?*
// @match        https://exhentai.org/favorites.php*
// @match        https://exhentai.org/tag/*
// @match        https://e-hentai.org/
// @match        https://e-hentai.org/?*
// @match        https://e-hentai.org/favorites.php*
// @match        https://e-hentai.org/tag/*
// @connect      exhentai.org
// @connect      e-hentai.org
// @connect      hath.network
// @connect      *.hath.network
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @license      MIT
// @icon         https://exhentai.org/favicon.ico
// ==/UserScript==

/*
 * ExHentai Download Button with Batch Support
 * 
 * This userscript enhances ExHentai/E-Hentai gallery pages by adding:
 * - Individual download buttons for each gallery (Original, Resample, H@H)
 * - Batch download functionality with progress tracking
 * - Error logging and retry capabilities
 * - Toast notifications for H@H operations
 * 
 * Part of the ExHentai Utilities toolkit:
 * https://github.com/troyt-666/exhentai-utilities
 * 
 * Installation:
 * 1. Install Tampermonkey or compatible userscript manager
 * 2. Click on this script's raw URL
 * 3. Tampermonkey will prompt to install
 * 
 * Usage:
 * - Individual downloads: Click buttons on each gallery
 * - Batch downloads: Use checkboxes and batch panel (top-right)
 * - H@H downloads are queued to your Hentai@Home client
 * 
 * Note: All downloads follow standard ExHentai rules and consume GP
 */

(function() {
    'use strict';

    // Function to show a toast notification with a shrinking progress bar
    function showToast(message) {
        // Create the toast container
        var toast = document.createElement('div');
        toast.style.position = 'fixed';
        toast.style.bottom = '20px';
        toast.style.right = '20px';
        toast.style.padding = '15px';
        toast.style.backgroundColor = '#333';
        toast.style.color = '#fff';
        toast.style.borderRadius = '5px';
        toast.style.boxShadow = '0px 0px 10px rgba(0,0,0,0.5)';
        toast.style.zIndex = '10000';
        toast.style.fontSize = '14px';
        toast.style.display = 'flex';
        toast.style.flexDirection = 'column';  // Stack icon/text and progress bar vertically
        toast.style.alignItems = 'center';
        toast.style.width = '300px';

        // Add an icon (optional)
        var icon = document.createElement('div');
        icon.innerHTML = '✔'; // Checkmark icon (you can replace this with any other icon)
        icon.style.marginBottom = '10px';
        icon.style.color = '#4CAF50'; // Green color for success
        icon.style.fontSize = '20px';
        toast.appendChild(icon);

        // Add the message
        var messageText = document.createElement('span');
        messageText.textContent = message;
        toast.appendChild(messageText);

        // Create a progress bar at the bottom of the toast
        var progressBar = document.createElement('div');
        progressBar.style.height = '5px';
        progressBar.style.width = '100%';
        progressBar.style.backgroundColor = '#4CAF50';  // Green progress bar
        progressBar.style.borderRadius = '0 0 5px 5px';  // Rounded corners only at the bottom
        progressBar.style.transition = 'width 3s linear';  // Smooth shrink over 3 seconds
        toast.appendChild(progressBar);

        // Append the toast to the body
        document.body.appendChild(toast);

        // Start the progress bar shrinking
        setTimeout(function() {
            progressBar.style.width = '0';  // Shrink the width to 0 over 3 seconds
        }, 100);  // Small delay to ensure the progress bar appears at full width first

        // Automatically remove the toast after 3 seconds
        setTimeout(function() {
            toast.style.transition = 'opacity 0.5s ease-in-out';
            toast.style.opacity = '0';
            setTimeout(function() {
                document.body.removeChild(toast);
            }, 500); // Wait for the fade-out transition to complete
        }, 3000); // Display for 3 seconds
    }

    // Function to show batch download progress
    function showBatchProgress(current, total, galleryTitle = '') {
        // Remove existing progress modal if any
        var existingModal = document.getElementById('batch-progress-modal');
        if (existingModal) {
            existingModal.remove();
        }

        // Create progress modal
        var modal = document.createElement('div');
        modal.id = 'batch-progress-modal';
        modal.style.position = 'fixed';
        modal.style.top = '50%';
        modal.style.left = '50%';
        modal.style.transform = 'translate(-50%, -50%)';
        modal.style.padding = '20px';
        modal.style.backgroundColor = '#333';
        modal.style.color = '#fff';
        modal.style.borderRadius = '10px';
        modal.style.boxShadow = '0px 0px 20px rgba(0,0,0,0.8)';
        modal.style.zIndex = '10001';
        modal.style.fontSize = '14px';
        modal.style.width = '400px';
        modal.style.textAlign = 'center';

        // Title
        var title = document.createElement('h3');
        title.textContent = 'Batch H@H Download Progress';
        title.style.margin = '0 0 15px 0';
        title.style.color = '#4CAF50';
        modal.appendChild(title);

        // Current gallery info
        var currentInfo = document.createElement('div');
        currentInfo.textContent = galleryTitle ? `Processing: ${galleryTitle}` : `Processing gallery ${current} of ${total}`;
        currentInfo.style.marginBottom = '15px';
        currentInfo.style.fontSize = '12px';
        modal.appendChild(currentInfo);

        // Progress bar container
        var progressContainer = document.createElement('div');
        progressContainer.style.width = '100%';
        progressContainer.style.height = '20px';
        progressContainer.style.backgroundColor = '#555';
        progressContainer.style.borderRadius = '10px';
        progressContainer.style.overflow = 'hidden';
        progressContainer.style.marginBottom = '10px';

        // Progress bar
        var progressBar = document.createElement('div');
        progressBar.style.height = '100%';
        progressBar.style.backgroundColor = '#4CAF50';
        progressBar.style.width = ((current / total) * 100) + '%';
        progressBar.style.transition = 'width 0.3s ease';
        progressContainer.appendChild(progressBar);
        modal.appendChild(progressContainer);

        // Progress text
        var progressText = document.createElement('div');
        progressText.textContent = `${current} of ${total} completed (${Math.round((current / total) * 100)}%)`;
        progressText.style.fontSize = '12px';
        modal.appendChild(progressText);

        document.body.appendChild(modal);
    }

    // Function to hide batch progress
    function hideBatchProgress() {
        var modal = document.getElementById('batch-progress-modal');
        if (modal) {
            modal.remove();
        }
    }


    // Create batch download control panel
    function createBatchDownloadPanel() {
        var panel = document.createElement('div');
        panel.id = 'batch-download-panel';
        panel.style.position = 'fixed';
        panel.style.top = '10px';
        panel.style.right = '10px';
        panel.style.backgroundColor = '#333';
        panel.style.color = '#fff';
        panel.style.borderRadius = '5px';
        panel.style.boxShadow = '0px 0px 10px rgba(0,0,0,0.5)';
        panel.style.zIndex = '9999';
        panel.style.fontSize = '12px';
        panel.style.transition = 'all 0.3s ease';

        // Create toggle button (always visible)
        var toggleButton = document.createElement('div');
        toggleButton.id = 'batch-toggle-btn';
        toggleButton.innerHTML = '📥 Batch H@H';
        toggleButton.style.padding = '8px 12px';
        toggleButton.style.cursor = 'pointer';
        toggleButton.style.backgroundColor = '#4CAF50';
        toggleButton.style.borderRadius = '5px';
        toggleButton.style.fontWeight = 'bold';
        toggleButton.style.textAlign = 'center';
        toggleButton.style.userSelect = 'none';
        toggleButton.style.fontSize = '11px';
        toggleButton.title = 'Click to expand/collapse batch download panel';
        panel.appendChild(toggleButton);

        // Create collapsible content container
        var contentContainer = document.createElement('div');
        contentContainer.id = 'batch-content';
        contentContainer.style.display = 'none';
        contentContainer.style.padding = '10px';
        contentContainer.style.borderTop = '1px solid #555';
        contentContainer.style.marginTop = '0';

        // Title for expanded view
        var title = document.createElement('div');
        title.textContent = 'Batch H@H Download';
        title.style.fontWeight = 'bold';
        title.style.textAlign = 'center';
        title.style.marginBottom = '10px';
        contentContainer.appendChild(title);

        // Controls container
        var controls = document.createElement('div');
        controls.style.display = 'flex';
        controls.style.flexDirection = 'column';
        controls.style.gap = '8px';
        controls.style.minWidth = '180px';

        // Select All/None buttons
        var selectButtonsContainer = document.createElement('div');
        selectButtonsContainer.style.display = 'flex';
        selectButtonsContainer.style.gap = '5px';

        var selectAllBtn = document.createElement('button');
        selectAllBtn.textContent = 'Select All';
        selectAllBtn.style.flex = '1';
        selectAllBtn.style.padding = '3px 6px';
        selectAllBtn.style.fontSize = '11px';
        selectAllBtn.style.cursor = 'pointer';
        selectAllBtn.style.border = '1px solid #555';
        selectAllBtn.style.backgroundColor = '#444';
        selectAllBtn.style.color = '#fff';
        selectAllBtn.style.borderRadius = '3px';

        var selectNoneBtn = document.createElement('button');
        selectNoneBtn.textContent = 'Select None';
        selectNoneBtn.style.flex = '1';
        selectNoneBtn.style.padding = '3px 6px';
        selectNoneBtn.style.fontSize = '11px';
        selectNoneBtn.style.cursor = 'pointer';
        selectNoneBtn.style.border = '1px solid #555';
        selectNoneBtn.style.backgroundColor = '#444';
        selectNoneBtn.style.color = '#fff';
        selectNoneBtn.style.borderRadius = '3px';

        selectButtonsContainer.appendChild(selectAllBtn);
        selectButtonsContainer.appendChild(selectNoneBtn);
        controls.appendChild(selectButtonsContainer);

        // Selected count
        var selectedCount = document.createElement('div');
        selectedCount.id = 'selected-count';
        selectedCount.textContent = 'Selected: 0';
        selectedCount.style.textAlign = 'center';
        selectedCount.style.fontSize = '11px';
        selectedCount.style.color = '#aaa';
        controls.appendChild(selectedCount);

        // Download button
        var batchDownloadBtn = document.createElement('button');
        batchDownloadBtn.textContent = 'Download Selected H@H';
        batchDownloadBtn.style.padding = '8px';
        batchDownloadBtn.style.fontSize = '11px';
        batchDownloadBtn.style.cursor = 'pointer';
        batchDownloadBtn.style.border = '1px solid #4CAF50';
        batchDownloadBtn.style.backgroundColor = '#4CAF50';
        batchDownloadBtn.style.color = '#fff';
        batchDownloadBtn.style.borderRadius = '3px';
        batchDownloadBtn.style.fontWeight = 'bold';
        controls.appendChild(batchDownloadBtn);

        // Error log container (initially hidden)
        var errorLogContainer = document.createElement('div');
        errorLogContainer.id = 'error-log-container';
        errorLogContainer.style.display = 'none';
        errorLogContainer.style.marginTop = '8px';
        errorLogContainer.style.border = '1px solid #ff6b6b';
        errorLogContainer.style.borderRadius = '3px';
        errorLogContainer.style.backgroundColor = '#2a1a1a';

        var errorLogTitle = document.createElement('div');
        errorLogTitle.textContent = 'Download Errors:';
        errorLogTitle.style.fontSize = '10px';
        errorLogTitle.style.color = '#ff6b6b';
        errorLogTitle.style.padding = '3px 5px';
        errorLogTitle.style.borderBottom = '1px solid #ff6b6b';
        errorLogTitle.style.fontWeight = 'bold';
        errorLogContainer.appendChild(errorLogTitle);

        var errorLogText = document.createElement('textarea');
        errorLogText.id = 'error-log-text';
        errorLogText.style.width = '100%';
        errorLogText.style.height = '80px';
        errorLogText.style.fontSize = '9px';
        errorLogText.style.backgroundColor = 'transparent';
        errorLogText.style.color = '#ff9999';
        errorLogText.style.border = 'none';
        errorLogText.style.padding = '5px';
        errorLogText.style.resize = 'none';
        errorLogText.style.outline = 'none';
        errorLogText.readOnly = true;
        errorLogContainer.appendChild(errorLogText);

        var clearLogBtn = document.createElement('button');
        clearLogBtn.textContent = 'Clear Log';
        clearLogBtn.style.width = '100%';
        clearLogBtn.style.padding = '3px';
        clearLogBtn.style.fontSize = '9px';
        clearLogBtn.style.cursor = 'pointer';
        clearLogBtn.style.border = '1px solid #ff6b6b';
        clearLogBtn.style.backgroundColor = '#ff6b6b';
        clearLogBtn.style.color = '#fff';
        clearLogBtn.style.borderRadius = '0 0 3px 3px';
        clearLogBtn.addEventListener('click', function() {
            errorLogText.value = '';
            errorLogContainer.style.display = 'none';
        });
        errorLogContainer.appendChild(clearLogBtn);

        controls.appendChild(errorLogContainer);

        contentContainer.appendChild(controls);
        panel.appendChild(contentContainer);
        document.body.appendChild(panel);

        // Toggle functionality
        var isExpanded = false;
        toggleButton.addEventListener('click', function() {
            isExpanded = !isExpanded;
            if (isExpanded) {
                contentContainer.style.display = 'block';
                toggleButton.innerHTML = '📤 Batch H@H';
                toggleButton.style.backgroundColor = '#ff6b6b';
                toggleButton.title = 'Click to collapse panel';
            } else {
                contentContainer.style.display = 'none';
                toggleButton.innerHTML = '📥 Batch H@H';
                toggleButton.style.backgroundColor = '#4CAF50';
                toggleButton.title = 'Click to expand panel';
            }
        });

        // Event listeners
        selectAllBtn.addEventListener('click', function() {
            var checkboxes = document.querySelectorAll('.gallery-batch-checkbox');
            checkboxes.forEach(function(cb) { cb.checked = true; });
            updateSelectedCount();
        });

        selectNoneBtn.addEventListener('click', function() {
            var checkboxes = document.querySelectorAll('.gallery-batch-checkbox');
            checkboxes.forEach(function(cb) { cb.checked = false; });
            updateSelectedCount();
        });

        batchDownloadBtn.addEventListener('click', function() {
            startBatchDownload();
        });
    }

    // Function to update selected count
    function updateSelectedCount() {
        var checkboxes = document.querySelectorAll('.gallery-batch-checkbox:checked');
        var countElement = document.getElementById('selected-count');
        if (countElement) {
            countElement.textContent = 'Selected: ' + checkboxes.length;
        }
    }

    // Function to log errors
    function logError(galleryTitle, galleryLink, errorMessage) {
        var errorLogText = document.getElementById('error-log-text');
        var errorLogContainer = document.getElementById('error-log-container');
        
        if (errorLogText && errorLogContainer) {
            var timestamp = new Date().toLocaleTimeString();
            var logEntry = `[${timestamp}] ${galleryTitle}\n${galleryLink}\nError: ${errorMessage}\n\n`;
            errorLogText.value += logEntry;
            errorLogContainer.style.display = 'block';
            
            // Scroll to bottom of textarea
            errorLogText.scrollTop = errorLogText.scrollHeight;
        }
    }

    // Create the batch download panel
    createBatchDownloadPanel();

    // Loop through all gallery items on the search page
    var galleryItems = document.querySelectorAll('.gl1t');

    galleryItems.forEach(function(item) {
        var galleryLink = item.querySelector('a').href; // Get the gallery link
        var galleryTitle = item.querySelector('.gl4t.glname.glink') ? 
                          item.querySelector('.gl4t.glname.glink').textContent.trim() : 
                          (item.querySelector('img') ? item.querySelector('img').title : 'Unknown Gallery'); // Get gallery title

        // Add checkbox for batch download
        var checkboxContainer = document.createElement('div');
        checkboxContainer.style.display = 'flex';
        checkboxContainer.style.alignItems = 'center';
        checkboxContainer.style.gap = '5px';
        checkboxContainer.style.marginBottom = '5px';

        var checkbox = document.createElement('input');
        checkbox.type = 'checkbox';
        checkbox.className = 'gallery-batch-checkbox';
        checkbox.style.cursor = 'pointer';
        checkbox.addEventListener('change', updateSelectedCount);
        // Store gallery data for batch processing
        checkbox.dataset.galleryLink = galleryLink;
        checkbox.dataset.galleryTitle = galleryTitle;

        var checkboxLabel = document.createElement('label');
        checkboxLabel.textContent = 'Batch H@H';
        checkboxLabel.style.fontSize = '11px';
        checkboxLabel.style.cursor = 'pointer';
        checkboxLabel.addEventListener('click', function() {
            checkbox.checked = !checkbox.checked;
            updateSelectedCount();
        });

        checkboxContainer.appendChild(checkbox);
        checkboxContainer.appendChild(checkboxLabel);
        item.appendChild(checkboxContainer);

        // Create a container div for buttons
        var buttonContainer = document.createElement('div');
        buttonContainer.style.display = 'flex'; // Flex container for side-by-side buttons
        buttonContainer.style.gap = '5px'; // Gap between the buttons

        // Create a button style function to match the existing page style
        function createButton(text) {
            var button = document.createElement('button');
            button.innerText = text;
            button.style.padding = '5px 10px';  // Padding similar to the existing button style
            button.style.cursor = 'pointer';    // Pointer cursor for interaction
            button.style.border = '1px solid #ccc';  // Light border similar to the website
            button.style.backgroundColor = 'transparent'; // Match the page background color
            button.style.color = 'inherit';  // Use the same text color as the page
            button.style.fontSize = '12px';  // Match the font size for the website
            return button;
        }

        // Create the Original Archive button
        var downloadOriginalButton = createButton('Download Orginal');

        // Create the Resample Archive button
        var downloadResampleButton = createButton('Download Resample');

        // Create the Remote Server Download button (H@H)
        var downloadHaHButton = createButton('Download H@H');

        // Add all buttons to the container
        buttonContainer.appendChild(downloadOriginalButton);
        buttonContainer.appendChild(downloadResampleButton);
        buttonContainer.appendChild(downloadHaHButton);

        // Add the button container to the gallery item
        item.appendChild(buttonContainer);

        // Helper function to handle downloads (Original, Resample, or H@H)
        function handleDownloadButton(archiveType) {
            console.log("Fetching gallery page: " + galleryLink); // Log the gallery link being accessed
            if (archiveType === 'hath') {
                showToast("Fetching gallery page...");
            }

            // Step 1: Fetch the gallery page to find the archive download link
            GM_xmlhttpRequest({
                method: 'GET',
                url: galleryLink,
                onload: function(response) {
                    var parser = new DOMParser();
                    var doc = parser.parseFromString(response.responseText, 'text/html');

                    // Find the anchor element with "onclick" containing "popUp"
                    var archiveDownloadAnchor = doc.querySelector('a[onclick^="return popUp"]');
                    
                    if (archiveDownloadAnchor) {
                        console.log("Found archiveDownloadAnchor:", archiveDownloadAnchor); // Log the anchor element
                        if (archiveType === 'hath') {
                            showToast("Found archive download link!");
                        }

                        // Extract the URL from the onclick attribute (popUp() call)
                        var onclickContent = archiveDownloadAnchor.getAttribute('onclick');
                        var archiveUrlMatch = onclickContent.match(/popUp\('(.+?)'/);
                        
                        if (archiveUrlMatch && archiveUrlMatch[1]) {
                            var archiveUrl = archiveUrlMatch[1];
                            console.log("Extracted archive URL:", archiveUrl); // Log the extracted URL
                            if (archiveType === 'hath') {
                                showToast("Extracted archive URL successfully!");
                            }

                            // Step 2: Fetch the page where the form exists
                            GM_xmlhttpRequest({
                                method: 'GET',
                                url: archiveUrl,
                                onload: function(archivePageResponse) {
                                    var archiveDoc = parser.parseFromString(archivePageResponse.responseText, 'text/html');

                                    // Check if we are handling the H@H download
                                    if (archiveType === 'hath') {
                                        // Handle H@H download by submitting the form for H@H server
                                        var formElement = archiveDoc.querySelector('#hathdl_form');
                                        if (formElement) {
                                            var formAction = formElement.getAttribute('action');
                                            var formData = new FormData(formElement);
                                            formData.set('hathdl_xres', 'org'); // Set to 'Original'

                                            // Submit the form for remote server download
                                            GM_xmlhttpRequest({
                                                method: 'POST',
                                                url: formAction,
                                                data: new URLSearchParams(formData), // Simulate the form submission
                                                onload: function(formSubmitResponse) {
                                                    var successMessage = "An original resolution download has been queued for client";
                                                    if (formSubmitResponse.responseText.includes(successMessage)) {
                                                        showToast("H@H download successfully queued!");
                                                        console.log("H@H download successfully queued.");
                                                    } else {
                                                        showToast("Failed to queue H@H download.");
                                                        console.log("Failed to queue H@H download.");
                                                    }
                                                }
                                            });
                                        } else {
                                            showToast('H@H form not found!');
                                            console.log("Form element for H@H download not found.");
                                        }
                                        return;
                                    }

                                    // For original or resample downloads
                                    var formElement = archiveDoc.querySelector('form[action*="archiver.php"]');
                                    var formAction = formElement.getAttribute('action');
                                    console.log("Form action URL:", formAction); // Log the form action URL

                                    // Step 3: Simulate submitting the form by POSTing the request
                                    var dltypeValue = archiveType === 'original' ? 'org' : 'res';
                                    GM_xmlhttpRequest({
                                        method: 'POST',
                                        url: formAction,
                                        headers: {
                                            'Content-Type': 'application/x-www-form-urlencoded'
                                        },
                                        data: `dltype=${dltypeValue}&dlcheck=Download ${archiveType === 'original' ? 'Original' : 'Resample'} Archive`,
                                        onload: function(formSubmitResponse) {
                                            var formSubmitDoc = parser.parseFromString(formSubmitResponse.responseText, 'text/html');

                                            // Extract the final redirect URL
                                            var redirectLink = formSubmitDoc.querySelector('#continue a') || formSubmitDoc.querySelector('script').innerText.match(/document\.location\s*=\s*"(.+?)"/)[1];

                                            if (redirectLink) {
                                                console.log(`Found final ${archiveType} download URL from redirect:`, redirectLink);

                                                // Poll for the final download link
                                                var checkForDownloadLink = function() {
                                                    GM_xmlhttpRequest({
                                                        method: 'GET',
                                                        url: redirectLink,
                                                        onload: function(downloadPageResponse) {
                                                            var downloadDoc = parser.parseFromString(downloadPageResponse.responseText, 'text/html');

                                                            // Check for the download link or message
                                                            var finalDownloadLink = downloadDoc.querySelector('a[href^="/archive/"]') || downloadDoc.body.innerHTML.includes("Click Here To Start Downloading");

                                                            if (finalDownloadLink) {
                                                                var finalUrl = redirectLink + "?start=1";
                                                                console.log("Final download URL:", finalUrl);
                                                                var a = document.createElement('a');
                                                                a.href = finalUrl;
                                                                a.style.display = 'none';
                                                                document.body.appendChild(a);
                                                                a.click();
                                                                document.body.removeChild(a);
                                                            } else {
                                                                console.log("Download link not available yet. Retrying in 2 seconds...");
                                                                setTimeout(checkForDownloadLink, 2000);
                                                            }
                                                        }
                                                    });
                                                };

                                                // Start polling for the download link
                                                checkForDownloadLink();
                                            } else {
                                                alert('Could not extract the final download URL!');
                                                console.log("Redirect URL not found in page:", formSubmitResponse.responseText);
                                            }
                                        }
                                    });
                                }
                            });
                        } else {
                            alert('Could not extract archive URL from onclick attribute!');
                            console.log("Onclick content:", onclickContent);
                        }
                    } else {
                        alert('Archive download link not found!');
                        console.log("Archive download link not found in page:", response.responseText);
                    }
                }
            });
        }

        // Add event listeners for all buttons
        downloadOriginalButton.addEventListener('click', function() {
            handleDownloadButton('original');
        });
        downloadResampleButton.addEventListener('click', function() {
            handleDownloadButton('resample');
        });
        downloadHaHButton.addEventListener('click', function() {
            handleDownloadButton('hath');
        });
    });

    // Batch download functionality
    function startBatchDownload() {
        var selectedCheckboxes = document.querySelectorAll('.gallery-batch-checkbox:checked');
        
        if (selectedCheckboxes.length === 0) {
            showToast('Please select at least one gallery to download.');
            return;
        }

        var totalCount = selectedCheckboxes.length;
        var currentCount = 0;
        var successCount = 0;
        var failCount = 0;

        showToast(`Starting batch download of ${totalCount} galleries...`);

        // Process downloads sequentially with delay
        function processNextDownload() {
            if (currentCount >= totalCount) {
                // All downloads completed
                hideBatchProgress();
                showToast(`Batch download completed! Success: ${successCount}, Failed: ${failCount}`);
                return;
            }

            var checkbox = selectedCheckboxes[currentCount];
            var galleryLink = checkbox.dataset.galleryLink;
            var galleryTitle = checkbox.dataset.galleryTitle;

            showBatchProgress(currentCount + 1, totalCount, galleryTitle);

            // Process H@H download for this gallery
            processGalleryHaHDownload(galleryLink, galleryTitle, function(success, errorMessage) {
                if (success) {
                    successCount++;
                } else {
                    failCount++;
                    if (errorMessage) {
                        logError(galleryTitle, galleryLink, errorMessage);
                    }
                }
                currentCount++;
                
                // Wait 800ms before processing next download to avoid rate limiting
                setTimeout(processNextDownload, 800);
            });
        }

        // Start processing
        processNextDownload();
    }

    // Function to process H@H download for a single gallery
    function processGalleryHaHDownload(galleryLink, galleryTitle, callback) {
        console.log("[TRACE] Batch processing gallery: " + galleryLink);
        console.log("[TRACE] Gallery title: " + galleryTitle);
        console.log("[TRACE] Fetching gallery page: " + galleryLink);

        GM_xmlhttpRequest({
            method: 'GET',
            url: galleryLink,
            onload: function(response) {
                if (response.status !== 200) {
                    console.log("[TRACE] Failed to fetch gallery page, HTTP status: " + response.status);
                    callback(false, `Failed to fetch gallery page (HTTP ${response.status})`);
                    return;
                }

                console.log("[TRACE] Successfully fetched gallery page");
                var parser = new DOMParser();
                var doc = parser.parseFromString(response.responseText, 'text/html');

                var archiveDownloadAnchor = doc.querySelector('a[onclick^="return popUp"]');
                
                if (archiveDownloadAnchor) {
                    console.log("[TRACE] Found archiveDownloadAnchor:", archiveDownloadAnchor);
                    console.log("[TRACE] Found archive download link!");
                    
                    var onclickContent = archiveDownloadAnchor.getAttribute('onclick');
                    console.log("[TRACE] Onclick content: " + onclickContent);
                    var archiveUrlMatch = onclickContent.match(/popUp\('(.+?)'/);
                    
                    if (archiveUrlMatch && archiveUrlMatch[1]) {
                        var archiveUrl = archiveUrlMatch[1];
                        console.log("[TRACE] Extracted archive URL: " + archiveUrl);
                        console.log("[TRACE] Extracted archive URL successfully!");

                        GM_xmlhttpRequest({
                            method: 'GET',
                            url: archiveUrl,
                            onload: function(archivePageResponse) {
                                if (archivePageResponse.status !== 200) {
                                    console.log("[TRACE] Failed to fetch archive page, HTTP status: " + archivePageResponse.status);
                                    callback(false, `Failed to fetch archive page (HTTP ${archivePageResponse.status})`);
                                    return;
                                }

                                console.log("[TRACE] Successfully fetched archive page");
                                var archiveDoc = parser.parseFromString(archivePageResponse.responseText, 'text/html');

                                var formElement = archiveDoc.querySelector('#hathdl_form');
                                if (formElement) {
                                    console.log("[TRACE] Found H@H form element");
                                    var formAction = formElement.getAttribute('action');
                                    console.log("[TRACE] Form action URL: " + formAction);
                                    var formData = new FormData(formElement);
                                    formData.set('hathdl_xres', 'org');
                                    console.log("[TRACE] Set hathdl_xres to 'org' for original resolution");

                                    console.log("[TRACE] Submitting H@H form...");
                                    GM_xmlhttpRequest({
                                        method: 'POST',
                                        url: formAction,
                                        data: new URLSearchParams(formData),
                                        onload: function(formSubmitResponse) {
                                            if (formSubmitResponse.status !== 200) {
                                                console.log("[TRACE] Failed to submit H@H form, HTTP status: " + formSubmitResponse.status);
                                                callback(false, `Failed to submit H@H form (HTTP ${formSubmitResponse.status})`);
                                                return;
                                            }

                                            console.log("[TRACE] Successfully submitted H@H form");
                                            var successMessage = "An original resolution download has been queued for client";
                                            var success = formSubmitResponse.responseText.includes(successMessage);
                                            
                                            if (success) {
                                                console.log("[TRACE] H@H download successfully queued for: " + galleryTitle);
                                                callback(true);
                                            } else {
                                                console.log("[TRACE] H@H queue failed - success message not found in response");
                                                console.log("[TRACE] Response text: " + formSubmitResponse.responseText.substring(0, 500) + "...");
                                                // Try to extract error message from response
                                                var errorDoc = parser.parseFromString(formSubmitResponse.responseText, 'text/html');
                                                var errorElement = errorDoc.querySelector('.stuffbox') || errorDoc.querySelector('p');
                                                var errorText = errorElement ? errorElement.textContent.trim() : 'Unknown error occurred';
                                                console.log("[TRACE] Extracted error text: " + errorText);
                                                callback(false, `H@H queue failed: ${errorText}`);
                                            }
                                        },
                                        onerror: function(error) {
                                            console.log("[TRACE] Network error submitting H@H form: " + (error.error || 'Unknown network error'));
                                            callback(false, `Network error submitting H@H form: ${error.error || 'Unknown network error'}`);
                                        }
                                    });
                                } else {
                                    console.log("[TRACE] H@H form element not found on archive page");
                                    console.log("[TRACE] Archive page HTML: " + archivePageResponse.responseText.substring(0, 1000) + "...");
                                    callback(false, 'H@H form not found on archive page');
                                }
                            },
                            onerror: function(error) {
                                console.log("[TRACE] Network error fetching archive page: " + (error.error || 'Unknown network error'));
                                callback(false, `Network error fetching archive page: ${error.error || 'Unknown network error'}`);
                            }
                        });
                    } else {
                        console.log("[TRACE] Could not extract archive URL from onclick attribute");
                        console.log("[TRACE] Onclick content was: " + onclickContent);
                        callback(false, 'Could not extract archive URL from gallery page');
                    }
                } else {
                    console.log("[TRACE] Archive download link not found on gallery page");
                    console.log("[TRACE] Gallery page HTML: " + response.responseText.substring(0, 1000) + "...");
                    callback(false, 'Archive download link not found on gallery page');
                }
            },
            onerror: function(error) {
                console.log("[TRACE] Network error fetching gallery page: " + (error.error || 'Unknown network error'));
                callback(false, `Network error fetching gallery page: ${error.error || 'Unknown network error'}`);
            }
        });
    }
})();