Greasy Fork

Greasy Fork is available in English.

Bing Copilot Image auto-downloader

Automatic image downloader for Bing Copilot Image Creator.

当前为 2024-11-14 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Bing Copilot Image auto-downloader
// @namespace    http://tampermonkey.net/
// @version      0.20
// @license      MIT
// @description  Automatic image downloader for Bing Copilot Image Creator.
// @match        https://copilot.microsoft.com/images/create*?*autosavetimer=*
// @grant        GM_download
// @grant        GM_openInTab
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @grant        GM_listValues
// @grant        GM_registerMenuCommand
// @require      http://code.jquery.com/jquery-3.7.1.min.js
// ==/UserScript==
//
// I just pasted this together from things found scattered around the internet.  Starting with: 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 then `autosavetimer=` will
// be removed from the URL and the script will stop working.  Removing `?*autosavetimer=*` from `@match` above _might_ work, but it's not
// well tested at this stage.

var $ = window.jQuery;

(function() {
    'use strict';
    const filenamePrefix = "bing/";
    const downloadables = "img[src$='&pid=ImgGn']";
    const referrerPath = "/images/create/";
    const downloadInterval = 300;
    const sourceContentTag = " #girrc";
    const tagPrefix = "CopilotImageDownloader";
    var pollRate = 60000;
    var activeDownloads = 0;
    var loadErrors = 0;
    var lastReload = 0;
    var lastReauth = Date.now();
    var sourceContent;

    var scanBufferID = null;
    var statusTimeoutID = null;
    var statusBufferID = null;
    var reloadTimerID = null;
    var reauthWindowID = null;

    function jitter(x) {
        return (Math.random() * 0.4 + 0.8) * x;
    }

    $(document).ready(() => {
        var params = new URLSearchParams(window.location.search);
        if (params.get('autosavetimer')) {
            pollRate = params.get('autosavetimer') * 1000;
        }

        scanBufferID = document.createElement("div");
        scanBufferID.setAttribute("id", "scanbuffer");
        scanBufferID.setAttribute("hidden", "");
        document.body.append(scanBufferID);
        statusBufferID = document.createElement("dialog");
        statusBufferID.setAttribute("id", "logmessage");
        statusBufferID.setAttribute("style", "z-index: 100;");
        document.body.append(statusBufferID);
        logger("Automatic image downloader is active.");

        checkMissedDownloads();

        // TODO: Try to figure this out dynamically:
        sourceContent = location.href;

        lastReload = Date.now();
        reloadTimerID = setTimeout(reload, jitter(1000));
        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);
    });

    GM_registerMenuCommand("Recheck missed downloads", function() {
        const count = checkMissedDownloads();
        if (count > 0) {
            logger("rescheduled " + count + " downloads");
        } else {
            logger("nothing to do");
        }
    });

    GM_registerMenuCommand("Clean up expired records", function() {
        var expired = [];
        for (const tag of GM_listValues()) {
            if (tag.startsWith(tagPrefix + "_info_")) {
                var img = new Image();
                Object.assign(img, GM_getValue(tag));
                if (Date.now() - img.stamp > 31 * 24 * 60 * 60 * 1000) {
                    expired.push(img);
                    GM_deleteValue(img.infoTag);
                    GM_deleteValue(img.busyTag);
                }
            }
        }
        if (expired.length > 0) {
            logger("Found " + expired.length + " old files");
            saveImageLog(expired);
        } else {
            logger("nothing to do");
        }
    });

    GM_registerMenuCommand("Download all records", function() {
        var records = [];
        for (const tag of GM_listValues()) {
            if (tag.startsWith(tagPrefix + "_info_")) {
                var img = new Image();
                Object.assign(img, GM_getValue(tag));
                records.push(img);
            }
        }
        if (records.length > 0) {
            saveImageLog(records);
        } else {
            logger("nothing to do");
        }
    });

    function checkMissedDownloads() {
        var delay = 100;
        var count = 0;
        for (const tag of GM_listValues()) {
            if (tag.startsWith(tagPrefix + "_info_")) {
                var img = new Image();
                Object.assign(img, GM_getValue(tag));
                // TODO: separate this bit out into separate function:
                if (img.scheduleDownload(delay)) {
                    console.log("Rescheduled download of", img.filename);
                    delay += jitter(downloadInterval);
                    count++;
                }
            }
        }
        return count;
    }

    function reload() {
        reloadTimerID = null;
        logger("Rescanning...");
        if (activeDownloads > 0) {
            logger("There are " + activeDownloads + " outstanding.");
        }
        const target = $(scanBufferID);
        var result = target.load(sourceContent + sourceContentTag, 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 {
                var allImages = $(target.find(downloadables).get().reverse());
                var allRefs = [];
                if (allImages.length < 10) {
                    console.log("Scan buffer doesn't have many images.  Is something wrong?");
                    if (loadErrors == 0) {
                        console.log("all images:", $(target.find("img").get().reverse()));
                    }
                    loadErrors++;
                } else {
                    closeauthwindow();
                    loadErrors = 0;
                }
                for (const i of allImages) {
                    var img = new Image(i);
                    if (img.ref) allRefs.push(img.ref);
                    if (img.scheduleDownload(delay)) {
                        delay += jitter(downloadInterval);
                    }
                }
                if (allRefs.length > 40) {
                    allRefs.sort();
                    // Pick another base URL from which to scan for updates,
                    // in case the initial one eventually expires.
                    // Taking the middle of a sorted list minimises the risk
                    // of accidentally picking up an outlier that doesn't fit
                    // the pattern.
                    var middleref = allRefs[Math.floor(allRefs.length / 2)];
                    middleref = middleref;
                    if (middleref != sourceContent) {
                        sourceContent = middleref;
                        //console.log("new source URL is:", sourceContent);
                    }
                }
                if (activeDownloads == 0) {
                    logger(null);
                }
            }
            if (loadErrors > 3) {
                reauthenticate();
                loadErrors = 1;
            }
            if (reloadTimerID) clearTimeout(reloadTimerID);
            reloadTimerID = setTimeout(reload, jitter(pollRate) + delay);
        });
        if (result.length < 1) {
            console.log("Some kind of load error?");
            reloadTimerID = setTimeout(reload, jitter(pollRate));
        }
        lastReload = Date.now();
    }

    function closeauthwindow() {
        if (reauthWindowID) {
            reauthWindowID.close();
            reauthWindowID = null;
        }
        return true;
    }

    function reauthenticate() {
        if (reauthWindowID && Date.now() - lastReauth < 600000) {
            console.log("too soon to try reauthenticating");
        } else {
            closeauthwindow();
            // TODO: determine this link automatically
            var reauthLink = "https://copilot.microsoft.com/fd/auth/signin" +
                "?action=interactive&provider=windows_live_id" +
                "&cobrandid=03f1ec5e-1843-43e5-a2f6-e60ab27f6b91" +
                "&noaadredir=1&FORM=GENUS1" +
                "&return_url=" + encodeURIComponent("https://copilot.microsoft.com/images/create/");
            // really want the return_url to be something that lets us close the window, but I don't know how to do that.
            console.log("trying to load", reauthLink);
            reauthWindowID = GM_openInTab(reauthLink, { insert: true });
            if (reauthWindowID) {
                lastReauth = Date.now();
            } else {
                console.log("reauth failed");
            }
        }
    }

    function saveImageLog(images) {
        const json = JSON.stringify(images, function(k, v) {
            if (k == 'stamp') return new Date(v).toJSON();
            return v;
        }, 2);
        const blob = encodeURIComponent(json);
        const data = "data:application/json;charset=UTF-8," + blob;
        GM_download({
            url: data,
            name: "image_downloads.txt",
            saveAs: true,
            conflictAction: "uniquify",
            onload: function() {
                console.log("saved images");
            },
            onerror: function(e) {
                console.log("error saving log:", e);
            },
            ontimeout: function(e) {
                console.log("timeout saving log:", e);
            }
        });
    }

    function logger(text) {
        if (statusTimeoutID) {
            statusBufferID.innerHTML = "";
            clearTimeout(statusTimeoutID);
            statusTimeoutID = null;
        }
        if (text) {
            statusBufferID.innerHTML += "<p>" + text + "</p>";
            statusBufferID.show();
        } else {
            statusTimeoutID = setTimeout(function() {
                statusBufferID.innerHTML = "";
                statusBufferID.close();
                statusTimeoutID = null;
            }, 1000);
        }
    }

    class Image {
        constructor(img) {
            // TODO: accept generic result of GM_getValue()
            if (img) {
                this.url = get_download_url(img);
                this.id = get_img_id(this.url);
                this.ref = get_href(img);
                this.alt = img.getAttribute("alt", null);
                this.stamp = Date.now();
                this.done = false;
                if (!this.isSaved) {
                    // TODO: race condition where successful download might be forgotten.
                    GM_setValue(this.infoTag);
                }
            }
        }
        get filename() {
            const src_filename = this.id;
            const pageid = get_page_id(this.ref) || "page";
            const desc = get_page_prompt(this.ref) || this.alt || "image";

            return filenamePrefix + this.id + "_" + pageid + "_" + desc + ".jpg";
        }
        scheduleDownload(delay) {
            if (!this.setBusy()) return false;
            logger("downloading: " + this.filename);
            setTimeout(function() {
                const download = GM_download({
                    url: this.url,
                    name: this.filename,
                    saveAs: false,
                    conflictAction: "uniquify",
                    onload: function() {
                        this.setSaved();
                    }.bind(this),
                    onerror: function(e) {
                        logger("error downloading: " + this.filename, e);
                        this.clearBusy();
                    }.bind(this),
                    ontimeout: function(e) {
                        logger("timeout downloading: " + this.filename, e);
                        this.clearBusy();
                    }.bind(this)
                });
            }.bind(this), delay);
            return true;
        }
        get infoTag() { return tagPrefix + "_info_" + this.id; }
        get busyTag() { return tagPrefix + "_busy_" + this.id; }
        setBusy() {
            if (!this.isReady) return false;
            GM_setValue(this.busyTag, Date.now());
            activeDownloads++;
            return true;
        }
        clearBusy() {
            GM_deleteValue(this.busyTag);
            activeDownloads--;
            if (activeDownloads == 0) {
                logger(null);
            } else if (activeDownloads < 0) {
                logger("Oops, download count underflow!");
                activeDownloads = 0;
            }
        }
        setSaved() {
            this.done = true;
            GM_setValue(this.infoTag, this);
            this.clearBusy();
        }
        get isSaved() {
            const stored = GM_getValue(this.infoTag) || this;
            this.done = stored.done;
            if (this.done) GM_deleteValue(this.busyTag);
            return this.done;
        }
        get isReady() {
            const timeout = 60*1000;
            if (this.isSaved) return false;
            const stamp = GM_getValue(this.busyTag, null);
            if (!stamp) return true;
            if (Date.now() - stamp > timeout) {
                console.log("file has been busy too long (lost event?): " + this.id);
                GM_deleteValue(this.busyTag);
                return true;
            }
            console.log("download already scheduled:", this.id);
            return false;
        }
    }

    // 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 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;
    }
})();