waifubitches.com Downloader

Download gallery from waifubitches.com

As of 2023-06-12. See the latest version.

// ==UserScript==
// @name         waifubitches.com Downloader
// @description  Download gallery from waifubitches.com
// @namespace    chimichanga
// @author       chimichanga
// @icon         https://waifubitches.com/favicon.ico
// @version      2.0
// @license      MIT
// @match        https://waifubitches.com/gallery/*
// @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      userapi.com
// @run-at       document-idle
// @grant        GM_xmlhttpRequest
// ==/UserScript==

// based 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 zip = new JSZip();
var resultBlob;
var sources = [];
var thumbnails = [];
var completed = 0;
var failed = 0;

$(document).ready(function () {
    sources = $('div.grid .grid-item a').map((_, { href }) => href).get();
    thumbnails = $('div.grid .grid-item img.img-fluid').map((_, { src }) => src).get();
    sources = sources.map((s, i) => [s, thumbnails[i]]);

    if (sources.length == 0) return;

    downBtn = $(`<a class="btn btn-sm btn-warning"><i class="bi bi-download"></i> <span>DOWNLOAD (${sources.length})</span></a>`);
    downStatus = $(downBtn).find('span');

    $('body > div:nth-child(1) > div.pb-2 > center > noindex').append(downBtn);

    zipFile = window.location.href.split('/gallery/')[1] + '+' + $('body > div:nth-child(1) > h1').get(0).innerText + '.zip';

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

function updateState(newState, progress) {
    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);

    let messages = {
        [State.NOT_STARTED]: `DOWNLOAD`,
        [State.DOWNLOADING]: `DOWNLOADING`,
        [State.COMPRESSING]: `COMPRESSING`,
        [State.DONE]: `ZIP READY`,
    }

    $(downStatus).html(messages[state] + (failed > 0 ? ` (${failed} failed)` : '') + (progress ? ` ${progress}` : ''));
}

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;

    Promise.allSettled(
        sources.map(([url, backup]) =>
            fetch(url).catch((cause) => {
                console.log(`can't fetch original image, ${cause}: ${url}`);
                return fetch(backup);
            }).then(({ response, url }) => {
                completed++;
                updateState(State.DOWNLOADING, `${completed}/${sources.length}`);
                let fileName = url.split(/(?:\/(?:a|impg)\/|\?)/)[1].replaceAll('/', '-');
                zip.file(fileName, response);
            }).catch((cause) => {
                console.log(`can't fetch thumbnail image, ${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);
    });
}