mangatoto.com, bato.to Downloader

Download chapter from mangatoto.com, bato.to. Based on waifubitches.com downloader

// ==UserScript==
// @name         mangatoto.com, bato.to Downloader
// @description  Download chapter from mangatoto.com, bato.to. Based on waifubitches.com downloader
// @namespace    chimichanga
// @author       chimichanga
// @icon         https://bato.to/amsta/img/batoto/favicon.ico
// @version      2.0
// @license      MIT
// @match        https://bato.to/chapter/*
// @match        https://bato.to/title/*
// @match        https://mangatoto.com/chapter/*
// @match        https://mangatoto.com/title/*
// @require      https://ajax.aspnetcdn.com/ajax/jQuery/jquery-3.7.0.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.9.1/jszip.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.4/FileSaver.min.js
// @noframes
// @connect      self
// @connect      batcg.org
// @run-at       document-idle
// @grant        GM_xmlhttpRequest
// ==/UserScript==

// based originally on the 8muses.com downloader script
var downBtn;
var downStatus;
var zipFile;
const State = { NOT_STARTED: Symbol('not_started'), DOWNLOADING: Symbol('downloading'), COMPRESSING: Symbol('compressing'), DONE: Symbol('done') };
var state = State.NOT_STARTED;
var detectSourcesInterval;

var zip = new JSZip();
var resultBlob;
var sources = [];
var thumbnails = [];
var completed = 0;
var failed = 0;

var V3 = $('astro-island').length; // page version

$(document).ready(function () {
    console.log(V3);
    downBtn = $(`<a class="btn btn-outline ${V3 ? `btn-sm w-full` : `btn-md col-24`} btn-warning rounded "><i ${V3 ? 'style="font-family:FontAwesome5_Solid;font-weight:normal;font-style:normal"' : 'class="fas fa-fw fa-download"'}>${V3 ? '':''}</i> <span></span></a>`)
    let btnWrapper = $(`<div class="mt-3"></div>`).append(downBtn);
    downStatus = $(downBtn).find('span');

    $('div#container div.row').append(btnWrapper);
    $('#app-wrapper > div:nth-child(3) > div').append(btnWrapper);

    zipFile = document.title + '.zip';
    if (V3) zipFile = zipFile.replace(" - Read Free Manga Online at Bato.To", "");

    detectSources();
    detectSourcesInterval = setInterval(detectSources, 1000);

    $(downBtn).click(download);
});

function detectSources() {
    let detectedSources = $(V3 ? '[name=image-item] img' : 'img.page-img').map((_, { src }) => src).get();
    if(detectedSources.length > sources.length) {
        sources = detectedSources;
        updateState(State.NOT_STARTED);
    } else if (state == State.NOT_STARTED) {
        updateState(State.NOT_STARTED, 'detecting...');
    }
}

function updateState(newState, status) {
    state = newState;
    $(downBtn).toggleClass('btn-success', state == State.DONE);
    $(downBtn).toggleClass('btn-warning', state == State.NOT_STARTED || state == State.COMPRESSING || state == State.DOWNLOADING);
    $(downBtn).toggleClass('btn-danger', failed > 0);

    function getMsg() {
        let failedMsg = failed > 0 ? ` (${failed} failed)` : '';
        let statusMsg = status ? ` ${status}` : '';

        switch(state){
            case State.NOT_STARTED:
                return `DOWNLOAD (${sources.length || status})`;
            case State.DOWNLOADING:
                return `DOWNLOADING${statusMsg}${failedMsg}`;
            case State.COMPRESSING:
                return `COMPRESSING${statusMsg}${failedMsg}`;
            case State.DONE:
                return `ZIP READY`;
        }
    }

    $(downStatus).html(getMsg());
}

function download() {
    if (state == State.DONE && failed == 0) {
        saveZip();
        return;
    }

    if (state == State.DOWNLOADING || state == State.COMPRESSING)
        return;

    updateState(State.DOWNLOADING);

    completed = 0;
    failed = 0;

    let padLength = Math.floor(Math.log10(sources.length))+1;

    Promise.allSettled(
        sources.map((url, i) =>
            fetch(url).then(({ response, url }) => {
                completed++;
                updateState(State.DOWNLOADING, `${completed}/${sources.length}`);
                let fileName = `${i+1}`.padStart(padLength, 0) + `.webp`;
                zip.file(fileName, response);
            }).catch((cause) => {
                console.log(`can't fetch image ${i}, ${cause}: ${url}`);
                failed++;
            }))).then(saveZip);
}

function fetch(url) {
    return new Promise((resolve, reject) => GM_xmlhttpRequest({
        method: 'GET',
        url: url,
        responseType: 'arraybuffer',
        onload: ({ response, status }) => status == 200 ? resolve({ response: response, url: url }) : reject('missing'),
        onerror: () => reject('error'),
        onabort: () => reject('abort'),
        ontimeout: () => reject('timeout'),
    }));
}

function saveZip() {
    if (state == State.DONE) {
        saveAs(resultBlob, zipFile);
        return;
    }

    zip.generateAsync(
        { type: 'blob' },
        ({ percent }) => updateState(State.COMPRESSING, `${percent.toFixed(2)}%`)
    ).then(function (blob) {
        updateState(State.DONE, `${completed}/${sources.length}`);
        resultBlob = blob;
        saveAs(resultBlob, zipFile);
    });
}