ComicWalkerRipper

Download Images From Comic-Walker

// ==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(),
);