Rips manga from any site running Comici reader or Gigaviewer
// ==UserScript==
// @name Rippper
// @namespace http://tampermonkey.net/
// @version 3
// @description Rips manga from any site running Comici reader or Gigaviewer
// @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 komichiHosts = [
"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",
"hanayume.com"
]
const gigaHosts = [
"comic-days.com",
"ichicomi.com",
"shonenjumpplus.com",
"tonarinoyj.jp",
"andsofa.com",
"morningtwo.com",
"getsumagakichi.com",
"bibliosirius.com",
"kuragebunch.com",
"comicbunch-kai.com",
"viewer.heros-web.com",
"comicborder.com",
"comic-gardo.com",
"comic-zenon.com",
"magcomi.com",
"comic-action.com",
"comic-trail.com",
"feelweb.jp",
"www.sunday-webry.com",
"comic-ogyaaa.com",
"comic-earthstar.com",
"ourfeel.jp",
"comic-seasons.com",
"comic-y-ours.com"
]
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";
title.style.textAlign = "center";
const statusText = document.createElement("div");
statusText.id = "dl-status";
statusText.textContent = "Ready to download";
statusText.style.marginBottom = "15px";
statusText.style.color = "#bbb";
statusText.style.textAlign = "center"
// 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;";
let isKomichi = false;
let isGiga = false;
if (komichiHosts.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);
isKomichi = true;
} else if (gigaHosts.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);
isGiga = true;
}
const setStatus = (txt, progress = null) => {
statusText.textContent = txt;
if (progress !== null) {
progressBarContainer.style.display = "block";
progressBar.style.width = `${progress}%`;
}
};
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 descrambleImageGiga(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));
}
const waitForJsonGiga = () => 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 });
});
// ---------- TOOLS ----------
function getComiciViewerId() {
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 getUrlComici() {
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 fetchDataComici(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 fetchPagesComici(viewerId, startURL, hash) {
const pages = await fetchDataComici(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 descrambleComici(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 runComici() {
try {
btn.disabled = true;
btn.style.background = "#555";
btn.style.cursor = "not-allowed";
btn.textContent = "Processing...";
const { id, hash } = getComiciViewerId();
setStatus("Loading ZIP library...");
await loadZip();
const zip = new JSZip();
setStatus("Fetching metadata...");
const firstURLPart = getUrlComici()
const pages = await fetchPagesComici(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 descrambleComici(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 fetchDataComici(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";
}
}
async function runGiga() {
await waitForJsonGiga();
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) {
setStatus("No pages found.");
return;
}
setStatus(`Found ${pages.length} pages.`);
await loadZip();
const zip = new JSZip();
for (let i = 0; i < pages.length; i++) {
const progress = Math.round(((i + 1) / pages.length) * 100);
setStatus(`Processing page ${i + 1}/${pages.length}...`, progress);
try {
const res = await fetch(pages[i].src);
const blob = await res.blob();
const fixed = await descrambleImageGiga(blob);
zip.file(`page_${(i + 1).toString().padStart(4, '0')}.jpg`, fixed);
} catch (e) {
console.error(e);
}
}
setStatus("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();
setStatus("Download Complete!");
}
if (isKomichi){
btn.addEventListener("click", runComici);
} else if (isGiga) {
btn.addEventListener("click", runGiga);
}
// Attach click event to the confirm button
})();