HH++ Download Images

Télécharge les images des personnages dans Harem Heroes

// ==UserScript==
// @name         HH++ Download Images
// @namespace    http://tampermonkey.net/
// @version      0.4
// @description  Télécharge les images des personnages dans Harem Heroes
// @author       ddhhapi
// @match           https://*.hentaiheroes.com/*
// @match           https://*.haremheroes.com/*
// @match           https://*.gayharem.com/*
// @match           https://*.comixharem.com/*
// @match           https://*.hornyheroes.com/*
// @match           https://*.pornstarharem.com/*
// @match           https://*.transpornstarharem.com/*
// @match           https://*.gaypornstarharem.com/*
// @match           https://*.mangarpg.com/*
// @match        https://www.hentaiheroes.com/characters/*
// @grant        GM_xmlhttpRequest
// @grant        GM_download
// @grant        GM_setValue
// @grant        GM_getValue
// @license      MIT
// ==/UserScript==


/*
===========================================================================
                    HH++ DOWNLOAD IMAGES - USER GUIDE
===========================================================================

📋 DESCRIPTION:
This script creates a floating download panel that automatically detects 
the currently selected character in your harem and allows you to batch 
download all their scene images in high quality.

🎯 FEATURES:
• Automatic character detection across all Harem Heroes variants
• High-quality image downloads (800x450 or 1600x900 depending on site)
• Custom file naming with optional prefixes
• Real-time download progress tracking
• Modern, GitHub-style interface
• Cross-platform compatibility
• Debug logging and error handling

🚀 QUICK START:
1. Install this script in Tampermonkey
2. For HentaiHeroes.com: Enable "Run in frames" in script settings
3. Go to your harem page
4. Select a character
5. Click the download icon (top-right corner)
6. Use the control panel to download scenes

📖 DETAILED INSTRUCTIONS:

INSTALLATION:
1. Install Tampermonkey extension in your browser
2. Click "Create new script" or import this script
3. Save the script (Ctrl+S)

CONFIGURATION FOR HENTAIHEROES.COM:
⚠️  IMPORTANT: For hentaiheroes.com, you MUST enable iframe execution:
1. Go to Tampermonkey dashboard
2. Click on this script name
3. Go to Settings tab
4. Set "Run in frames" to "Yes"
5. Save settings

USAGE:
1. Navigate to your harem page on any supported site
2. Click on a character to select them
3. Look for the floating download icon (⬇️) in the top-right corner
4. Click the icon to open the control panel

CONTROL PANEL:
• Character Info: Shows name, ID, and number of available scenes
• Prefix Field: Optional text to add before filenames (e.g., "001")
• Refresh (↻): Updates character information
• Download (DL): Starts batch download of all scenes
• Log: Downloads a debug log file for troubleshooting

FILE NAMING:
Downloaded files are named: "{prefix}{Character Name} {Scene Number}.jpg"
Example: "(001) Ayane 1.jpg", "(001) Ayane 2.jpg", etc.

SUPPORTED SITES:
✅ HentaiHeroes.com (requires iframe mode)
✅ HaremHeroes.com (Nutaku)
🤷‍♂️ GayHarem.com
🤷‍♂️ ComixHarem.com
🤷‍♂️ HornyHeroes.com
🤷‍♂️ PornstarHarem.com
🤷‍♂️ TransPornstarHarem.com
🤷‍♂️ GayPornstarHarem.com
🤷‍♂️ MangaRPG.com

🔧 TROUBLESHOOTING:

"No character detected":
• Make sure you've selected a character in your harem
• For HentaiHeroes.com: Ensure "Run in frames" is enabled
• Try refreshing the page and selecting the character again
• Check that you're on the harem page, not another game section

Downloads not working:
• Check your browser's download settings
• Ensure pop-ups are allowed for the game site
• Try downloading one scene manually to test browser permissions
• Check the log panel for specific error messages

Slow performance:
• The script waits for page elements to load before activating
• On slower connections, it may take a few seconds to appear
• If the icon doesn't appear after 30 seconds, refresh the page

🛡️ PRIVACY & SECURITY:
• Script only accesses game pages, no external sites
• Downloads images directly from game servers
• No personal data collection or transmission
• Open source code - review before installation

📝 TECHNICAL NOTES:
• Uses GM_download API for reliable file saving
• Automatically handles session tokens where required
• Adapts to different site structures and URL patterns
• Includes retry logic for better reliability
• Modern ES6+ JavaScript with error handling

🆘 SUPPORT:
If you encounter issues:
1. Check the troubleshooting section above
2. Use the Log button to generate a debug file
3. Report issues with the debug log attached
4. Include your browser version and game site URL

⚖️ LICENSE:
MIT License - Free to use, modify, and distribute

🔄 UPDATES:
Script automatically checks for updates through Tampermonkey.
Manual updates can be downloaded from the homepage URL.

===========================================================================
*/
(function() {
    'use strict';

    // Nouvelle taille du panneau
    const PANEL_WIDTH = '260px';
    const PANEL_HEIGHT = 'auto';

    // Log global pour export
    let exportLog = [];

    // Fonction pour créer l'icône flottante (SVG moderne, style GitHub)
    function createFloatingIcon() {
        const icon = document.createElement('div');
        icon.id = 'hh-download-icon';
        icon.innerHTML = `
            <svg width="28" height="28" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
                <circle cx="8" cy="8" r="8" fill="#fff" stroke="#d0d7de"/>
                <path d="M8 3v6m0 0l-3-3m3 3l3-3" stroke="#24292f" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
                <rect x="5" y="12" width="6" height="1.5" rx="0.5" fill="#24292f"/>
            </svg>
        `;
        icon.style.cssText = `
            position: fixed;
            top: 16px;
            right: 16px;
            width: 40px;
            height: 40px;
            display: flex;
            align-items: center;
            justify-content: center;
            z-index: 10000;
            cursor: pointer;
            background: #fff;
            border: 1px solid #d0d7de;
            border-radius: 50%;
            box-shadow: 0 4px 16px rgba(27,31,35,0.07);
            transition: box-shadow 0.2s;
        `;
        icon.addEventListener('mouseenter', () => {
            icon.style.boxShadow = '0 8px 24px rgba(27,31,35,0.15)';
        });
        icon.addEventListener('mouseleave', () => {
            icon.style.boxShadow = '0 4px 16px rgba(27,31,35,0.07)';
        });
        icon.addEventListener('click', function() {
            const panel = document.getElementById('hh-download-panel');
            if (panel) {
                panel.style.display = (panel.style.display === 'none') ? 'block' : 'none';
            }
        });
        document.body.appendChild(icon);
    }

    // Fonction pour créer le panneau de contrôle (style GitHub)
    function createControlPanel() {
        const panel = document.createElement('div');
        panel.id = 'hh-download-panel';
        panel.style.cssText = `
            position: fixed;
            top: 70px;
            right: 16px;
            width: ${PANEL_WIDTH};
            background: #fff;
            border: 1px solid #d0d7de;
            border-radius: 10px;
            padding: 18px 18px 12px 18px;
            z-index: 9999;
            box-shadow: 0 8px 24px rgba(27,31,35,0.15);
            font-family: 'Segoe UI', 'Liberation Sans', Arial, sans-serif;
            font-size: 15px;
            color: #24292f;
            display: none;
        `;

        // Titre du panneau
        const title = document.createElement('div');
        title.textContent = 'Download Character Scenes';
        title.style.fontWeight = '600';
        title.style.fontSize = '17px';
        title.style.marginBottom = '10px';
        title.style.letterSpacing = '0.01em';
        panel.appendChild(title);

        // Informations du personnage
        const infoDiv = document.createElement('div');
        infoDiv.id = 'character-info';
        infoDiv.style.marginBottom = '12px';
        infoDiv.style.fontSize = '13px';
        infoDiv.style.color = '#57606a';
        panel.appendChild(infoDiv);

        // Contrôles
        const controlsDiv = document.createElement('div');
        controlsDiv.style.marginBottom = '10px';
        controlsDiv.style.display = 'flex';
        controlsDiv.style.gap = '8px';
        controlsDiv.style.alignItems = 'center';

        // Input pour le préfixe
        const startInput = document.createElement('input');
        startInput.type = 'text';
        startInput.id = 'start-number';
        startInput.placeholder = 'Prefix (e.g. 011)';
        startInput.style.width = '70px';
        startInput.style.fontSize = '14px';
        startInput.style.padding = '4px 8px';
        startInput.style.border = '1px solid #d0d7de';
        startInput.style.borderRadius = '6px';
        startInput.style.background = '#f6f8fa';
        startInput.style.color = '#24292f';
        startInput.style.outline = 'none';
        startInput.style.transition = 'border 0.2s';
        startInput.addEventListener('focus', () => {
            startInput.style.border = '1.5px solid #0969da';
        });
        startInput.addEventListener('blur', () => {
            startInput.style.border = '1px solid #d0d7de';
        });
        controlsDiv.appendChild(startInput);

        // Boutons stylés façon GitHub
        function makeButton(label, onClick) {
            const btn = document.createElement('button');
            btn.textContent = label;
            btn.onclick = onClick;
            btn.style.flex = '1';
            btn.style.fontSize = '14px';
            btn.style.padding = '4px 0';
            btn.style.background = '#f6f8fa';
            btn.style.color = '#24292f';
            btn.style.border = '1px solid #d0d7de';
            btn.style.borderRadius = '6px';
            btn.style.cursor = 'pointer';
            btn.style.fontWeight = '500';
            btn.style.transition = 'background 0.2s, border 0.2s';
            btn.addEventListener('mouseenter', () => {
                btn.style.background = '#eaeef2';
                btn.style.border = '1.5px solid #0969da';
            });
            btn.addEventListener('mouseleave', () => {
                btn.style.background = '#f6f8fa';
                btn.style.border = '1px solid #d0d7de';
            });
            return btn;
        }
        controlsDiv.appendChild(makeButton('↻', refreshInfo));
        controlsDiv.appendChild(makeButton('DL', downloadImages));
        controlsDiv.appendChild(makeButton('Log', downloadLogFile));
        panel.appendChild(controlsDiv);

        // Log stylé façon GitHub
        const logDiv = document.createElement('div');
        logDiv.id = 'download-log';
        logDiv.style.cssText = `
            height: 90px;
            overflow-y: auto;
            border: 1px solid #d0d7de;
            padding: 7px 8px;
            font-size: 12px;
            background: #f6f8fa;
            color: #1a1a1a;
            border-radius: 6px;
            font-family: 'JetBrains Mono', 'Fira Mono', 'Consolas', monospace;
        `;
        panel.appendChild(logDiv);

        document.body.appendChild(panel);
        refreshInfo();
    }

    // Fonction pour ajouter un message au log
    function addLogMessage(message, isError = false, raw = null) {
        const logDiv = document.getElementById('download-log');
        if (!logDiv) return;
        const now = new Date().toLocaleTimeString();
        const messageDiv = document.createElement('div');
        messageDiv.textContent = `[${now}] ${message}`;
        messageDiv.style.color = isError ? 'red' : '#222';
        logDiv.appendChild(messageDiv);
        logDiv.scrollTop = logDiv.scrollHeight;
        // Ajout au log exportable
        exportLog.push(`[${now}] ${message}` + (raw ? `\n${raw}` : ''));
    }

    // Improved getCharacterInfo for all platforms
    function getCharacterInfo() {
        // Step 1: Find harem_right or fallback for HH
        let haremRight = document.querySelector('#harem_right');
        if (haremRight) addLogMessage('Found #harem_right');
        if (!haremRight) {
            haremRight = document.querySelector('.harem_right, [id*=harem_right]');
            if (haremRight) addLogMessage('Found .harem_right or [id*=harem_right]');
        }
        // NEW: Fallback for hentaiheroes.com (no #harem_right)
        if (!haremRight && window.location.hostname.includes('hentaiheroes.com')) {
            // Try to find the opened girl panel in #harem_whole or .global-container
            let openedDiv = document.querySelector('#harem_whole .opened[girl], .global-container .opened[girl], #harem_whole [girl].opened, .global-container [girl].opened');
            if (openedDiv) {
                addLogMessage('Found .opened[girl] in #harem_whole or .global-container (HH fallback)');
                haremRight = { children: [openedDiv] }; // fake haremRight for compatibility
            }
        }
        if (!haremRight) {
            addLogMessage('Could not find harem_right panel.', true);
            return null;
        }

        // Step 2: Find opened girl div
        let openedDiv = haremRight.querySelector ? haremRight.querySelector('.opened') : haremRight.children[0];
        if (openedDiv) addLogMessage('Found .opened');
        if (!openedDiv && haremRight.querySelector) {
            openedDiv = haremRight.querySelector('[girl]');
            if (openedDiv) addLogMessage('Found [girl] attribute');
        }
        // Step 3: Fallback: first child with name and scenes
        if (!openedDiv) {
            addLogMessage('Trying fallback: first child with name and scenes');
            const candidates = Array.from(haremRight.children);
            for (const cand of candidates) {
                if (
                    (cand.querySelector('h3') || cand.querySelector('h2') || cand.querySelector('.name')) &&
                    cand.querySelector('.girl_quests a, [class*=girl_quests] a')
                ) {
                    openedDiv = cand;
                    addLogMessage('Fallback: found candidate with name and scenes');
                    break;
                }
            }
        }
        if (!openedDiv) {
            addLogMessage('Could not find any opened/selected girl block.', true);
            return null;
        }

        // Step 4: Get girl id
        const girlId = openedDiv.getAttribute('girl') || openedDiv.getAttribute('data-girl-id') || '';
        addLogMessage('Girl ID: ' + girlId);
        // Step 5: Get name
        let name = '';
        if (openedDiv.querySelector('h3')) name = openedDiv.querySelector('h3').textContent;
        else if (openedDiv.querySelector('h2')) name = openedDiv.querySelector('h2').textContent;
        else if (openedDiv.querySelector('.name')) name = openedDiv.querySelector('.name').textContent;
        name = name.trim();
        addLogMessage('Girl name: ' + name);
        const folderName = name.replace(/\s+/g, '-').replace(/\./g, '').toLowerCase();

        // Step 6: Get scene links
        let quests = [];
        if (window.location.hostname.includes('hentaiheroes.com')) {
            quests = Array.from(openedDiv.querySelectorAll('.girl_quests a, [class*=girl_quests] a'));
            addLogMessage('Found ' + quests.length + ' scene links (HH)');
        } else {
            let questsDiv = openedDiv.querySelector('.girl_quests');
            if (!questsDiv) questsDiv = openedDiv.querySelector('[class*=girl_quests]');
            if (!questsDiv) {
                addLogMessage('Could not find girl_quests block.', true);
                return null;
            }
            quests = Array.from(questsDiv.querySelectorAll('a'));
            addLogMessage('Found ' + quests.length + ' scene links');
        }
        const nbQuests = quests.length;

        // Step 7: Get quest number and session token
        let questNumber = '';
        let sessToken = '';
        if (quests.length > 0) {
            const firstQuest = quests[0].getAttribute('href');
            const questMatch = firstQuest.match(/quest\/(\d+)/);
            if (questMatch) questNumber = questMatch[1];
            const sessMatch = firstQuest.match(/sess=([^&]+)/);
            if (sessMatch) sessToken = sessMatch[1];
            addLogMessage('First quest: ' + questNumber + ', sess: ' + sessToken);
        }

        return {
            girlId,
            name,
            folderName,
            nbQuests,
            firstQuest: questNumber,
            sessToken,
            quests
        };
    }

    // English refreshInfo
    function refreshInfo() {
        const info = getCharacterInfo();
        const infoDiv = document.getElementById('character-info');
        if (!infoDiv) return;

        if (!info) {
            infoDiv.innerHTML = '<p style="color: red;">No character detected. Please select a character in your harem.</p>';
            addLogMessage('No character detected.', true);
            return;
        }

        infoDiv.innerHTML = `
            <p><strong>Name:</strong> ${info.name}</p>
            <p><strong>ID:</strong> ${info.girlId}</p>
            <p><strong>Scenes:</strong> ${info.nbQuests}</p>
            <p><strong>First quest:</strong> ${info.firstQuest}</p>
        `;

        addLogMessage('Character info refreshed.');
    }

    // Update all log and error messages in downloadImages to English
    function downloadImages() {
        const info = getCharacterInfo();
        if (!info) {
            addLogMessage('No character detected. Please refresh and select a character.', true);
            return;
        }
        let prefix = document.getElementById('start-number').value || '';
        prefix = prefix.trim();
        if (prefix) prefix = `(${prefix}) `;
        let displayName = info.name.replace(/\s+/g, ' ').replace(/\./g, '').trim();
        let questLinks = [];
        if (window.location.hostname.includes('hentaiheroes.com')) {
            questLinks = info.quests;
        } else {
            const questsDiv = document.querySelector('#harem_right .girl_quests, .harem_right .girl_quests, [id*=harem_right] .girl_quests, [id*=harem_right] [class*=girl_quests]');
            questLinks = questsDiv ? questsDiv.querySelectorAll('a') : [];
        }
        addLogMessage(`Starting download for ${displayName}`);
        if (questLinks.length === 0) {
            addLogMessage('No scene links found for this character.', true);
            return;
        }
        questLinks.forEach((a, idx) => {
            const href = a.getAttribute('href');
            const match = href.match(/quest\/(\d+)/);
            const questNum = match ? match[1] : null;
            let url = '';
            if (window.location.hostname.includes('hentaiheroes.com')) {
                if (!questNum) {
                    addLogMessage(`Invalid quest link: ${href}`, true);
                    return;
                }
                url = `https://www.hentaiheroes.com/img/quests/${questNum}/1/800x450cut/${questNum}-1.jpg`;
            } else if (window.location.hostname.includes('nutaku.haremheroes.com')) {
                const sessMatch = href.match(/sess=([^&]+)/);
                const sessToken = sessMatch ? sessMatch[1] : info.sessToken;
                if (!questNum || !sessToken) {
                    addLogMessage(`Invalid quest link: ${href}`, true);
                    return;
                }
                url = `https://nutaku.haremheroes.com/img/quests/${questNum}/1/1600x900cut/${questNum}.jpg?sess=${sessToken}`;
            } else {
                addLogMessage(`Unsupported site: ${window.location.hostname}`, true);
                return;
            }
            const filename = `${prefix}${displayName} ${idx+1}.jpg`;
            addLogMessage(`Trying: ${url} => ${filename}`);
            GM_download({
                url: url,
                name: filename,
                onload: function(resp) {
                    addLogMessage(`Downloaded: ${filename} (success)`, false, `URL: ${url}`);
                },
                onerror: function(error) {
                    addLogMessage(`Error downloading ${filename}: ${error}`, true, `URL: ${url}`);
                }
            });
        });
    }

    // Fonction pour télécharger le log
    function downloadLogFile() {
        const blob = new Blob([exportLog.join('\n')], { type: 'text/plain' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = 'hh_download_debug.log';
        document.body.appendChild(a);
        a.click();
        setTimeout(() => {
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
        }, 100);
    }

    // Attendre que la page soit chargée
    window.addEventListener('load', function() {
        setTimeout(() => {
            createFloatingIcon();
            createControlPanel();
        }, 2000);
    });
})();