// ==UserScript==
// @name ComicWalkerRipper
// @namespace adrian
// @author adrian
// @match https://comic-walker.com/*
// @version 1.1
// @description Download Images From Comic-Walker
// @require https://cdn.jsdelivr.net/npm/@violentmonkey/shortcut@1
// @require https://unpkg.com/@zip.js/[email protected]/dist/zip-full.min.js
// @grant GM_registerMenuCommand
// @license MIT
// ==/UserScript==
const createKey = (hash) => {
const parts = hash.slice(0, 16).match(/[\da-f]{2}/gi);
return new Uint8Array(parts.map((data) => Number.parseInt(data, 16)));
};
function createXorFunc(key) {
return (image) => {
const { length: imageLength } = image;
const { length: keyLength } = key;
const decrypted = new Uint8Array(imageLength);
for (let index = 0; index < imageLength; index += 1)
decrypted[index] = image[index] ^ key[index % keyLength];
return decrypted;
};
}
function toPng(webp) {
return new Promise((resolve, reject) => {
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
const image = new Image();
image.src = URL.createObjectURL(new Blob([webp]));
image.crossOrigin = "anonymous";
image.onload = (e) => {
canvas.width = image.width;
canvas.height = image.height;
URL.revokeObjectURL(e.target.src);
context.drawImage(e.target, 0, 0, canvas.width, canvas.height);
canvas.toBlob(
(data) => {
resolve(data);
},
"image/png",
100,
);
};
image.onerror = (e) => reject(e);
});
}
const downloadImages = async (id, title) => {
if (
!/https:\/\/comic-walker\.com\/detail\/.*\/episodes\/.*/.test(
window.location.href,
)
)
return;
const progressBar = document.createElement("div");
progressBar.id = "dl-progress";
progressBar.textContent = "Starting...";
progressBar.style.padding = "20px";
progressBar.style.backgroundColor = "black";
progressBar.style.borderRadius = "10px";
progressBar.style.border = "1px solid white";
progressBar.style.boxShadow = "0 25px 50px -12px rgb(0 0 0 / 0.25)";
progressBar.style.position = "fixed";
progressBar.style.left = "50%";
progressBar.style.top = "50%";
progressBar.style.transform = "translate(-50%,-50%)";
progressBar.style.zIndex = "9999";
progressBar.style.fontSize = "20px";
progressBar.style.color = "white";
document.body.appendChild(progressBar);
let episodeId;
if (id) {
episodeId = id;
} else {
const currentPath = window.location.pathname;
const pathSplit = currentPath.split("/");
const episodeCode = pathSplit.pop();
pathSplit.pop();
const workCode = pathSplit.pop();
const episodeData = await fetch(
`https://comic-walker.com/api/contents/details/episode?episodeCode=${episodeCode}&workCode=${workCode}&episodeType=latest`,
).then((res) => res.json());
episodeId = episodeData.episode.id;
}
if (!episodeId) {
progressBar.textContent = "unable to find episodeId.";
setTimeout(() => progressBar.remove(), 1000);
}
console.log(episodeId);
const apiData = await fetch(
`https://comic-walker.com/api/contents/viewer?episodeId=${episodeId}&imageSizeType=width:1284`,
).then((res) => res.json());
const images = apiData.manuscripts;
console.log(images);
progressBar.textContent = `${images.length} images found.`;
const zipWriter = new zip.ZipWriter(new zip.BlobWriter("application/zip"), {
bufferedWrite: true,
});
for (let i = 0; i < images.length; i++) {
const image = images[i];
const xorKey = createKey(image.drmHash);
const xorFunc = createXorFunc(xorKey);
const response = await fetch(image.drmImageUrl);
if (!response.ok) {
progressBar.textContent = `failed to fetch image ${i + 1}/${images.length}`;
throw new Error("Failed to fetch image");
}
const arrayBuffer = await response.arrayBuffer();
const decryptedData = xorFunc(new Uint8Array(arrayBuffer));
zipWriter.add(
`${i + 1}.png`,
new zip.BlobReader(await toPng(decryptedData)),
{},
);
progressBar.textContent = `fetched and decrypted image ${i + 1}/${images.length}`;
console.log("done with ", i + 1);
}
console.log("image fetching done. generating zip");
progressBar.textContent = "image fetching done. generating zip";
const blobURL = URL.createObjectURL(await zipWriter.close());
const link = document.createElement("a");
link.href = blobURL;
link.download = `${title || document.title}.zip`;
link.click();
progressBar.textContent = "done.";
setTimeout(() => progressBar.remove(), 1000);
};
function waitForElement(selector, callback) {
const observer = new MutationObserver((mutations, observer) => {
const element = document.querySelector(selector);
if (element) {
observer.disconnect();
callback(element);
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
}
function onChangeElement(selector, callback) {
const observer = new MutationObserver((mutations, observer) => {
const element = document.querySelector(selector);
if (element) {
callback(element);
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
}
onChangeElement("._mainScreen_kodus_7", (element) => {
if (element.querySelector("#dl-button")) return;
const dlButton = document.createElement("button");
dlButton.id = "dl-button";
dlButton.textContent = "Download";
dlButton.style.padding = "5px 12px";
dlButton.style.backgroundColor = "#ef0029";
dlButton.style.borderRadius = "8px";
dlButton.style.border = "3px solid #000";
dlButton.style.boxShadow = "0 4px 0 #000";
dlButton.style.position = "absolute";
dlButton.style.right = "5px";
dlButton.style.bottom = "50px";
dlButton.style.zIndex = "9999";
dlButton.style.fontSize = ".75rem";
dlButton.style.fontWeight = "800";
dlButton.style.color = "white";
dlButton.addEventListener("click", () => downloadImages());
element.appendChild(dlButton);
});
waitForElement("._mainScreen_kodus_7", (element) => {
if (element.querySelector("#dl-button")) return;
const dlButton = document.createElement("button");
dlButton.id = "dl-button";
dlButton.textContent = "Download";
dlButton.style.padding = "5px 12px";
dlButton.style.backgroundColor = "#ef0029";
dlButton.style.borderRadius = "8px";
dlButton.style.border = "3px solid #000";
dlButton.style.boxShadow = "0 4px 0 #000";
dlButton.style.position = "absolute";
dlButton.style.right = "5px";
dlButton.style.bottom = "50px";
dlButton.style.zIndex = "9999";
dlButton.style.fontSize = ".75rem";
dlButton.style.fontWeight = "800";
dlButton.style.color = "white";
dlButton.addEventListener("click", () => downloadImages());
element.appendChild(dlButton);
});
const addButtons = async (element) => {
const currentPath = window.location.pathname;
const pathSplit = currentPath.split("/");
pathSplit.pop();
pathSplit.pop();
const workCode = pathSplit.pop();
const workData = await fetch(
`https://comic-walker.com/api/contents/details/work?workCode=${workCode}`,
).then((res) => res.json());
const episodes = {};
// biome-ignore lint/complexity/noForEach: <explanation>
workData.latestEpisodes.result.forEach((episode) => {
episodes[`${episode.title} ${episode.subTitle}`] = episode.id;
});
// biome-ignore lint/complexity/noForEach: <explanation>
workData.firstEpisodes.result.forEach((episode) => {
episodes[`${episode.title} ${episode.subTitle}`] = episode.id;
});
const episodeElements = element.querySelectorAll(
".EpisodeThumbnail_infoWrapper__XWQHA",
);
for (const episode of episodeElements) {
const episodeTitle = episode.querySelector(
".EpisodeThumbnail_title__G1eWj",
);
if (episode.querySelector("#dl-button")) continue;
if (!episodes[episodeTitle.textContent]) continue;
const button = document.createElement("button");
button.textContent = "Download";
button.id = "dl-button";
button.style.padding = "5px 12px";
button.style.height = "fit-content";
button.style.backgroundColor = "#ef0029";
button.style.borderRadius = "8px";
button.style.border = "3px solid #000";
button.style.boxShadow = "0 4px 0 #000";
button.style.zIndex = "9999";
button.style.fontSize = ".75rem";
button.style.fontWeight = "800";
button.style.transform = "translateY(-3px)";
button.style.color = "white";
button.style.margin = "auto 0";
button.addEventListener(
"click",
(e) => {
e.preventDefault();
e.stopPropagation();
downloadImages(
episodes[episodeTitle.textContent],
episodeTitle.textContent.endsWith(" ")
? episodeTitle.textContent.slice(0, -1)
: episodeTitle.textContent,
);
},
false,
);
episode.style.gridTemplateColumns = "1fr min-content";
episode.appendChild(button);
}
};
waitForElement(".ContentsDetailPage_episodeList__kOQID", (element) => {
addButtons(element);
const observer = new MutationObserver((mutations, observer) => {
const element = document.querySelector(
".ContentsDetailPage_episodeList__kOQID",
);
if (element) {
addButtons(element);
}
});
observer.observe(element, {
childList: true,
subtree: true,
});
});
VM.shortcut.register("cm-s", () => downloadImages());
VM.shortcut.enable();
GM_registerMenuCommand("Download Images (Ctrl/Cmd + S)", () =>
downloadImages(),
);