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