Skip to content

Instantly share code, notes, and snippets.

@tezkod
Created January 21, 2026 14:43
Show Gist options
  • Select an option

  • Save tezkod/86c4361bae9d92ef944513b0fba26050 to your computer and use it in GitHub Desktop.

Select an option

Save tezkod/86c4361bae9d92ef944513b0fba26050 to your computer and use it in GitHub Desktop.
Download McGraw Hill eBooks

Download McGraw Hill eBooks eTextbooks

Thanks 101arrowz for inspiration

Features

  • Better JSZip loading: handles default/JSZip/global to avoid “not a constructor” errors
  • Parallel downloads: limited concurrency speeds up large books versus strictly serial fetches
  • Retry logic: per-file retries with backoff instead of aborting on the first failure

How to use script

1. Open an GOOGLE CHROME incognito window

2. Open your book/textbook normally [https://myebooks.mheducation.com/]

3. Manually type 'javascript:'

4. Copy and paste the javascript code after the :

5. Press enter and wait for book to download

This code is provided strictly for educational, research, and interoperability purposes. It is intended to demonstrate how web-delivered content can be programmatically accessed by users who already have lawful access to that content. The author does not host, distribute, or provide any copyrighted material, nor does this project enable access to content that has not been legitimately purchased or licensed by the user. Any use of this code must comply with applicable laws, including the Australian Copyright Act 1968 (Cth), and the terms and conditions of the relevant content provider. The author does not encourage or support copyright infringement, redistribution, or commercial use, and accepts no responsibility for misuse of this code.

// McGraw-Hill EPUB downloader:
// Wrap everything in an async IIFE so it can run as a one-off script
javascript:(async () => {
// Prevent multiple instances of the downloader from running at once
if (window.__mheDlRunning) return;
window.__mheDlRunning = true;
// Create and inject a small floating UI panel into the page
const addUI = () => {
const box = document.createElement('div');
box.id = 'mhe-dl';
box.style = 'position:fixed;z-index:2147483647;right:16px;bottom:16px;width:320px;padding:12px;background:#0b172a;color:#f0f4ff;font:12px/1.4 system-ui;border:1px solid #243b5a;border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,0.35)';
box.innerHTML = `
<div style="font-weight:600;margin-bottom:6px;">McGraw-Hill EPUB Downloader</div>
<div id="mhe-status" style="margin-bottom:6px;">Starting…</div>
<div style="height:8px;background:#12233d;border-radius:4px;overflow:hidden;">
<div id="mhe-bar" style="height:100%;width:0%;background:#3ad1ff;transition:width 0.15s;"></div>
</div>
<div id="mhe-sub" style="margin-top:6px;opacity:0.8;">Idle</div>
<div id="mhe-err" style="margin-top:6px;color:#ffb3b3;display:none;"></div>
<div style="margin-top:8px;border-top:1px solid #243b5a;padding-top:6px;">
<div id="mhe-log-toggle" style="cursor:pointer;opacity:0.7;font-size:11px;margin-bottom:4px;">▼ Log</div>
<div id="mhe-log" style="display:block;max-height:200px;overflow-y:auto;background:#0a1220;padding:6px;border-radius:4px;font-size:11px;font-family:monospace;line-height:1.4;"></div>
</div>
`;
// Attach the UI to the current page
document.body.appendChild(box);
const toggleBtn = document.getElementById('mhe-log-toggle');
const logEl = document.getElementById('mhe-log');
toggleBtn.addEventListener('click', () => {
const isHidden = logEl.style.display === 'none';
logEl.style.display = isHidden ? 'block' : 'none';
toggleBtn.textContent = isHidden ? '▼ Log' : '▲ Log';
});
// Return helpers to update the UI from the rest of the script
return {
setStatus: (t) => (document.getElementById('mhe-status').textContent = t),
setSub: (t) => (document.getElementById('mhe-sub').textContent = t),
setBar: (p) => (document.getElementById('mhe-bar').style.width = `${Math.max(0, Math.min(100, p))}%`),
showErr: (t) => {
const el = document.getElementById('mhe-err');
el.textContent = t;
el.style.display = 'block';
},
log: (msg, type = 'info') => {
const el = document.getElementById('mhe-log');
const time = new Date().toLocaleTimeString();
const colors = { info: '#a0c4ff', success: '#90ee90', warn: '#ffd700', error: '#ff6b6b' };
const entry = document.createElement('div');
entry.style.color = colors[type] || colors.info;
entry.style.marginBottom = '2px';
entry.textContent = `[${time}] ${msg}`;
el.appendChild(entry);
el.scrollTop = el.scrollHeight;
},
close: () => box.remove(),
};
};
const ui = addUI();
// Simple delay helper for retries and pacing
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
// Clean up a string so it is safe to use as a filename on most systems
const sanitizeName = (s) => (s || 'textbook').replace(/[<>:"/\\|?*\x00-\x1F]/g, '').trim().slice(0, 80) || 'textbook';
try {
ui.setStatus('Loading JSZip…');
ui.log('Loading JSZip library from CDN...', 'info');
// Dynamically load JSZip from a CDN (no local dependency needed)
const mod = await import('https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js');
const JSZip = mod?.default || mod?.JSZip || window.JSZip;
if (!JSZip) throw new Error('JSZip failed to load');
ui.log('JSZip loaded successfully', 'success');
ui.setStatus('Fetching base URL…');
ui.log('Fetching EPUB base URL from player API...', 'info');
// Ask the McGraw-Hill player API for the base URL of the current book’s EPUB assets
const baseUrl = (await (await fetch('https://player-api.mheducation.com/lti', { credentials: 'include' })).json()).custom_epub_url;
ui.log(`Base URL: ${baseUrl}`, 'success');
// Create the EPUB folder structure inside a new ZIP
const zip = new JSZip();
const meta = zip.folder('META-INF');
const ops = zip.folder('OPS');
ui.setStatus('Fetching container & OPF…');
ui.log('Fetching META-INF/container.xml...', 'info');
meta.file('container.xml', await (await fetch(baseUrl + 'META-INF/container.xml', { credentials: 'include' })).text());
ui.log('Fetching OPS/content.opf...', 'info');
const opfText = await (await fetch(baseUrl + 'OPS/content.opf', { credentials: 'include' })).text();
ops.file('content.opf', opfText);
ui.log('Parsing OPF manifest...', 'info');
// Parse the OPF file to get the manifest (list of all book files)
const opf = new DOMParser().parseFromString(opfText, 'application/xml');
const items = [...opf.querySelectorAll('manifest item')];
const total = items.length;
ui.log(`Found ${total} files in manifest`, 'success');
let done = 0;
const errors = [];
const concurrency = 6;
const retries = 2;
// Download a single manifest item with retry logic
const fetchFile = async (href) => {
let lastErr;
for (let i = 0; i <= retries; i++) {
try {
if (i > 0) ui.log(`Retrying ${href} (attempt ${i + 1}/${retries + 1})...`, 'warn');
const res = await fetch(baseUrl + 'OPS/' + href, { credentials: 'include' });
if (!res.ok) throw new Error('HTTP ' + res.status);
const data = await res.arrayBuffer();
if (i > 0) ui.log(`Successfully downloaded ${href} after retry`, 'success');
return data;
} catch (e) {
lastErr = e;
if (i < retries) await sleep(200 * (i + 1));
}
}
throw lastErr;
};
ui.setStatus('Downloading files…');
ui.log(`Starting download with ${concurrency} concurrent workers...`, 'info');
const queue = [...items];
// Worker loop: repeatedly take the next item from the queue and download it
const worker = async () => {
while (queue.length) {
const item = queue.shift();
const href = item.getAttribute('href');
try {
ui.log(`Downloading: ${href}`, 'info');
const data = await fetchFile(href);
ops.file(href, data);
done++;
ui.setBar((done / total) * 100);
ui.setSub(`Downloaded ${done}/${total}: ${href}`);
ui.log(`✓ ${href} (${(data.byteLength / 1024).toFixed(1)} KB)`, 'success');
} catch (e) {
errors.push({ href, error: e });
ui.setSub(`Failed ${href}`);
ui.log(`✗ Failed: ${href} - ${e.message}`, 'error');
}
}
};
await Promise.all(Array.from({ length: concurrency }, worker));
ui.log(`Download complete: ${done}/${total} files, ${errors.length} failed`, errors.length > 0 ? 'warn' : 'success');
// If some files failed, warn the user but still produce an EPUB
if (errors.length) {
ui.showErr(`Skipped ${errors.length} file(s); EPUB may miss assets.`);
ui.log(`Warning: ${errors.length} files failed to download`, 'warn');
}
ui.setStatus('Compressing EPUB…');
ui.log('Compressing EPUB archive...', 'info');
// Compress everything into a single EPUB (ZIP) blob
const blob = await zip.generateAsync(
{ type: 'blob', compression: 'DEFLATE', compressionOptions: { level: 6 } },
(meta) => {
ui.setBar(meta.percent || 0);
if (meta.percent % 10 < 1) ui.log(`Compression: ${Math.floor(meta.percent)}%`, 'info');
}
);
ui.log(`EPUB compressed: ${(blob.size / 1024 / 1024).toFixed(2)} MB`, 'success');
ui.setStatus('Saving…');
// Use the textbook title as the EPUB filename
const title = sanitizeName(opf.querySelector('metadata title')?.textContent);
ui.log(`Saving as: ${title}.epub`, 'info');
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${title}.epub`;
a.click();
URL.revokeObjectURL(url);
ui.setBar(100);
ui.setSub('Done');
ui.setStatus('EPUB ready');
ui.log('Download complete!', 'success');
await sleep(1500);
ui.close();
} catch (err) {
// Handle any unexpected fatal error and keep the UI consistent
ui.showErr(String(err));
ui.setStatus('Failed');
ui.log(`Fatal error: ${err.message}`, 'error');
} finally {
// Allow the script to be run again in the future
window.__mheDlRunning = false;
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment