Bing Image Creator auto-download

Automatic image downloader for Bing Image Creator.

// ==UserScript==
// @name         Bing Image Creator auto-download
// @namespace    http://tampermonkey.net/
// @version      0.011
// @license      MIT
// @description  Automatic image downloader for Bing Image Creator.
// @match        https://copilot.microsoft.com/images/create*?*autosavetimer=*
// @grant        GM_download
// @require      http://code.jquery.com/jquery-3.7.1.min.js
// ==/UserScript==
//
// I just pasted this together from things found scattered around the internet.  Primarily: https://github.com/Emperorlou/MidJourneyTools
//
// To enable periodic downloading of newly-created images, go to a 'recent creations' page, and add "&autosavetimer=60" to the URL;
// something like: `https://www.bing.com/images/create/-/1234?autosavetimer=60`.
//
// This implementation is designed to be left unattended - periodically reloading itself.  If you click a link it will disable the script,
// unless you remove `?*autosavetimer=*` from `@match` above.

(function() {
    'use strict';

    const filename_prefix = "bing/";
    const downloadables = "img[src$='&pid=ImgGn']";
    const downloadInterval = 600;
    var pollRate = 60000;
    var activeDownloads = 0;
    var loadErrors = 0;
    var lastReload = 0;
    var sourceContent;

    $(document).ready(() => {
        var new_images = document.createElement("div");
        new_images.setAttribute("id", "scanbuffer");
        new_images.setAttribute("hidden", "");
        document.body.append(new_images);
        var dialog = document.createElement("dialog");
        dialog.setAttribute("id", "logmessage");
        dialog.setAttribute("style", "z-index: 100;");
        document.body.append(dialog);
        logger("Automatic image downloader is active.");

        // TODO: check for expired busyImage-* tags and retry them

        // TODO: Try to figure this out dynamically:
        sourceContent = location.href + " #girrc";

        var params = new URLSearchParams(window.location.search);
        pollRate = (params.get('autosavetimer') || 60) * 1000;
        setTimeout(reload, 1000);
        lastReload = Date.now();
        setInterval(function() {
            const timeout = pollRate * 5 + 20 * downloadInterval;
            if (Date.now() - lastReload > timeout) {
                console.log("Reload function seems to have stopped.");
                reload();
            }
        }, pollRate * 1.5);
    });

    // sample: https://tse4.mm.bing.net/th?id=OIG2.AbCdEfGhIjKlMnOp123.&w=100&h=100&c=6&o=5&pid=ImgGn
    function get_img_id(src) {
        var url = new URL(src);
        var id = url.searchParams.get('id') || url.pathname.split('/').pop();
        if (id == null || id.length < 20) {
            console.log("couldn't parse image id from:", src, " got:", id);
        }
        return id;
    }

    // sample: /images/create/kebab-case-prompt/1-0123456789abcedf0123456789abcdef?FORM=GUH2CR
    //         https://copilot.microsoft.com/images/create?q=prompt%20with%20spaces&rt=4&FORM=GENCRE&id=1-0123456789abcedf0123456789abcdef
    function get_page_id(ref) {
        var url = new URL(ref);
        var id = url.searchParams.get('id') || url.searchParams.get('pageId');
        if (id == null) {
            var path = url.pathname.split('/');
            while (path.length && path.shift() != 'create')
                ;
            if (path.length == 2 && path[1].length >= 32) id = path[1];
        }
        if (id == null) {
            console.log("couldn't parse referrer id from:", ref);
        }
        return id;
    }

    // sample: /images/create/kebab-case-prompt/1-0123456789abcedf0123456789abcdef?FORM=GUH2CR
    function get_page_prompt(ref) {
        var url = new URL(ref);
        var q = url.searchParams.get('q');
        if (q == null) {
            var path = url.pathname.split('/');
            while (path.length && path.shift() != 'create')
                ;
            if (path.length == 2 && path[1].length >= 32) q = path[0];
        }
        if (q == null) {
            console.log("couldn't parse referrer prompt from:", ref);
        }
        return q;
    }

    function make_filename(img, src, ref) {
        var src_filename = get_img_id(src);
        var desc = get_page_prompt(ref) || img.getAttribute("alt", "image");
        var pageid = get_page_id(ref) || "page";

        return filename_prefix + src_filename + "_" + pageid + "_" + desc + ".jpg";
    }

    function logger(text) {
        var status = $("#logmessage")[0];
        if (text) {
            status.innerHTML += "<p>" + text + "</p>";
            status.show();
        } else {
            status.innerHTML = "";
            status.close();
        }
    }

    function reload() {
        logger("Rescanning...");
        if (activeDownloads > 0) {
            logger("There are " + activeDownloads + " outstanding.");
        }
        var target = $("#scanbuffer");
        var result = target.load(sourceContent, function(response, status, xhr) {
            var delay = 100;
            if ( status == "error" ) {
                console.log("problem loading content:", response, status, xhr);
                logger(null);
                if (loadErrors > 0) {
                    logger("previous failures: " + loadErrors);
                }
                logger("problem doing rescan: " + status + ": " + response);
                logger("xhr: " + xhr);
                loadErrors++;
            } else {
                loadErrors = 0;
                var allImages = $(target.find(downloadables).get().reverse());
                if (allImages.length < 10) {
                    console.log("Scan buffer doesn't have many images.  Is something wrong?");
                    console.log("all images:", $(target.find("img").get().reverse()));
                }
                for (const img of allImages) {
                    const src = get_download_url(img);
                    if (isUrlReady(src)) {
                        const ref = get_href(img) || "https://www.example.com/";
                        const filename = make_filename(img, src, ref);
                        downloadFile(delay, src, filename, ref);
                        delay += downloadInterval;
                    }
                }
                setTimeout(function() {
                    if (activeDownloads == 0) {
                        logger(null);
                    }
                }, 300);
            }
            setTimeout(reload, pollRate + delay);
        });
        lastReload = Date.now();
    }

    function downloadFile(delay, url, filename, referrer) {
        setUrlBusy(url, filename, referrer);
        logger("downloading: " + url + " as " + filename);
        logger("  referrer: " + referrer + ", in " + delay + "ms");
        setTimeout(function() {
            const download = GM_download({
                url: url,
                name: filename,
                saveAs: false,
                conflictAction: "uniquify",
                onload: function () {
                    setUrlSaved(url);
                },
                onerror: function () {
                    logger("error downloading: " + url);
                    clearUrlBusy(url, "download error");
                },
                ontimeout: function () {
                    logger("timeout downloading: " + url);
                    clearUrlBusy(url, "download timeout");
                }
            });
        }, delay);
    };

    function get_download_url(img) {
      var url = new URL(img.attributes.src.nodeValue);
      url.searchParams.delete("w");
      url.searchParams.delete("h");
      url.searchParams.delete("c");
      url.searchParams.delete("o");
      return url.href;
    }

    function get_href(elem) {
        while (elem) {
            if (elem.hasAttribute('href')) return elem.href;
            elem = elem.parentElement;
        }
        return null;
    }

    function mk_img_id(pfx, src) {
        return pfx + "-" + get_img_id(src);
    }

    function setUrlBusy(src, filename, referrer) {
        const id = mk_img_id("busyImage", src);
//        localStorage.setItem(id, { "time": Date.now(), "src": src, "filename": filename, "referrer": referrer });
        localStorage.setItem(id, Date.now());
        activeDownloads++;
    }

    function clearUrlBusy(src, why) {
        const id = mk_img_id("busyImage", src);
        console.log("removing busy:", id, localStorage.getItem(id), "because:", why);
        localStorage.removeItem(id);
        activeDownloads--;
        if (activeDownloads == 0) {
            setTimeout(function() {
                if (activeDownloads == 0) logger(null);
            }, 1000);
        } else if (activeDownloads < 0) {
            logger("Oops, download count underflow!");
            activeDownloads = 0;
        }
    }

    function setUrlSaved(src) {
        const id = mk_img_id("savedImage", src);
        localStorage.setItem(id, true);
        clearUrlBusy(src, "saved");
    }

    function isUrlSaved(src) {
        const id = mk_img_id("savedImage", src);
        return localStorage.getItem(id) === "true" ? true : false;
    }

    function isUrlReady(src) {
        if (!src || isUrlSaved(src)) return false;

        const id = mk_img_id("busyImage", src);
        const stamp = localStorage.getItem(id);
        if (!stamp) return true;
        if (Date.now() - stamp > 60000) {
            logger("file has been busy too long (lost event?): " + src);
            clearUrlBusy(src, "stale");
            return true;
        }
        console.log("still waiting to finish:", src);
        return false;
    }

})();