Created
June 9, 2026 06:38
-
-
Save xgorn/4aa6d87df14e020f6a5f5de02a37655a to your computer and use it in GitHub Desktop.
Download asurascans chapter as pdf to read it offline
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==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