Lezhin Comics Downloader

Scrape a full lezhin chapter. Creates a zip with PNGs

// ==UserScript==
// @name         Lezhin Comics Downloader
// @namespace    lezhin-comics-scraper
// @version      1.1.1
// @description  Scrape a full lezhin chapter. Creates a zip with PNGs
// @author       TzurS11
// @match        https://www.lezhinus.com/*
// @match        https://www.lezhin.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=lezhinus.com
// @grant        GM_setValue
// @grant        GM_getValue
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.6.0/jszip.min.js
// @license      MIT
// ==/UserScript==

(function() {
    async function getImageSrcFromElement(imageElementString,index) {
        // Create a temporary div element to hold the image element
        const tempDiv = document.createElement('div');
        tempDiv.innerHTML = imageElementString;

        // Find the image element within the temporary div
        const imageElement = tempDiv.querySelector('img');

        if (imageElement) {
            const imageUrl = imageElement.src;

            try {
                await timeout(500);
                const response = await fetch(imageUrl);
                const blob = await response.blob();

                // Create a temporary anchor element to trigger the download
                const downloadLink = document.createElement('a');
                downloadLink.href = URL.createObjectURL(blob);
                downloadLink.download = `image-${index}.jpg`;
                downloadLink.style.display = 'none';

                return downloadLink;
            } catch (error) {
                return false;
            }
        } else {
            return false
        }
    }



    function timeout(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }


    function downloadImagesAsZip(anchorElements) {
        const zip = new JSZip();

        const promises = Array.from(anchorElements).map((anchor, index) => {
            const url = anchor.href;
            return fetch(url)
                .then(response => response.blob())
                .then(blob => {
                const filename = `image_${index + 1}.jpg`; // Customize the filename as needed
                zip.file(filename, blob);
            });
        });

        Promise.all(promises)
            .then(() => {
            return zip.generateAsync({ type: 'blob' });
        })
            .then(zipBlob => {
            let pathNames = window.location.pathname.split('/');
            const zipFilename = `${pathNames[3]} ${pathNames[4]}.zip`;
            const link = document.createElement('a');
            link.href = URL.createObjectURL(zipBlob);
            link.download = zipFilename;
            link.style.display = 'none';
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
        })
            .catch(error => {
            console.error('Error:', error);
        });
    }

    function waitForImgInDiv(targetDiv) {
        return new Promise(resolve => {

            if (!(targetDiv instanceof HTMLDivElement)) {
                resolve(false);
                return;

            }

            // Check if there's already an <img> element inside the div
            if (targetDiv.querySelector('img')) {
                resolve(true);
                return;
            }

            const observer = new MutationObserver(mutations => {
                mutations.forEach(mutation => {
                    mutation.addedNodes.forEach(node => {
                        if (node instanceof HTMLImageElement && node.tagName === 'IMG') {
                            observer.disconnect(); // Stop observing once an <img> is added
                            resolve(true);
                            return;
                        }
                    });
                });
            });

            const config = { childList: true, subtree: true };
            observer.observe(targetDiv, config);

            let skip = setTimeout(async () => {
                observer.disconnect(); // Disconnect the observer
                resolve(true); // Resolve with true after the timeout
            }, 1000);
        });
    }


    function waitForDivChanges(divId) {
        return new Promise(resolve => {
            const targetDiv = document.getElementById(divId);

            if (!targetDiv) {
                console.error(`Element with ID "${divId}" not found.`);
                resolve(false);
                return;
            }

            const observer = new MutationObserver(mutations => {
                mutations.forEach(mutation => {
                    if (mutation.type === 'attributes' || mutation.type === 'characterData') {
                        resolve(true);
                        observer.disconnect();
                    } else if (mutation.type === 'childList') {
                        for (const addedNode of mutation.addedNodes) {
                            if (addedNode instanceof HTMLElement && addedNode.id === divId) {
                                resolve(true);
                                observer.disconnect();
                                return;
                            }
                        }
                    }
                });
            });

            const config = { attributes: true, childList: true, characterData: true, subtree: true };
            observer.observe(targetDiv, config);
        });
    }

    let pathNames = window.location.pathname.split('/');
    if(pathNames[2] == "comic"){
        waitForDivChanges("scroll-list").then(x=>{
            let button = document.createElement("button");
            button.innerHTML = "Download Chapter";
            button.style.position = "fixed";
            button.style.bottom = 0
            button.style.zIndex = 10000;
            button.style.fontSize = "30px"
            button.style.marginLeft = "10px"
            button.id = "DownloadButton"
            button.onclick=function(){downloadChapter()}
            document.body.appendChild(button);
        })
    }



    let scrollList = document.getElementById("scroll-list");
    if(scrollList != null){
        if(localStorage.getItem("firstTime") == null){
            localStorage.setItem("firstTime", "no")
            alert("To use the Lezhin downloader wait for a button to appear on the bottom left corner and press it. This alert will not appear again.")
        }
    }

    async function downloadChapter(){
        let downloadButton= document.getElementById("DownloadButton");
        downloadButton.innerHTML = "Scraping..."
        downloadButton.disabled = true;
        let elements = []
        if(scrollList == null){
            alert("You are not viewing a chapter");
            return;
        }
        let descendents = scrollList.getElementsByTagName('*');
        let imgLen = descendents.length;
        for (let i = 0; i < descendents.length; ++i) {
            downloadButton.innerHTML = `Scraping... (${i+1}/${imgLen})`
            let element = descendents[i];
            element.scrollIntoView();
            await waitForImgInDiv(element)
            let image = element.innerHTML;
            //await timeout(500);
            let res = await getImageSrcFromElement(image,i);
            if (res != false){
                elements.push(res)
                //console.log(i+1 + "/" + imgLen)
            }
        }
        //console.log("creating zip")
        downloadImagesAsZip(elements)
        downloadButton.innerHTML = "Download Chapter";
        downloadButton.disabled = false;
    }
})();