e-hentai preload next page

preload, download and cache next page image to display the image faster

// ==UserScript==
// @name        e-hentai preload next page
// @description preload, download and cache next page image to display the image faster
// @author      owowed
// @version     0.0.3
// @namespace   e.owowed.moe
// @license     GPL-3.0-or-later
// @match       *://e-hentai.org/s/*
// @grant       GM_addStyle
// @grant       GM_getValue
// @grant       GM_setValue
// @run-at      document-end
// @copyright   All rights reserved. Licensed under GPL-3.0-or-later. View license at https://spdx.org/licenses/GPL-3.0-or-later.html
// ==/UserScript==

// Debug

function log(...args) {
    console.log("[e-hentai preload next page]", ...args);
}

// Settings

const settingsEvent = new EventTarget();
const getMaxImgCache = () => GM_getValue("max-img-cache", 10);
const getCacheTimeout = () => GM_getValue("cache-timeout", 150_000);

settings();

GM_addStyle(`
    #owo-pfp-owo {
        display: inline-block;
        width: 20px;
        height: 20px;
        background: url(https://files.catbox.moe/gg9tzd.jpg);
        background-size: contain;
    }
    #e-hentai-preload-settings {
        margin: 12px 0;
    }
    #e-hentai-preload-settings .settings-title {
        font-weight: bold;
        font-style: italic;
        font-size: 14pt;
    }
    #e-hentai-preload-settings .entry-title {
        font-size: 10pt;
    }
    #e-hentai-preload-settings .note {
        font-style: italic;
    }
    #e-hentai-preload-settings > *:not(:first-child) {
        padding: 4px 0;
    }
`);

function settings() {
    const settingsPage = Object.assign(document.createElement("div"), {
        id: "e-hentai-preload-settings",
        innerHTML: `
            <div class="settings-title">e-hentai preload next page settings</div>
            <div class="note">userscript made by <span id="owo-pfp-owo"></span>owowed</div>
            <div class="entry">
                <div class="entry-title">Max Image Cache</div>
                <div>How many page you want to preload</div>
                <input type="number" id="max-img-cache" value="${getMaxImgCache()}"/>
            </div>
            <div class="entry">
                <div class="entry-title">Cache Timeout</div>
                <div>When timeout is reached, image cache will be reset and reloaded</div>
                <input type="number" id="cache-timeout" value="${getCacheTimeout()}"/>
            </div>
            <button id="reset-cache">Reset Cache</button>
            <div class="note">settings are automatically saved and applied</div>
        `
    });
    const query = settingsPage.querySelector.bind(settingsPage);

    const maxImgCacheInput = query("#max-img-cache");
    const cacheTimeoutInput = query("#cache-timeout");
    const resetCache = query("#reset-cache");

    maxImgCacheInput.addEventListener("change", () => {
        GM_setValue("max-img-cache", parseInt(maxImgCacheInput.value));
        settingsEvent.dispatchEvent(new Event("change"));
    });

    cacheTimeoutInput.addEventListener("change", () => {
        GM_setValue("cache-timeout", parseInt(cacheTimeoutInput.value));
        settingsEvent.dispatchEvent(new Event("change"));
    });

    resetCache.addEventListener("click", () => {
        settingsEvent.dispatchEvent(new Event("change"));
    });

    document.body.appendChild(settingsPage);
}

// Main

const linkPreloadList = [];

!async function () {
    let preloadImageGenerator = await getPreloadImageGenerator();
    let lastPage = history.state?.page ?? 0;

    const resetPreloadImageGenerator = async () => preloadImageGenerator = await getPreloadImageGenerator();

    observePageChange(async () => {
        if (lastPage > history.state.page) {
            await resetPreloadImageGenerator();
        }

        preloadImageGenerator.next();

        lastPage = history.state.page;
    });

    let lastPageVisibleTime = Date.now();
    const maxPageVisibleTimeout = 60_000;

    document.addEventListener("visibilitychange", () => {
        if (!document.hidden && (Date.now() - lastPageVisibleTime) > maxPageVisibleTimeout) {
            lastPageVisibleTime = Date.now();
            resetPreloadImageGenerator();
        }
    });

    setInterval(() => resetPreloadImageGenerator(), getCacheTimeout());

    settingsEvent.addEventListener("change", () => {
        resetPreloadImageGenerator();
    });
}();

async function getPreloadImageGenerator({ maxImgCache = getMaxImgCache() } = {}) {
    linkPreloadList.forEach(i => i.remove());
    linkPreloadList.length = 0;

    const preloadImageGenerator = preloadImgFromAnchor("#next");

    for (let iteration = 0; iteration < maxImgCache; iteration++) {
        await preloadImageGenerator.next();
    }

    return preloadImageGenerator;
}

async function* preloadImgFromAnchor(selector, anchor = document.querySelector(selector)) {
    const html = await fetch(anchor.href).then(i => i.text()); 

    const domParser = new DOMParser();
    const parsedDom = domParser.parseFromString(html, "text/html");

    const nextPageImg = parsedDom.querySelector("#img");
    const linkPreload = Object.assign(document.createElement("link"), {
        rel: "preload",
        href: nextPageImg.src,
        as: "image"
    });

    log("preloading page:",
        parseInt(parsedDom.querySelector("#prev + div > span:first-child").textContent),
        nextPageImg.src);

    document.head.appendChild(linkPreload);

    if (linkPreloadList.length == getMaxImgCache()) {
        linkPreloadList.shift().remove();
    }

    linkPreloadList.push(linkPreload);

    yield linkPreload;
    yield* preloadImgFromAnchor(selector, parsedDom.querySelector(selector));
}

function awaitPageChange() {
    return new Promise(resolve => {
        observePageChange(() => resolve(), { once: true });
    });
}

function observePageChange(callback, { once } = {}) {
    const i3 = document.querySelector("#i3");
    const mut = new MutationObserver(() => {
        if (once) mut.disconnect();
        callback();
    });
    
    mut.observe(i3, {
        childList: true
    });

    return mut;
}