Twitch Auto Drops Claim

Twitch 自動領取 (掉寶/Drops) , 窗口標籤顯示進度 , 直播結束時還沒領完 , 會自動尋找任意掉寶直播 , 並開啟後繼續掛機 , 代碼自訂義設置

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name                Twitch Auto Drops Claim
// @name:zh-TW          Twitch 自動領取掉寶
// @name:zh-CN          Twitch 自动领取掉宝
// @name:en             Twitch Auto Drops Claim
// @name:ja             Twitch 自動ドロップ受け取り
// @name:ko             Twitch 자동 드롭 수령
// @name:ru             Twitch Автоматическое получение дропов
// @version             2025.10.12-Beta
// @author              Canaan HS
// @description         Twitch 自動領取 (掉寶/Drops) , 窗口標籤顯示進度 , 直播結束時還沒領完 , 會自動尋找任意掉寶直播 , 並開啟後繼續掛機 , 代碼自訂義設置
// @description:zh-TW   Twitch 自動領取 (掉寶/Drops) , 窗口標籤顯示進度 , 直播結束時還沒領完 , 會自動尋找任意掉寶直播 , 並開啟後繼續掛機 , 代碼自訂義設置
// @description:zh-CN   Twitch 自动领取 (掉宝/Drops) , 窗口标签显示进度 , 直播结束时还没领完 , 会自动寻找任意掉宝直播 , 并开启后继续挂机 , 代码自定义设置
// @description:en      Automatically claim Twitch Drops, display progress in the tab, and if not finished when the stream ends, it will automatically find another Drops-enabled stream and continue farming. Customizable settings in the code.
// @description:ja      Twitch のドロップを自動的に受け取り、タブに進捗狀況を表示し、ストリーム終了時にまだ受け取っていない場合、自動的に別のドロップ有効なストリームを検索し、収穫を続けます。コードでのカスタマイズ可能な設定
// @description:ko      Twitch 드롭을 자동으로 받아오고 탭에 진행 상황을 표시하며, 스트림이 종료되었을 때 아직 완료되지 않았다면 자동으로 다른 드롭 활성 스트림을 찾아 계속 수집합니다. 코드에서 사용자 정의 설정 가능합니다
// @description:ru      Автоматически получает дропы Twitch, отображает прогресс во вкладке, и если дропы не завершены к концу трансляции, автоматически находит другую трансляцию с активированными дропами и продолжает фарминг. Настраиваемые параметры в коде.

// @match        https://www.twitch.tv/*
// @supportURL   https://github.com/Canaan-HS/MonkeyScript/issues
// @icon         https://cdn-icons-png.flaticon.com/512/8214/8214044.png

// @license      MPL-2.0
// @namespace    https://greasyfork.org/users/989635

// @grant        GM_setValue
// @grant        GM_getValue
// @grant        window.close
// @grant        GM_deleteValue
// @grant        window.onurlchange
// @grant        GM_registerMenuCommand

// @run-at       document-body
// ==/UserScript==

(() => {
    const Backup = GM_getValue("Config", {});
    const Config = {
        Dev: false, // 開發打印

        RestartLive: true, // 使用重啟直播
        EndAutoClose: true, // 全部進度完成後自動關閉
        TryStayActive: true, // 嘗試讓頁面保持活躍
        RestartLiveMute: true, // 重啟的直播靜音
        RestartLowQuality: false, // 重啟直播最低畫質

        UpdateDisplay: true, // 於標題展示更新倒數
        ClearExpiration: true, // 清除過期的掉寶進度
        ProgressDisplay: true, // 於標題展示掉寶進度

        UpdateInterval: 120, // (seconds) 更新進度狀態的間隔
        JudgmentInterval: 6, // (Minute) 經過多長時間進度無增加, 就重啟直播 [設置太短會可能誤檢測]

        FindTag: ["drops", "啟用掉寶", "启用掉宝", "드롭활성화됨"], // 查找直播標籤, 只要有包含該字串即可
        ...Backup
    };
    const supportPage = "https://www.twitch.tv/drops/inventory";
    const supportCheck = (url = location.href) => url === supportPage;
    class Detection {
        constructor() {
            this.progressParse = progress => progress.sort((a, b) => b - a).find(number => number < 100);
            this.getTime = () => {
                const time = this.currentTime;
                const year = time.getFullYear();
                const month = `${time.getMonth() + 1}`.padStart(2, "0");
                const date = `${time.getDate()}`.padStart(2, "0");
                const hour = `${time.getHours()}`.padStart(2, "0");
                const minute = `${time.getMinutes()}`.padStart(2, "0");
                const second = `${time.getSeconds()}`.padStart(2, "0");
                return `${year}-${month}-${date} ${hour}:${minute}:${second}`;
            };
            this.storage = (key, value = null) => value == null ? (value = sessionStorage.getItem(key),
                value != null ? JSON.parse(value) : value) : sessionStorage.setItem(key, JSON.stringify(value));
            this.adapter = {
                _convertPM: time => time.replace(/(\d{1,2}):(\d{2})/, (_, hours, minutes) => `${+hours + 12}:${minutes}`),
                "en-US": (timeStamp, currentYear) => new Date(`${timeStamp} ${currentYear}`),
                "en-GB": (timeStamp, currentYear) => new Date(`${timeStamp} ${currentYear}`),
                "es-ES": (timeStamp, currentYear) => new Date(`${timeStamp} ${currentYear}`),
                "fr-FR": (timeStamp, currentYear) => new Date(`${timeStamp} ${currentYear}`),
                "pt-PT": (timeStamp, currentYear) => {
                    const convert = timeStamp.replace(/(\d{1,2})\/(\d{1,2})/, (_, day, month) => `${month}/${day}`);
                    return new Date(`${convert} ${currentYear}`);
                },
                "pt-BR": (timeStamp, currentYear) => {
                    const ISO = { jan: "Jan", fev: "Feb", mar: "Mar", abr: "Apr", mai: "May", jun: "Jun", jul: "Jul", ago: "Aug", set: "Sep", out: "Oct", nov: "Nov", dez: "Dec", dom: "Sun", seg: "Mon", ter: "Tue", qua: "Wed", qui: "Thu", sex: "Fri", "sáb": "Sat" };
                    const convert = timeStamp.replace(/de/g, "").replace(/(jan|fev|mar|abr|mai|jun|jul|ago|set|out|nov|dez|dom|seg|ter|qua|qui|sex|sáb)/gi, match => ISO[match.toLowerCase()]);
                    return new Date(`${convert} ${currentYear}`);
                },
                "ru-RU": (timeStamp, currentYear) => {
                    const ISO = { "янв": "Jan", "фев": "Feb", "мар": "Mar", "апр": "Apr", "май": "May", "июн": "Jun", "июл": "Jul", "авг": "Aug", "сен": "Sep", "окт": "Oct", "ноя": "Nov", "дек": "Dec", "пн": "Mon", "вт": "Tue", "ср": "Wed", "чт": "Thu", "пт": "Fri", "сб": "Sat", "вс": "Sun" };
                    const convert = timeStamp.replace(/(янв|фев|мар|апр|май|июн|июл|авг|сен|окт|ноя|дек|пн|вт|ср|чт|пт|сб|вс)/gi, match => ISO[match.toLowerCase()]);
                    return new Date(`${convert} ${currentYear}`);
                },
                "de-DE": (timeStamp, currentYear) => {
                    const ISO = {
                        jan: "Jan", feb: "Feb", "mär": "Mar", apr: "Apr", mai: "May", jun: "Jun", jul: "Jul", aug: "Aug", sep: "Sep", okt: "Oct", nov: "Nov", dez: "Dec", mo: "Mon", di: "Tue", mi: "Wed", do: "Thu", fr: "Fri", sa: "Sat", so: "Sun"
                    };
                    const convert = timeStamp.replace(/(jan|feb|mär|apr|mai|jun|jul|aug|sep|okt|nov|dez|mo|di|mi|do|fr|sa|so)/gi, match => ISO[match.toLowerCase()]);
                    return new Date(`${convert} ${currentYear}`);
                },
                "it-IT": (timeStamp, currentYear) => {
                    const ISO = { gen: "Jan", feb: "Feb", mar: "Mar", apr: "Apr", mag: "May", giu: "Jun", lug: "Jul", ago: "Aug", set: "Sep", ott: "Oct", nov: "Nov", dic: "Dec", dom: "Sun", lun: "Mon", mar: "Tue", mer: "Wed", gio: "Thu", ven: "Fri", sab: "Sat" };
                    const convert = timeStamp.replace(/(gen|feb|mar|apr|mag|giu|lug|ago|set|ott|nov|dic|dom|lun|mar|mer|gio|ven|sab)/gi, match => ISO[match.toLowerCase()]);
                    return new Date(`${convert} ${currentYear}`);
                },
                "tr-TR": (timeStamp, currentYear) => {
                    const ISO = { oca: "Jan", "şub": "Feb", mar: "Mar", nis: "Apr", may: "May", haz: "Jun", tem: "Jul", "ağu": "Aug", eyl: "Sep", eki: "Oct", kas: "Nov", ara: "Dec", paz: "Sun", pts: "Mon", sal: "Tue", "çar": "Wed", per: "Thu", cum: "Fri", cmt: "Sat" };
                    const convert = timeStamp.replace(/(oca|şub|mar|nis|may|haz|tem|ağu|eyl|eki|kas|ara|paz|pts|sal|çar|per|cum|cmt)/gi, match => ISO[match.toLowerCase()]);
                    const match = convert.match(/(\d{1,2}) ([a-z]+) ([a-z]+) (\d{1,2}:\d{1,2}) (GMT[+-]\d{1,2})/i);
                    return new Date(`${match[3]} ${match[1]} ${match[2]} ${match[4]} ${match[5]} ${currentYear}`);
                },
                "es-MX": (timeStamp, currentYear) => {
                    const match = timeStamp.match(/^([a-zñáéíóúü]+) (\d{1,2}) de ([a-zñáéíóúü]+), (\d{1,2}:\d{1,2}) (?:[ap]\.m\.) (GMT[+-]\d{1,2})/i);
                    const time = timeStamp.includes("p.m") ? this.adapter._convertPM(match[4]) : match[4];
                    return new Date(`${match[1]}, ${match[2]} ${match[3]}, ${time} ${match[5]} ${currentYear}`);
                },
                "ja-JP": (timeStamp, currentYear) => {
                    const match = timeStamp.match(/(\d{1,2})\D+(\d{1,2})\D+(\d{1,2}:\d{1,2}) (GMT[+-]\d{1,2})/);
                    return new Date(`${currentYear}-${match[1]}-${match[2]} ${match[3]}:00 ${match[4]}`);
                },
                "ko-KR": (timeStamp, currentYear) => {
                    const match = timeStamp.match(/(\d{1,2})\D+(\d{1,2})\D+(\d{1,2}:\d{1,2}) (GMT[+-]\d{1,2})/);
                    const time = timeStamp.includes("오후") ? this.adapter._convertPM(match[3]) : match[3];
                    return new Date(`${currentYear}-${match[1]}-${match[2]} ${time}:00 ${match[4]}`);
                },
                "zh-TW": (timeStamp, currentYear) => {
                    const match = timeStamp.match(/(\d{1,2})\D+(\d{1,2})\D+\D+(\d{1,2}:\d{1,2}) \[(GMT[+-]\d{1,2})\]/);
                    const time = timeStamp.includes("下午") ? this.adapter._convertPM(match[3]) : match[3];
                    return new Date(`${currentYear}-${match[1]}-${match[2]} ${time}:00 ${match[4]}`);
                },
                "zh-CN": (timeStamp, currentYear) => {
                    const match = timeStamp.match(/(\d{1,2})\D+(\d{1,2})\D+\D+(GMT[+-]\d{1,2}) (\d{1,2}:\d{1,2})/);
                    return new Date(`${currentYear}-${match[1]}-${match[2]} ${match[4]}:00 ${match[3]}`);
                }
            };
            this.pageRefresh = async (updateDisplay, interval, finishCall) => {
                let timer;
                const start = Date.now();
                const refresh = setInterval(() => {
                    if (!supportCheck()) {
                        clearInterval(refresh);
                        clearTimeout(timer);
                        this.titleObserver?.disconnect();
                        finishCall?.();
                    } else if (updateDisplay) {
                        const elapsed = Math.floor((Date.now() - start) / 1e3);
                        const remaining = interval - elapsed;
                        if (remaining >= 0) {
                            document.title = `【 ${remaining}s 】 ${this.progressStr}`;
                        }
                    }
                }, 1e3);
                timer = setTimeout(() => {
                    clearInterval(refresh);
                    finishCall?.();
                }, (interval + 1) * 1e3);
            };
            this.showProgress = () => {
                this.titleObserver = new MutationObserver(() => {
                    document.title !== this.progressStr && (document.title = this.progressStr);
                });
                this.titleObserver.observe(document.querySelector("title"), {
                    childList: 1,
                    subtree: 0
                });
                document.title = this.progressStr;
            };
            this.expiredCleanup = (element, adapter, timestamp, callback) => {
                const targetTime = adapter?.(timestamp, this.currentTime.getFullYear()) ?? this.currentTime;
                this.currentTime > targetTime ? this.Config.ClearExpiration && element.remove() : callback(element);
            };
            this.progressStr;
            this.titleObserver;
            this.Config = {
                ...Config,
                EndLine: "p a[href='/drops/campaigns']",
                Campaigns: "a[href='/drops/campaigns']",
                Inventory: "a[href='/drops/inventory']",
                allProgress: ".inventory-max-width > div:not(:first-child)",
                ProgressBar: "[role='progressbar'] + div span",
                ActivityTime: ".inventory-campaign-info span:last-child"
            };
        }
        get currentTime() {
            return new Date();
        }
        static async run() {
            regMenu();
            const self = new Detection();
            const config = self.Config;
            const updateDisplay = config.UpdateDisplay;
            let campaigns, inventory, adapter;
            let taskCount, currentProgress, inProgressIndex, progressInfo;
            const initData = () => {
                self.progressStr = "Twitch";
                taskCount = 0, currentProgress = 0, inProgressIndex = 0;
                progressInfo = {};
            };
            initData();
            const process = (token = 10) => {
                campaigns ??= devTrace("Campaigns", document.querySelector(config.Campaigns));
                inventory ??= devTrace("Inventory", document.querySelector(config.Inventory));
                const allProgress = devTrace("AllProgress", document.querySelectorAll(config.allProgress));
                if (allProgress?.length > 0) {
                    let activityTime, progressBar;
                    adapter ??= self.adapter[document.documentElement.lang];
                    allProgress.forEach(data => {
                        activityTime = devTrace("ActivityTime", data.querySelector(config.ActivityTime));
                        self.expiredCleanup(data, adapter, activityTime?.textContent, notExpired => {
                            notExpired.querySelectorAll("button").forEach(draw => {
                                draw.click();
                            });
                            progressBar = devTrace("ProgressBar", notExpired.querySelectorAll(config.ProgressBar));
                            progressInfo[taskCount++] = [...progressBar].map(progress => +progress.textContent);
                        });
                    });
                    const oldTask = self.storage("Task") ?? {};
                    const newTask = Object.fromEntries(Object.entries(progressInfo).map(([key, value]) => [key, self.progressParse(value)]));
                    let taskIndex, newProgress;
                    const taskEntries = Object.entries(newTask);
                    for ([taskIndex, newProgress] of taskEntries) {
                        const oldProgress = oldTask[taskIndex] || newProgress;
                        if (newProgress !== oldProgress) {
                            inProgressIndex = taskIndex;
                            currentProgress = newProgress;
                            break;
                        }
                    }
                    if (typeof inProgressIndex === "number" && taskEntries.length > 1) {
                        [taskIndex, newProgress] = taskEntries.reduce((max, cur) => cur[1] > max[1] ? cur : max);
                        inProgressIndex = taskIndex;
                        currentProgress = newProgress;
                    }
                    self.storage("Task", newTask);
                }
                if (currentProgress > 0) {
                    if (config.ProgressDisplay) {
                        self.progressStr = `${currentProgress}%`;
                        !updateDisplay && self.showProgress();
                    }
                } else if (token > 0 && supportCheck()) {
                    setTimeout(() => {
                        process(token - 1);
                    }, 2e3);
                    return;
                }
                const [record, timestamp] = self.storage("Record") ?? [0, self.getTime()];
                const diffInterval = ~~((self.currentTime - new Date(timestamp)) / (1e3 * 60));
                const notHasToken = token === 0;
                const hasProgress = currentProgress > 0;
                if (diffInterval >= config.JudgmentInterval && hasProgress && currentProgress === record) {
                    config.RestartLive && restartLive.run(inProgressIndex);
                    self.storage("Record", [currentProgress, self.getTime()]);
                } else if (hasProgress && currentProgress !== record) {
                    self.storage("Record", [currentProgress, self.getTime()]);
                } else if (config.EndAutoClose && notHasToken && !hasProgress && record !== 0) {
                    window.open("", "NewWindow", "top=0,left=0,width=1,height=1").close();
                    window.close();
                } else if (notHasToken && supportCheck()) {
                    location.assign(supportPage);
                }
            };
            const waitLoad = (select, interval = 500, timeout = 15e3) => {
                let elapsed = 0;
                return new Promise((resolve, reject) => {
                    const query = () => {
                        if (document.querySelector(select)) resolve(); else {
                            elapsed += interval;
                            elapsed >= timeout ? supportCheck() && location.assign(supportPage) : setTimeout(query, interval);
                        }
                    };
                    setTimeout(query, interval);
                });
            };
            const monitor = () => {
                self.pageRefresh(updateDisplay, config.UpdateInterval, async () => {
                    initData();
                    if (!supportCheck()) {
                        waitSupport();
                        return;
                    }
                    campaigns?.click();
                    await waitLoad(".accordion-header");
                    inventory?.click();
                    await waitLoad(config.EndLine);
                    process();
                    monitor();
                });
            };
            waitEl(document, config.EndLine, () => {
                process();
                monitor();
                config.TryStayActive && stayActive(document);
            }, {
                timeoutResult: true
            });
        }
    }
    class RestartLive {
        constructor() {
            this.liveMute = async _document => {
                waitEl(_document, "video", video => {
                    const silentInterval = setInterval(() => {
                        video.muted = 1;
                    }, 500);
                    setTimeout(() => {
                        clearInterval(silentInterval);
                    }, 15e3);
                });
            };
            this.liveLowQuality = async _document => {
                const dom = _document;
                waitEl(dom, "[data-a-target='player-settings-button']", menu => {
                    menu.click();
                    waitEl(dom, "[data-a-target='player-settings-menu-item-quality']", quality => {
                        quality.click();
                        waitEl(dom, "[data-a-target='player-settings-menu']", settings => {
                            settings.lastElementChild.click();
                            setTimeout(() => menu.click(), 800);
                        });
                    });
                });
            };
            this.waitDocument = async (_window, checkFu) => {
                let _document, animationFrame;
                return new Promise((resolve, reject) => {
                    let observe;
                    _window.onload = () => {
                        cancelAnimationFrame(animationFrame);
                        _document = _window.document;
                        observe = new MutationObserver($throttle(() => {
                            if (checkFu(_document)) {
                                observe.disconnect();
                                resolve(_document);
                            }
                        }, 300));
                        observe.observe(_document, {
                            subtree: 1,
                            childList: 1,
                            characterData: 1
                        });
                    };
                    const query = () => {
                        _document = _window.document;
                        if (_document && checkFu(_document)) {
                            cancelAnimationFrame(animationFrame);
                            observe?.disconnect();
                            resolve(_document);
                        } else {
                            animationFrame = requestAnimationFrame(query);
                        }
                    };
                    animationFrame = requestAnimationFrame(query);
                });
            };
            this.Config = {
                ...Config,
                Offline: ".home-carousel-info strong",
                Online: "[data-a-target='animated-channel-viewers-count']",
                Channel: ".preview-card-channel-link",
                Container: "#directory-game-main-content",
                ContainerHandle: ".scrollable-area",
                ActivityLink1: "[data-test-selector='DropsCampaignInProgressDescription-hint-text-parent']",
                ActivityLink2: "[data-test-selector='DropsCampaignInProgressDescription-no-channels-hint-text']"
            };
        }
        async run(maxIndex) {
            window.open("", "NewWindow", "top=0,left=0,width=1,height=1").close();
            const self = this;
            const config = self.Config;
            let newWindow;
            let channel = document.querySelectorAll(config.ActivityLink2)[maxIndex];
            if (channel) {
                newWindow = window.open(channel.href, "NewWindow");
                dirSearch(newWindow);
            } else {
                channel = document.querySelectorAll(config.ActivityLink1)[maxIndex];
                const openLink = [...channel.querySelectorAll("a")].reverse();
                findLive(0);
                async function findLive(index) {
                    if (openLink.length - 1 < index) return 0;
                    const href = openLink[index].href;
                    newWindow = !newWindow ? window.open(href, "NewWindow") : (newWindow.location.assign(href),
                        newWindow);
                    if (href.includes("directory")) {
                        dirSearch(newWindow);
                    } else {
                        const _document = await self.waitDocument(newWindow, document => document.querySelector(config.Offline) || document.querySelector(config.Online));
                        if (devTrace("Offline", _document.querySelector(config.Offline))) {
                            findLive(index + 1);
                        } else if (devTrace("Online", _document.querySelector(config.Online))) {
                            config.RestartLiveMute && self.liveMute(_document);
                            config.TryStayActive && stayActive(_document);
                            config.RestartLowQuality && self.liveLowQuality(_document);
                        }
                    }
                }
            }
            const pattern = config.FindTag.map(s => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
            const tagRegex = new RegExp(pattern, "i");
            async function dirSearch(newWindow) {
                const _document = await self.waitDocument(newWindow, document => document.querySelector(config.Container));
                let scrollHandle;
                const container = devTrace("Container", _document.querySelector(config.Container));
                const startFind = () => {
                    try {
                        scrollHandle ??= devTrace("ContainerHandle", container.closest(config.ContainerHandle));
                        const channel = devTrace("Channel", container.querySelectorAll(`${config.Channel}:not([Drops-Processed])`));
                        const liveLink = [...channel].find(channel => {
                            channel.setAttribute("Drops-Processed", true);
                            const haveDrops = [...channel.nextElementSibling?.querySelectorAll("span")].some(span => tagRegex.test(span.textContent));
                            return haveDrops ? channel : null;
                        });
                        if (liveLink) {
                            liveLink.click();
                            liveLink.click();
                            config.RestartLiveMute && self.liveMute(_document);
                            config.TryStayActive && stayActive(_document);
                            config.RestartLowQuality && self.liveLowQuality(_document);
                        } else if (scrollHandle) {
                            scrollHandle.scrollTo({
                                top: scrollHandle.scrollHeight
                            });
                            setTimeout(startFind, 1500);
                        }
                    } catch {
                        setTimeout(startFind, 1500);
                    }
                };
                startFind();
            }
        }
    }
    async function stayActive(_document) {
        const id = "Stay-Active";
        const head = _document.head;
        if (head.getElementById(id)) return;
        const script = document.createElement("script");
        script.id = id;
        script.textContent = `
            function WorkerCreation(code) {
                const blob = new Blob([code], {type: "application/javascript"});
                return new Worker(URL.createObjectURL(blob));
            }

            const Active = WorkerCreation(\`
                onmessage = function(e) {
                    setTimeout(() => {
                        const { url } = e.data;
                        fetch(url);
                        postMessage({ url });
                    }, 1e4);
                }
            \`);

            Active.postMessage({ url: location.href });
            Active.onmessage = (e) => {
                const { url } = e.data;
                document.querySelector("video")?.play();
                Active.postMessage({ url });
            };

            let emptyAudio = new Audio("data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEA...");
            emptyAudio.loop = true;
            emptyAudio.muted = true;

            // 後台播放 / 前台暫停
            const visHandler = (isHidden) => {
                if (typeof isHidden !== 'boolean') isHidden = document.hidden;
                if (isHidden) {
                    emptyAudio.play().catch(()=>{});
                } else {
                    emptyAudio.pause();
                }
            };

            if (typeof document.hidden !== "undefined") {
                document.addEventListener("visibilitychange", () => visHandler());
            } else {
                window.addEventListener("focus", () => visHandler(false));
                window.addEventListener("blur", () => visHandler(true));
            }

            visHandler();
        `;
        head.append(script);
    }
    function $throttle(func, delay) {
        let lastTime = 0;
        return (...args) => {
            const now = Date.now();
            if (now - lastTime >= delay) {
                lastTime = now;
                func(...args);
            }
        };
    }
    let cleaner = null;
    let traceRecord = {};
    function getCompositeKey(elements) {
        return Array.from(elements).map(el => {
            if (!(el instanceof Element)) return "";
            return el.tagName + (el.id || "id") + (el.className || "class");
        }).join("|");
    }
    function devTrace(tag, element) {
        if (!Config.Dev) return element;
        const record = traceRecord[tag];
        const isNodeList = element instanceof NodeList;
        const recordKey = isNodeList ? getCompositeKey(element) : element;
        if (record && record.has(recordKey)) return element;
        traceRecord[tag] = new Map().set(recordKey, true);
        clearTimeout(cleaner);
        cleaner = setTimeout(() => {
            traceRecord = {};
        }, 1e4);
        const isEmpty = !element || isNodeList && element.length === 0;
        const baseStyle = "padding: 2px 6px; border-radius: 3px; font-weight: bold; margin: 0 2px;";
        const tagStyle = `${baseStyle} background: linear-gradient(45deg, #667eea 0%, #764ba2 100%); color: white; text-shadow: 1px 1px 2px rgba(0,0,0,0.3);`;
        let statusStyle, statusIcon, statusText;
        if (isEmpty) {
            statusStyle = `${baseStyle} background: linear-gradient(45deg, #e74c3c 0%, #c0392b 100%); color: white; text-shadow: 1px 1px 2px rgba(0,0,0,0.5);`;
            statusIcon = "❌";
            statusText = "NOT FOUND";
        } else {
            statusStyle = `${baseStyle} background: linear-gradient(45deg, #2ecc71 0%, #27ae60 100%); color: white; text-shadow: 1px 1px 2px rgba(0,0,0,0.3);`;
            statusIcon = "✅";
            statusText = "FOUND";
        }
        console.groupCollapsed(`%c🔍 ${tag} %c${statusIcon} ${statusText}`, tagStyle, statusStyle);
        if (isEmpty) {
            console.log(`%c📭 Element: %c${element === null ? "null" : "empty NodeList"}`, "color: #e74c3c; font-weight: bold;", "color: #c0392b; font-style: italic;");
        } else {
            console.log("%c📦 Element:", "color: #27ae60; font-weight: bold;", element);
        }
        console.trace("🎯 Source");
        console.groupEnd();
        return element;
    }
    async function waitEl(document, selector, found, {
        timeout = 1e4,
        throttle = 200,
        timeoutResult = false
    } = {}) {
        let timer, element;
        const observer = new MutationObserver($throttle(() => {
            element = document.querySelector(selector);
            if (element) {
                observer.disconnect();
                clearTimeout(timer);
                found(element);
            }
        }, throttle));
        observer.observe(document, {
            subtree: 1,
            childList: 1,
            characterData: 1
        });
        timer = setTimeout(() => {
            observer.disconnect();
            timeoutResult && found(element);
        }, timeout);
    }
    function onUrlChange(callback, timeout = 15) {
        let timer = null;
        let cleaned = false;
        let support_urlchange = false;
        const originalPushState = history.pushState;
        const originalReplaceState = history.replaceState;
        const eventHandler = {
            urlchange: () => trigger("urlchange"),
            popstate: () => trigger("popstate"),
            hashchange: () => trigger("hashchange")
        };
        function trigger(type) {
            clearTimeout(timer);
            if (!support_urlchange && type === "urlchange") support_urlchange = true;
            timer = setTimeout(() => {
                if (support_urlchange) off(false, true);
                callback({
                    type: type,
                    url: location.href,
                    domain: location.hostname
                });
            }, Math.max(15, timeout));
        }
        function off(all = true, clean = false) {
            if (clean && cleaned) return;
            clearTimeout(timer);
            history.pushState = originalPushState;
            history.replaceState = originalReplaceState;
            window.removeEventListener("popstate", eventHandler.popstate);
            window.removeEventListener("hashchange", eventHandler.hashchange);
            all && window.removeEventListener("urlchange", eventHandler.urlchange);
            cleaned = true;
        }
        window.addEventListener("urlchange", eventHandler.urlchange);
        window.addEventListener("popstate", eventHandler.popstate);
        window.addEventListener("hashchange", eventHandler.hashchange);
        history.pushState = function () {
            originalPushState.apply(this, arguments);
            trigger("pushState");
        };
        history.replaceState = function () {
            originalReplaceState.apply(this, arguments);
            trigger("replacestate");
        };
        return {
            off: off
        };
    }
    function regMenu() {
        if (Object.keys(Backup).length > 0) {
            GM_registerMenuCommand("🗑️ Clear Config", () => {
                GM_deleteValue("Config");
                location.reload();
            });
        } else {
            const SaveConfig = structuredClone(Config);
            GM_registerMenuCommand("📝 Save Config", () => {
                GM_setValue("Config", SaveConfig);
            });
        }
    }
    function waitSupport() {
        const {
            off
        } = onUrlChange(uri => {
            if (supportCheck(uri.url)) {
                Detection.run();
                off();
            }
        });
    }
    const restartLive = new RestartLive();
    if (supportCheck()) Detection.run(); else waitSupport();
})();