Rips the raws directly from the API and then de-scrambles them.
// ==UserScript==
// @name Comic Days Ripper
// @version 1.0
// @description Rips the raws directly from the API and then de-scrambles them.
// @author mikhi32
// @match https://comic-days.com/*
// @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js
// @grant none
// @license Apache 2.0
// @namespace https://greasyfork.org/users/1584825
// ==/UserScript==
(async function() {
'use strict';
// --- UI Helpers ---
const createUI = () => {
const container = document.createElement('div');
container.id = 'downloader-ui';
Object.assign(container.style, {
position: 'fixed', top: '20px', right: '20px', zIndex: '9999',
backgroundColor: '#000000', padding: '20px',
borderRadius: '8px', boxShadow: '0 4px 15px rgba(0,0,0,0.5)',
fontFamily: 'sans-serif', minWidth: '250px', border: '1px solid #333'
});
container.innerHTML = `
<h3 style="margin:0 0 10px 0; font-size: 16px; color:white;">Comic Days Ripper</h3>
<div id="ui-content">
<p id="ui-msg" style="font-size: 13px; margin-bottom: 15px; color: white;">Detecting pages...</p>
<div id="ui-progress-wrapper" style="display:none; background:#000000; height:10px; border-radius:5px; overflow:hidden;">
<div id="ui-progress-bar" style="width:0%; height:100%; background:#00c853; transition: width 0.2s;"></div>
</div>
<div id="ui-actions" style="margin-top:15px; display:flex; gap:10px;"></div>
</div>
`;
document.body.appendChild(container);
return container;
};
const updateStatus = (msg, percent = null) => {
const msgEl = document.getElementById('ui-msg');
const bar = document.getElementById('ui-progress-bar');
const wrapper = document.getElementById('ui-progress-wrapper');
if (msgEl) msgEl.innerText = msg;
if (percent !== null) {
wrapper.style.display = 'block';
bar.style.width = `${percent}%`;
}
};
// --- De-scramble Logic ---
async function descrambleImage(blob, divideNum = 4, multiple = 8) {
const img = new Image();
const url = URL.createObjectURL(blob);
await new Promise(res => { img.onload = res; img.src = url; });
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
const totalTiles = divideNum * multiple;
const cellWidth = Math.floor(img.width / totalTiles) * multiple;
const cellHeight = Math.floor(img.height / totalTiles) * multiple;
ctx.drawImage(img, 0, 0);
for (let i = 0; i < totalTiles * totalTiles; i++) {
const row = Math.floor(i / totalTiles);
const col = i % totalTiles;
const sourceX = col * cellWidth;
const sourceY = row * cellHeight;
const destIndex = col * totalTiles + row;
const destX = (destIndex % totalTiles) * cellWidth;
const destY = Math.floor(destIndex / totalTiles) * cellHeight;
if (destX < img.width && destY < img.height) {
ctx.drawImage(img, sourceX, sourceY, cellWidth, cellHeight, destX, destY, cellWidth, cellHeight);
}
}
URL.revokeObjectURL(url);
return new Promise(res => canvas.toBlob(res, 'image/jpeg', 0.95));
}
// --- Main Logic ---
const waitForJson = () => new Promise(resolve => {
const check = () => document.getElementById('episode-json');
if (check()) return resolve();
const obs = new MutationObserver(() => { if (check()) { obs.disconnect(); resolve(); } });
obs.observe(document.body, { childList: true, subtree: true });
});
await waitForJson();
const ui = createUI();
const script = document.getElementById('episode-json');
const json = JSON.parse(script?.dataset?.value || script?.textContent.trim() || "{}");
const pages = json.readableProduct?.pageStructure?.pages?.filter(p => p.type === 'main' && p.src) || [];
if (pages.length === 0) {
updateStatus("No pages found.");
return;
}
updateStatus(`Found ${pages.length} pages.`);
const actionArea = document.getElementById('ui-actions');
const dlBtn = document.createElement('button');
dlBtn.innerText = "Download ZIP";
Object.assign(dlBtn.style, {
padding: '8px 12px', border: 'none', borderRadius: '4px',
backgroundColor: '#00c853', color: 'white', cursor: 'pointer', fontWeight: 'bold'
});
actionArea.appendChild(dlBtn);
dlBtn.onclick = async () => {
dlBtn.remove();
const zip = new JSZip();
for (let i = 0; i < pages.length; i++) {
const progress = Math.round(((i + 1) / pages.length) * 100);
updateStatus(`Processing page ${i + 1}/${pages.length}...`, progress);
try {
const res = await fetch(pages[i].src);
const blob = await res.blob();
const fixed = await descrambleImage(blob);
zip.file(`page_${(i + 1).toString().padStart(4, '0')}.jpg`, fixed);
} catch (e) {
console.error(e);
}
}
updateStatus("Downloading...");
const content = await zip.generateAsync({ type: "blob" });
const zipUrl = URL.createObjectURL(content);
const a = document.createElement('a');
a.href = zipUrl;
a.download = `${json.readableProduct.title}.zip`;
a.click();
updateStatus("Download Complete!");
setTimeout(() => ui.remove(), 5000);
};
})();