// ==UserScript==
// @name BookWalker Downloader
// @namespace http://tampermonkey.net/
// @version 1.3
// @description Download full and preview books from BookWalker
// @author GolyBidoof
// @match https://viewer.bookwalker.jp/*
// @match https://viewer-trial.bookwalker.jp/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=bookwalker.jp
// @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js
// @grant none
// @run-at document-start
// @license MIT
// ==/UserScript==
(function() {
'use strict';
const bookData = {
isPreview: false,
config: null,
pages: [],
title: null,
targetWidth: null,
targetHeight: null
};
const originalFetch = window.fetch;
window.fetch = async function(...args) {
const response = await originalFetch.apply(this, args);
const url = args[0];
if (typeof url === 'string') {
if (url.includes('configuration_pack.json')) {
const clonedResponse = response.clone();
try {
const configData = await clonedResponse.json();
bookData.isPreview = true;
bookData.config = configData;
if (configData.configuration && configData.configuration.contents) {
bookData.pages = configData.configuration.contents;
}
const firstPageKey = Object.keys(configData).find(key => key.includes('.xhtml'));
if (firstPageKey && configData[firstPageKey]) {
const pageInfo = configData[firstPageKey];
if (pageInfo.FileLinkInfo && pageInfo.FileLinkInfo.PageLinkInfoList && pageInfo.FileLinkInfo.PageLinkInfoList[0]) {
const pageData = pageInfo.FileLinkInfo.PageLinkInfoList[0].Page;
bookData.targetWidth = pageData.Size.Width;
bookData.targetHeight = pageData.Size.Height;
}
if (pageInfo.Title) {
bookData.title = pageInfo.Title;
}
}
updateDownloadButton();
} catch (error) {}
} else if (url.includes('.xhtml.region') && url.includes('.json')) {
const clonedResponse = response.clone();
try {
const regionData = await clonedResponse.json();
if (regionData.w && regionData.h) {
bookData.targetWidth = regionData.w;
bookData.targetHeight = regionData.h;
}
} catch (error) {}
}
}
return response;
};
const originalXHR = window.XMLHttpRequest;
window.XMLHttpRequest = function() {
const xhr = new originalXHR();
const originalOpen = xhr.open;
const originalSend = xhr.send;
let requestURL = '';
xhr.open = function(method, url, ...rest) {
requestURL = url;
return originalOpen.apply(this, [method, url, ...rest]);
};
xhr.send = function(...args) {
if (requestURL.includes('configuration_pack.json')) {
const originalOnLoad = xhr.onload;
xhr.onload = function() {
try {
const configData = JSON.parse(xhr.responseText);
bookData.isPreview = true;
bookData.config = configData;
if (configData.configuration && configData.configuration.contents) {
bookData.pages = configData.configuration.contents;
}
const firstPageKey = Object.keys(configData).find(key => key.includes('.xhtml'));
if (firstPageKey && configData[firstPageKey]) {
const pageInfo = configData[firstPageKey];
if (pageInfo.FileLinkInfo && pageInfo.FileLinkInfo.PageLinkInfoList && pageInfo.FileLinkInfo.PageLinkInfoList[0]) {
const pageData = pageInfo.FileLinkInfo.PageLinkInfoList[0].Page;
bookData.targetWidth = pageData.Size.Width;
bookData.targetHeight = pageData.Size.Height;
}
if (pageInfo.Title) {
bookData.title = pageInfo.Title;
}
}
updateDownloadButton();
} catch (e) {}
if (originalOnLoad) {
originalOnLoad.apply(this, arguments);
}
};
} else if (requestURL.includes('.xhtml.region') && requestURL.includes('.json')) {
const originalOnLoad = xhr.onload;
xhr.onload = function() {
try {
const regionData = JSON.parse(xhr.responseText);
if (regionData.w && regionData.h) {
bookData.targetWidth = regionData.w;
bookData.targetHeight = regionData.h;
}
} catch (e) {}
if (originalOnLoad) {
originalOnLoad.apply(this, arguments);
}
};
}
return originalSend.apply(this, args);
};
return xhr;
};
window.addEventListener('DOMContentLoaded', () => {
setTimeout(initializeDownloader, 3000);
});
function getTotalPages() {
const counter = document.querySelector('#pageSliderCounter');
if (counter) {
const match = counter.textContent.match(/\/(\d+)/);
if (match) return parseInt(match[1]);
}
if (bookData.isPreview) {
return bookData.pages.length;
}
return 0;
}
function getCurrentPage() {
const counter = document.querySelector('#pageSliderCounter');
if (counter) {
const match = counter.textContent.match(/(\d+)\//);
if (match) return parseInt(match[1]);
}
return 1;
}
function isRTL() {
const slider = document.querySelector('#pageSliderBar');
if (!slider || !window.$) return true;
try {
const handle = slider.querySelector('.ui-slider-handle');
if (handle) {
const leftPos = parseFloat(handle.style.left);
return leftPos > 50;
}
} catch (e) {}
return true;
}
async function goToNextPage() {
const slider = document.querySelector('#pageSliderBar');
if (!slider || !window.$) return false;
const targetPage = getCurrentPage() + 1;
if (targetPage > getTotalPages()) return false;
try {
const sliderValue = $(slider).slider('value');
$(slider).slider('value', downloadState.isRTL ? sliderValue - 1 : sliderValue + 1);
let waited = 0;
while (waited < 1000) {
await new Promise(r => setTimeout(r, 20));
if (getCurrentPage() === targetPage) return true;
waited += 20;
}
return false;
} catch (e) {
return false;
}
}
async function goToFirstPage() {
const slider = document.querySelector('#pageSliderBar');
if (!slider || !window.$) return false;
const totalPages = getTotalPages();
if (totalPages === 0) return false;
try {
const targetValue = downloadState.isRTL ? $(slider).slider('option', 'max') : $(slider).slider('option', 'min');
$(slider).slider('value', targetValue);
let waited = 0;
while (waited < 3000) {
await new Promise(r => setTimeout(r, 100));
if (getCurrentPage() === 1) return true;
waited += 100;
}
return getCurrentPage() === 1;
} catch (e) {
return false;
}
}
function waitForImageLoaded(timeout = 1000) {
return new Promise((resolve) => {
const start = Date.now();
function check() {
try {
const canvas = document.querySelector('.currentScreen canvas');
if (canvas) {
const ctx = canvas.getContext('2d', { willReadFrequently: true });
const data = ctx.getImageData(0, 0, Math.min(20, canvas.width), Math.min(20, canvas.height)).data;
for (let i = 0; i < data.length; i += 4) {
if (data[i] > 0 || data[i + 1] > 0 || data[i + 2] > 0) {
resolve();
return;
}
}
}
} catch (e) {
resolve();
return;
}
if (Date.now() - start > timeout) {
resolve();
return;
}
setTimeout(check, 10);
}
check();
});
}
async function waitForCanvasResize(w, h, timeout = 2000) {
let waited = 0;
while (waited < timeout) {
const canvas = document.querySelector('.currentScreen canvas');
if (canvas && canvas.width >= w && canvas.height >= h) {
return true;
}
await new Promise(r => setTimeout(r, 30));
waited += 30;
}
return false;
}
async function resizeViewerOnce(width, height) {
const renderer = document.querySelector('#renderer, .renderer');
const dpr = window.devicePixelRatio || 1;
if (renderer) {
renderer.style.width = width + 'px';
renderer.style.height = height + 'px';
}
const viewports = document.querySelectorAll('[id^="viewport"]');
viewports.forEach(viewport => {
viewport.style.width = width + 'px';
viewport.style.height = height + 'px';
viewport.style.overflow = 'visible';
const canvas = viewport.querySelector('canvas');
if (canvas) {
canvas.width = width * dpr;
canvas.height = height * dpr;
canvas.style.width = width + 'px';
canvas.style.height = height + 'px';
}
});
window.dispatchEvent(new Event('resize'));
await new Promise(r => setTimeout(r, 300));
await waitForCanvasResize(width * dpr, height * dpr);
}
async function waitForCanvasContent(timeout = 400) {
let waited = 0;
while (waited < timeout) {
const canvas = document.querySelector('.currentScreen canvas');
if (canvas) {
const ctx = canvas.getContext('2d', { willReadFrequently: true });
const size = Math.min(30, canvas.width, canvas.height);
const data = ctx.getImageData(0, 0, size, size).data;
let pixels = 0;
for (let i = 0; i < data.length; i += 4) {
if (data[i] > 5 || data[i + 1] > 5 || data[i + 2] > 5) {
pixels++;
}
}
if (pixels > 10) return true;
}
await new Promise(r => setTimeout(r, 10));
waited += 10;
}
return false;
}
async function captureCurrentPage(w = bookData.targetWidth, h = bookData.targetHeight) {
const canvas = document.querySelector('.currentScreen canvas');
if (!canvas) throw new Error('No canvas');
await waitForCanvasContent();
const dpr = window.devicePixelRatio || 1;
if (canvas.width === w * dpr && canvas.height === h * dpr) {
const temp = document.createElement('canvas');
temp.width = w;
temp.height = h;
const ctx = temp.getContext('2d');
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(canvas, 0, 0, w * dpr, h * dpr, 0, 0, w, h);
return temp.toDataURL('image/webp', 0.95) || temp.toDataURL('image/jpeg', 0.95);
}
return canvas.toDataURL('image/webp', 0.95) || canvas.toDataURL('image/jpeg', 0.95);
}
function dataURLtoBlob(dataURL) {
const arr = dataURL.split(',');
const mime = arr[0].match(/:(.*?);/)[1];
const bstr = atob(arr[1]);
let n = bstr.length;
const u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new Blob([u8arr], { type: mime });
}
function getBookTitle() {
try {
if (bookData.title) return bookData.title;
const pageTitleElement = document.querySelector('#pagetitle .titleText, #pagetitle');
if (pageTitleElement) {
const title = pageTitleElement.textContent || pageTitleElement.getAttribute('title');
if (title && title.trim()) {
return title.trim();
}
}
const titleElement = document.querySelector('title');
if (titleElement) {
return titleElement.textContent.trim();
}
return 'manga';
} catch (error) {
return 'manga';
}
}
function createSafeFilename(title) {
return title.replace(/[<>:"/\\|?*]/g, '_').replace(/\s+/g, '_').substring(0, 100);
}
async function createZipArchive(images, bookTitle = 'manga') {
const zip = new JSZip();
for (const image of images) {
const blob = dataURLtoBlob(image.data);
zip.file(image.filename, blob);
}
const content = await zip.generateAsync({ type: 'blob' });
const url = URL.createObjectURL(content);
const a = document.createElement('a');
a.href = url;
a.download = createSafeFilename(bookTitle) + '.zip';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
const downloadState = {
images: [],
isDownloading: false,
shouldStop: false,
currentPage: 0,
totalPages: 0,
isRTL: true
};
function createDownloadButton() {
const existingButton = document.getElementById('manga-download-btn');
if (existingButton) existingButton.remove();
const button = document.createElement('button');
button.id = 'manga-download-btn';
if (!bookData.targetWidth || !bookData.targetHeight) {
button.textContent = 'Loading...';
button.disabled = true;
button.style.cssText = 'position:fixed;top:20px;right:20px;z-index:9999;padding:10px 20px;background:#999;color:white;border:none;border-radius:5px;font-size:14px;cursor:not-allowed;font-family:Arial,sans-serif';
} else {
button.textContent = 'Download';
button.disabled = false;
button.style.cssText = 'position:fixed;top:20px;right:20px;z-index:9999;padding:10px 20px;background:#ff6b35;color:white;border:none;border-radius:5px;font-size:14px;cursor:pointer;font-family:Arial,sans-serif';
}
button.onclick = async () => {
if (button.disabled) return;
if (downloadState.isDownloading) {
downloadState.shouldStop = true;
button.textContent = 'Stopping...';
} else if (downloadState.images.length > 0) {
await downloadCurrentProgress();
} else {
await downloadAllPages();
}
};
document.body.appendChild(button);
return button;
}
function updateDownloadButton() {
const button = document.getElementById('manga-download-btn');
if (!button) return;
if (downloadState.isDownloading) {
button.textContent = `Stop (${downloadState.images.length})`;
button.disabled = false;
button.style.background = '#ff6b35';
button.style.cursor = 'pointer';
} else if (!bookData.targetWidth || !bookData.targetHeight) {
button.textContent = 'Loading...';
button.disabled = true;
button.style.background = '#999';
button.style.cursor = 'not-allowed';
} else {
const totalPages = getTotalPages();
if (totalPages > 0) {
button.textContent = `Download (${totalPages})`;
button.disabled = false;
button.style.background = '#ff6b35';
button.style.cursor = 'pointer';
} else {
button.textContent = 'Waiting...';
button.disabled = true;
button.style.background = '#999';
button.style.cursor = 'not-allowed';
}
}
}
async function downloadCurrentProgress() {
if (downloadState.images.length === 0) return;
const bookTitle = getBookTitle();
await createZipArchive(downloadState.images, bookTitle);
}
async function downloadAllPages(width = bookData.targetWidth, height = bookData.targetHeight) {
if (downloadState.isDownloading) return;
if (!bookData.targetWidth || !bookData.targetHeight) {
await new Promise(resolve => {
const checkInterval = setInterval(() => {
if (bookData.targetWidth && bookData.targetHeight) {
clearInterval(checkInterval);
resolve();
}
}, 500);
setTimeout(() => {
clearInterval(checkInterval);
resolve();
}, 10000);
});
}
downloadState.isDownloading = true;
downloadState.shouldStop = false;
downloadState.images = [];
const totalPages = getTotalPages();
downloadState.totalPages = totalPages;
if (totalPages === 0) {
downloadState.isDownloading = false;
return;
}
width = bookData.targetWidth;
height = bookData.targetHeight;
downloadState.isRTL = isRTL();
await goToFirstPage();
await new Promise(r => setTimeout(r, 500));
await resizeViewerOnce(width, height);
const bookTitle = getBookTitle();
console.log(`Downloading ${totalPages} pages...`);
for (let i = 1; i <= totalPages; i++) {
if (downloadState.shouldStop) break;
try {
downloadState.currentPage = i;
updateDownloadButton();
if (i > 1) {
await goToNextPage();
await new Promise(r => setTimeout(r, 800));
}
await waitForImageLoaded();
let finalImageData = await captureCurrentPage(width, height);
let retryCount = 0;
const maxRetries = 12;
const hashImage = (data) => {
return data.substring(100, 500) +
data.substring(1000, 1400) +
data.substring(2000, 2400) +
data.substring(5000, 5400) +
data.substring(10000, 10400);
};
const findDuplicate = (data) => {
if (downloadState.images.length === 0) return false;
const hash = hashImage(data);
const checkLast = Math.min(5, downloadState.images.length);
for (let j = 0; j < checkLast; j++) {
const idx = downloadState.images.length - 1 - j;
if (hash === hashImage(downloadState.images[idx].data)) {
return idx + 1;
}
}
return false;
};
let dupePage = findDuplicate(finalImageData);
while (dupePage && retryCount < maxRetries) {
if (downloadState.shouldStop) break;
retryCount++;
const wait = Math.min(400 + (retryCount * 100), 2500);
await new Promise(r => setTimeout(r, wait));
await waitForImageLoaded();
finalImageData = await captureCurrentPage(width, height);
dupePage = findDuplicate(finalImageData);
}
downloadState.images.push({
filename: `page_${String(i).padStart(3, '0')}.webp`,
data: finalImageData
});
if (i % 20 === 0) {
const percent = Math.round(i / totalPages * 100);
console.log(`${i}/${totalPages} pages (${percent}%)`);
}
} catch (error) {
console.log(`Page ${i} failed, continuing...`);
}
}
downloadState.isDownloading = false;
downloadState.shouldStop = false;
updateDownloadButton();
const captured = downloadState.images.length;
if (captured > 0) {
console.log(`Done! Got ${captured}/${totalPages} pages`);
await createZipArchive(downloadState.images, bookTitle);
}
}
function initializeDownloader() {
if (window.location.href.includes('viewer-trial') || window.location.href.includes('viewer-epubs-trial')) {
bookData.isPreview = true;
}
const slider = document.querySelector('#pageSliderBar');
const totalPages = getTotalPages();
if (!slider || totalPages === 0) {
setTimeout(initializeDownloader, 1000);
return;
}
if (!bookData.targetWidth || !bookData.targetHeight) {
setTimeout(initializeDownloader, 1000);
return;
}
createDownloadButton();
const configCheckInterval = setInterval(() => {
if (bookData.targetWidth && bookData.targetHeight) {
updateDownloadButton();
clearInterval(configCheckInterval);
}
}, 500);
setTimeout(() => {
clearInterval(configCheckInterval);
}, 30000);
}
})();