BookWalker Downloader

Download full and preview books from BookWalker

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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);
    }

})();