Skip to content

Instantly share code, notes, and snippets.

@xgorn
Created June 9, 2026 06:38
Show Gist options
  • Select an option

  • Save xgorn/4aa6d87df14e020f6a5f5de02a37655a to your computer and use it in GitHub Desktop.

Select an option

Save xgorn/4aa6d87df14e020f6a5f5de02a37655a to your computer and use it in GitHub Desktop.
Download asurascans chapter as pdf to read it offline
// ==UserScript==
// @name Asurascans PDF Downloader
// @namespace http://xgorn.com/
// @version 0.0.1
// @description Download asurascans chapter as pdf to read it offline
// @author Noid
// @include /^https:\/\/asurascans\.com\/comics\/[^\/]+\/?$/
// @icon https://www.google.com/s2/favicons?sz=64&domain=asurascans.com
// @grant GM_xmlhttpRequest
// @require https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js
// ==/UserScript==
function waitForElm(selector) {
return new Promise(resolve => {
if (document.querySelector(selector)) {
return resolve(document.querySelector(selector));
}
const observer = new MutationObserver(mutations => {
if (document.querySelector(selector)) {
observer.disconnect();
resolve(document.querySelector(selector));
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
});
}
(async function() {
'use strict';
const { jsPDF } = window.jspdf;
const chapterCache = [];
let start_title = '';
let end_title = '';
async function downloadChapterWithRetry(chapterUrl, button, title, maxRetries = 5) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
button.textContent = `Attempt ${attempt}/${maxRetries}`;
await downloadChapter(chapterUrl, button, title);
// Success
return;
} catch (err) {
lastError = err;
console.error(
`Attempt ${attempt} failed:`,
err
);
if (attempt < maxRetries) {
await new Promise(resolve =>
setTimeout(resolve, 2000)
);
}
}
}
throw lastError;
};
async function downloadChapter(chapterUrl, button, title) {
button.textContent = 'Loading...';
const response = await fetch(chapterUrl);
const html = await response.text();
const doc = new DOMParser().parseFromString(html, 'text/html');
const reader = doc.querySelector('.max-w-full');
if (!reader) {
throw new Error('Reader element not found');
}
const imageUrls = [
...doc.querySelectorAll('.max-w-full img')
]
.map(img =>
img.dataset.src ||
img.getAttribute('data-src') ||
img.dataset.lazySrc ||
img.dataset.original ||
img.src
)
.filter(Boolean);
if (!imageUrls.length) {
throw new Error('No images found');
}
await createPDF(
imageUrls,
title,
button
);
}
async function createPDF(
imageUrls,
title,
button
) {
for (let i = 0; i < imageUrls.length; i++) {
button.textContent =
`Page ${i + 1}/${imageUrls.length}`;
try {
if (!chapterCache.length) {
start_title = title
} else {
end_title = title
};
const blob =
await downloadBlob(
imageUrls[i]
);
console.log(blob);
chapterCache.push(blob);
} catch (err) {
console.error(
'Failed image:',
imageUrls[i],
err
);
}
}
button.textContent =
'✅ Downloaded';
}
function downloadBlob(url) {
return new Promise(
(resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url,
responseType: 'blob',
headers: {
Referer:
'https://asurascans.com/'
},
onload: response => {
if (
response.status !== 200
) {
reject(
new Error(
`HTTP ${response.status}`
)
);
return;
}
resolve(
response.response
);
},
onerror: reject
});
}
);
}
async function blobToJPEG(blob) {
const bitmap =
await createImageBitmap(blob);
const canvas =
document.createElement(
'canvas'
);
canvas.width =
bitmap.width;
canvas.height =
bitmap.height;
const ctx =
canvas.getContext('2d');
ctx.drawImage(
bitmap,
0,
0
);
const jpegBlob =
await new Promise(resolve =>
canvas.toBlob(
resolve,
'image/jpeg',
0.95
)
);
const dataUrl =
await blobToDataURL(
jpegBlob
);
return {
dataUrl,
width: bitmap.width,
height: bitmap.height
};
}
function blobToDataURL(blob) {
return new Promise(resolve => {
const reader =
new FileReader();
reader.onload =
() =>
resolve(
reader.result
);
reader.readAsDataURL(
blob
);
});
}
const observer = new MutationObserver(() => {
document.querySelectorAll(
'.divide-y [href*="/chapter/"]:not([data-download-added])'
).forEach(chapterLink => {
chapterLink.dataset.downloadAdded = 'true';
const btn = document.createElement('button');
btn.textContent = 'Download';
btn.className = 'px-4 py-2.5 text-xs font-semibold text-white bg-[#913FE2] hover:bg-[#7c35c4] rounded-lg transition-colors cursor-pointer disabled:opacity-50 whitespace-nowrap';
btn.style = 'margin-left: 10px;'
btn.addEventListener('click', async e => {
e.preventDefault();
e.stopPropagation();
const chapterCount = btn.closest('.flex').querySelector('.font-medium').textContent;
console.log(chapterCount);
await downloadChapterWithRetry(
chapterLink.href,
btn,
chapterCount,
10 // max retries
);
});
chapterLink.appendChild(btn);
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
const saveAllBtn = document.createElement('button');
saveAllBtn.textContent = '💾 Save All as PDF';
saveAllBtn.style.cssText = `
position:fixed;
bottom:20px;
right:20px;
z-index:99999;
padding:10px 15px;
background:#2196f3;
color:white;
border:none;
border-radius:6px;
cursor:pointer;
`;
document.body.appendChild(saveAllBtn);
saveAllBtn.addEventListener('click', async () => {
if (!chapterCache.length) {
alert('No chapters queued');
return;
}
let pdf = null;
let i = 0
for (const blob of chapterCache) {
console.log(blob);
saveAllBtn.textContent = `Progress: ${i+1}/${chapterCache.length}`;
const {
dataUrl,
width,
height
} = await blobToJPEG(blob);;
const orientation =
width > height
? 'landscape'
: 'portrait';
if (!pdf) {
pdf = new jsPDF({
orientation,
unit: 'px',
format: [
width,
height
]
});
} else {
pdf.addPage(
[width, height],
orientation
);
pdf.setPage(i + 1);
}
pdf.addImage(
dataUrl,
'JPEG',
0,
0,
width,
height
);
i++
}
let full_title = '';
if (!end_title) {
full_title = start_title
} else {
full_title = `${start_title} - ${end_title}`
}
const title = document.title.split('|')[0].trim()
pdf.save(`${title} ${full_title}.pdf`);
});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment