Rips manga from any site running Comici
// ==UserScript==
// @name Rippper
// @namespace http://tampermonkey.net/
// @version 2.0
// @description Rips manga from any site running Comici
// @match *://*/*
// @license MIT
// @grant none
// @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js
// ==/UserScript==
(async function () {
'use strict';
const allowedHosts = [
"rimacomiplus.jp",
"younganimal.com",
"takecomic.jp",
"kimicomi.com",
"youngchampion.jp",
"mangalt.jp",
"mangaspa.nikkan-spa.jp",
"piacomic.jp",
"comic-room-base.com",
"manga-zegra.com",
"comic-growl.com",
"asacomi.jp",
"comicpash.jp",
"comic.j-nbooks.jp",
"hayacomic.jp",
"championcross.jp",
"kansai.mag-garden.co.jp",
"comicride.jp",
"carula.jp",
"comic-medu.com",
"comics.manga-bang.com",
"bigcomics.jp",
"studio.booklista.co.jp"
]
const ui = document.createElement("div");
ui.id = "comici-dl-ui";
ui.style = `
position: fixed;
top: 20px;
right: 20px;
z-index: 1000000;
background: #1a1a1a;
color: #ffffff;
padding: 20px;
font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
font-size: 14px;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,0.6);
border: 1px solid #333;
min-width: 240px;
transition: all 0.3s ease;
`;
// Create Close Button (X)
const closeBtn = document.createElement("div");
closeBtn.textContent = "×";
closeBtn.style = `
position: absolute;
top: 10px;
right: 15px;
cursor: pointer;
font-size: 20px;
color: #888;
line-height: 1;
transition: color 0.2s;
`;
closeBtn.onmouseover = () => closeBtn.style.color = "#ff4444";
closeBtn.onmouseout = () => closeBtn.style.color = "#888";
closeBtn.onclick = () => ui.remove(); // Removes the UI from the page
const title = document.createElement("div");
title.innerHTML = "<strong style='color: #00ff88;'>Ripppper</strong>";
title.style.marginBottom = "15px";
title.style.borderBottom = "1px solid #333";
title.style.paddingBottom = "8px";
title.style.fontSize = "15px";
title.style.letterSpacing = "0.5px";
const statusText = document.createElement("div");
statusText.id = "dl-status";
statusText.textContent = "Ready to download";
statusText.style.marginBottom = "15px";
statusText.style.color = "#bbb";
// Create the Action Button
const btn = document.createElement("button");
btn.textContent = "Start Download";
btn.style = `
width: 100%;
background: #00ff88;
color: #000;
border: none;
padding: 10px;
border-radius: 6px;
font-weight: bold;
cursor: pointer;
transition: background 0.2s;
`;
btn.onmouseover = () => btn.style.background = "#00cc6e";
btn.onmouseout = () => btn.style.background = "#00ff88";
const progressBarContainer = document.createElement("div");
progressBarContainer.style = "width: 100%; background: #333; height: 6px; border-radius: 3px; margin-top: 15px; overflow: hidden; display: none;";
const progressBar = document.createElement("div");
progressBar.style = "width: 0%; background: #00ff88; height: 100%; transition: width 0.2s ease;";
console.log(window.location.href.split("/")[2] in allowedHosts)
if (allowedHosts.includes(window.location.href.split("/")[2])){
progressBarContainer.appendChild(progressBar);
ui.appendChild(closeBtn); // Add the X to the UI container
ui.appendChild(title);
ui.appendChild(statusText);
ui.appendChild(btn);
ui.appendChild(progressBarContainer);
document.body.appendChild(ui);
}
const setStatus = (txt, progress = null) => {
statusText.textContent = txt;
if (progress !== null) {
progressBarContainer.style.display = "block";
progressBar.style.width = `${progress}%`;
}
};
// ---------- TOOLS ----------
function getViewerId() {
const viewer = document.getElementById('comici-viewer');
if (!viewer) throw new Error("Viewer not found");
const id = viewer.getAttribute('comici-viewer-id') || viewer.dataset.comiciViewerId;
if (!id) throw new Error("Viewer ID missing");
const hashEl = document.getElementById('xHeader');
const hash = hashEl.getAttribute('data-series-hash')
return {
id: id,
hash: hash
};
}
function getUrl() {
const viewer = document.getElementById('comici-viewer');
if (!viewer) throw new Error("Viewer not found");
const url = viewer.getAttribute('data-api-domain')
if (!url) throw new Error("Viewer ID missing");
if (window.location.href.split('/')[2] === url){
return url
} else {
return `${window.location.href.split('/')[2]}${url}`
}
return url;
}
async function loadZip() {
if (window.JSZip) return;
return new Promise(res => {
const s = document.createElement("script");
s.src = "https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js";
s.onload = res;
document.head.appendChild(s);
});
}
async function fetchData(viewerId, startURL, hash){
if (hash !== null){
const res = await fetch(`https://${startURL}/book/contentsInfo?user-id=&comici-viewer-id=${viewerId}&page-from=0&page-to=1`)
const data = await res.json();
console.log(data);
if (!data || !data.result) throw new Error(data);
return {
pages: data.totalPages,
name: ""
}
} else {
const url = `https://${startURL}/book/episodeInfo?comici-viewer-id=${viewerId}`;
const res = await fetch(url, { credentials: "include" });
const data = await res.json();
if (!data || !data.result) throw new Error("API failed");
const result = data.result.find(({ id }) => id === viewerId);
return {
pages: result.page_count,
name: result.name,
}
}
}
async function fetchPages(viewerId, startURL, hash) {
const pages = await fetchData(viewerId, startURL, hash)
const url = `https://${startURL}/book/contentsInfo?user-id=0&comici-viewer-id=${viewerId}&page-from=0&page-to=${(pages.pages === undefined) ? 100 : pages.pages}`;
const res = await fetch(url, { credentials: "include" });
const data = await res.json();
if (!data || !data.result) throw new Error("Fetch all pages failed");
return data.result;
}
// ---------- DESCRAMBLE (VERTICAL LOGIC) ----------
async function descramble(page) {
const img = new Image();
img.crossOrigin = "anonymous";
img.src = page.imageUrl;
await img.decode();
const mapping = JSON.parse(page.scramble);
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = page.width;
canvas.height = page.height;
const cols = 4;
const rows = 4;
const tileW = canvas.width / cols;
const tileH = canvas.height / rows;
for (let i = 0; i < 16; i++) {
const sourceIndex = mapping[i];
// Use Math.floor to prevent sub-pixel rendering lines
const tileW = Math.floor(canvas.width / cols);
const tileH = Math.floor(canvas.height / rows);
// Column-major Source coordinates
const sx = Math.floor(sourceIndex / rows) * tileW;
const sy = (sourceIndex % rows) * tileH;
// Column-major Destination coordinates
const dx = Math.floor(i / rows) * tileW;
const dy = (i % rows) * tileH;
// Draw using integer dimensions to snap to the pixel grid
ctx.drawImage(img, sx, sy, tileW, tileH, dx, dy, tileW, tileH);
}
return new Promise(res => canvas.toBlob(res, "image/jpeg", 0.95));
}
// ---------- MAIN EXECUTION ----------
async function run() {
try {
btn.disabled = true;
btn.style.background = "#555";
btn.style.cursor = "not-allowed";
btn.textContent = "Processing...";
const { id, hash } = getViewerId();
setStatus("Loading ZIP library...");
await loadZip();
const zip = new JSZip();
setStatus("Fetching metadata...");
const firstURLPart = getUrl()
const pages = await fetchPages(id, firstURLPart, hash);
const total = pages.length;
for (let i = 0; i < total; i++) {
const percent = Math.round(((i + 1) / total) * 100);
setStatus(`Processing: ${i + 1}/${total}`, percent);
try {
const blob = await descramble(pages[i]);
const filename = `page_${String(pages[i].sort).padStart(3, "0")}.jpg`;
zip.file(filename, blob);
} catch (err) {
console.error("Failed page:", i, err);
}
}
setStatus("Packing ZIP...");
const content = await zip.generateAsync({ type: "blob" });
const a = document.createElement("a");
a.href = URL.createObjectURL(content);
const name = await fetchData(id, firstURLPart, hash)
a.download = `${name.name}.zip`;
a.click();
setStatus("Done!");
btn.textContent = "Download Again";
btn.disabled = false;
btn.style.background = "#00ff88";
btn.style.cursor = "pointer";
} catch (err) {
console.error(err);
setStatus("Error: " + err.message);
ui.style.border = "1px solid #ff4444";
btn.disabled = false;
btn.textContent = "Retry";
btn.style.background = "#ff4444";
}
}
// Attach click event to the confirm button
btn.addEventListener("click", run);
})();