Osu!猜歌

osu猜歌,需要先登录osu账号,在玩家页使用

// ==UserScript==
// @name         Osu!猜歌
// @namespace    https://github.com/Exsper/
// @supportURL   https://github.com/Exsper/osuweb-tools/issues
// @version      0.0.6
// @description  osu猜歌,需要先登录osu账号,在玩家页使用
// @author       Exsper
// @match        https://osu.ppy.sh/users/*
// @grant        GM_xmlhttpRequest
// @grant        GM.xmlHttpRequest
// @require      https://code.jquery.com/jquery-3.7.1.min.js
// @license      MIT License
// @run-at       document-end
// ==/UserScript==

let GMX;
if (typeof GM == "undefined") {
    GMX = {
        xmlHttpRequest: GM_xmlhttpRequest,
    };
} else {
    GMX = GM;
}

function getAPI(url, method = "GET") {
    return new Promise(function (resolve, reject) {
        GMX.xmlHttpRequest({
            method: method,
            headers: { "Content-Type": "application/x-www-form-urlencoded" },
            url: url,
            timeout: 10000,
            responseType: "json",
            onload: function (data) {
                if ((data.status >= 200 && data.status < 300) || data.status == 304) {
                    resolve(data.response);
                } else {
                    reject({
                        status: data.status,
                        statusText: data.statusText
                    });
                }
            },
            onerror: function (data) {
                reject({
                    status: data.status,
                    statusText: data.statusText
                });
            }
        });
    });
}

class BeatmapSetInfo {
    constructor() {
        this.id = -1;
        this.artist = "";
        this.artist_unicode = "";
        this.cover = "";
        this.title = "";
        this.title_unicode = "";
        this.creator = "";
        this.preview_url = "";
    }

    convertFromBP(bpdata) {
        this.id = bpdata.beatmapset.id;
        this.artist = bpdata.beatmapset.artist;
        this.artist_unicode = bpdata.beatmapset.artist_unicode;
        this.cover = bpdata.beatmapset.covers.cover;
        this.title = bpdata.beatmapset.title;
        this.title_unicode = bpdata.beatmapset.title_unicode;
        this.creator = bpdata.beatmapset.creator;
        this.preview_url = "https:" + bpdata.beatmapset.preview_url;
        return this;
    }

    convertFromMostPlayed(mpdata) {
        this.id = mpdata.beatmapset.id;
        this.artist = mpdata.beatmapset.artist;
        this.artist_unicode = mpdata.beatmapset.artist_unicode;
        this.cover = mpdata.beatmapset.covers.cover;
        this.title = mpdata.beatmapset.title;
        this.title_unicode = mpdata.beatmapset.title_unicode;
        this.creator = mpdata.beatmapset.creator;
        this.preview_url = "https:" + mpdata.beatmapset.preview_url;
        return this;
    }

    convertFromRankingMostPlayed(rdata) {
        this.id = rdata.id;
        this.artist = rdata.artist;
        this.artist_unicode = rdata.artist_unicode;
        this.cover = rdata.covers.cover;
        this.title = rdata.title;
        this.title_unicode = rdata.title_unicode;
        this.creator = rdata.creator;
        this.preview_url = "https:" + rdata.preview_url;
        return this;
    }
}

class GuessData {
    /**
     * @param {Array<BeatmapSetInfo>} bsis 
     */
    constructor(bsis) {
        this.bsis = bsis;
        this.filter();
        this.guessed = [];
    }

    filter() {
        let ids = [];
        let bsis = [];
        this.bsis.map((bsi) => {
            if (ids.includes(bsi.id)) {
                console.log("[Guess] " + bsi.id + " 相同谱面集ID,丢弃");
                return;
            }
            if (bsi.preview_url.length <= 8) {
                console.log("[Guess] " + bsi.id + " 无预览音频,丢弃");
                return;
            }
            if (bsi.cover.length <= 8) {
                console.log("[Guess] " + bsi.id + " 无背景图,丢弃");
                return;
            }
            ids.push(bsi.id);
            bsis.push(bsi);
        });
        this.bsis = bsis;
    }

    getQuestion() {
        if (this.bsis.length <= 0) return null;
        let index = Math.floor(Math.random() * this.bsis.length);
        let bsi = this.bsis[index];
        this.guessed.push(bsi);
        this.bsis.splice(index, 1);
        return bsi;
    }

    getLeftQuestionCount() {
        return this.bsis.length;
    }
}

function getUrlWithParams(url, params) {
    if (params) {
        var paramarray = [];
        for (var k in params) {
            paramarray.push(k + "=" + encodeURIComponent(params[k]));
        }
        return url + "?" + paramarray.join("&");
    } else {
        return url;
    }
}

class BPInfo {
    static async getBPInfo(href, mode) {
        try {
            let params = { mode, limit: 100, offset: 0 };
            let url = getUrlWithParams(href + "/scores/best", params);
            let mi = await getAPI(url, "GET").then((data) => {
                if (!data || !Array.isArray(data)) return null;
                return data;
            });
            return mi;
        }
        catch (ex) {
            console.log(ex);
            return null;
        }
    }

    static getMode() {
        let selectMode = $(".game-mode-link.game-mode-link--active").attr("data-mode");
        let mode = "osu";
        if (selectMode.indexOf("taiko") >= 0) mode = "taiko";
        if (selectMode.indexOf("fruits") >= 0) mode = "fruits";
        if (selectMode.indexOf("mania") >= 0) mode = "mania";
        return mode;
    }

    static getUrl() {
        let urlex = /users\/\d+/.exec(location.href);
        if (urlex) {
            return location.origin + "/" + urlex[0];
        }
        else throw "网址错误";
    }

    static convert2GuessData(data) {
        let bsis = data.map((b) => {
            return new BeatmapSetInfo().convertFromBP(b);
        });
        return new GuessData(bsis);
    }

    static async getGuessData() {
        let data = await this.getBPInfo(this.getUrl(), this.getMode());
        if (data) {
            return this.convert2GuessData(data);
        }
        else {
            throw "无法获取BP信息";
        }
    }
}

class MostPlayedInfo {
    static async getMostPlayedInfo(href) {
        try {
            let params = { limit: 100, offset: 0 };
            let url = getUrlWithParams(href + "/beatmapsets/most_played", params);
            let mi = await getAPI(url, "GET").then((data) => {
                if (!data || !Array.isArray(data)) return null;
                return data;
            });
            return mi;
        }
        catch (ex) {
            console.log(ex);
            return null;
        }
    }

    static getUrl() {
        let urlex = /users\/\d+/.exec(location.href);
        if (urlex) {
            return location.origin + "/" + urlex[0];
        }
        else throw "网址错误";
    }

    static convert2GuessData(data) {
        let bsis = data.map((b) => {
            return new BeatmapSetInfo().convertFromMostPlayed(b);
        });
        return new GuessData(bsis);
    }

    static async getGuessData() {
        let data = await this.getMostPlayedInfo(this.getUrl());
        if (data) {
            return this.convert2GuessData(data);
        }
        else {
            throw "无法获取最多游玩信息";
        }
    }
}

class FavouriteInfo {
    static async getFavouriteInfo(href) {
        try {
            let params = { limit: 100, offset: 0 };
            let url = getUrlWithParams(href + "/beatmapsets/favourite", params);
            let mi = await getAPI(url, "GET").then((data) => {
                if (!data || !Array.isArray(data)) return null;
                return data;
            });
            return mi;
        }
        catch (ex) {
            console.log(ex);
            return null;
        }
    }

    static getUrl() {
        let urlex = /users\/\d+/.exec(location.href);
        if (urlex) {
            return location.origin + "/" + urlex[0];
        }
        else throw "网址错误";
    }

    static convert2GuessData(data) {
        let bsis = data.map((b) => {
            // 格式与ranking一致
            return new BeatmapSetInfo().convertFromRankingMostPlayed(b);
        });
        return new GuessData(bsis);
    }

    static async getGuessData() {
        let data = await this.getFavouriteInfo(this.getUrl());
        if (data) {
            return this.convert2GuessData(data);
        }
        else {
            throw "无法获取个人收藏信息";
        }
    }
}

class BeatmapRankingInfo {
    static async getRankingMostPlayedInfo(mode, sort) {
        try {
            let mi = await getAPI(`https://osu.ppy.sh/beatmapsets/search?e=&c=&g=&l=&m=${mode}&nsfw=&played=&q=&r=&sort=${sort}&s=`, "GET").then((data) => {
                if (!data || !Array.isArray(data.beatmapsets)) return null;
                return data;
            });
            return mi;
        }
        catch (ex) {
            console.log(ex);
            return null;
        }
    }

    static getMode() {
        let selectMode = $(".game-mode-link.game-mode-link--active").attr("data-mode");
        let mode = "";
        if (selectMode.indexOf("taiko") >= 0) mode = "1";
        if (selectMode.indexOf("fruits") >= 0) mode = "2";
        if (selectMode.indexOf("mania") >= 0) mode = "3";
        return mode;
    }

    static convert2GuessData(data) {
        let bsis = data.map((b) => {
            return new BeatmapSetInfo().convertFromRankingMostPlayed(b);
        });
        return new GuessData(bsis);
    }

    /**
     * @param {"plays_desc"|"favourites_desc"} key 
     * @returns {GuessData}
     */
    static async getGuessData(key) {
        let data = await this.getRankingMostPlayedInfo(this.getMode(), key);
        if (data && data.beatmapsets) {
            return this.convert2GuessData(data.beatmapsets);
        }
        else {
            throw "无法获取谱面排行页信息";
        }
    }
}

class GuessStat {
    /**
     * @param {"song"|"bg"} questionType 
     * @param {GuessData} gd 
     * @param {number} questionCount
     */
    constructor(questionType, gd, questionCount) {
        this.questionType = questionType;
        this.guessdata = gd;
        this.questionCount = questionCount;
        if (this.questionCount >= this.guessdata.getLeftQuestionCount()) this.questionCount = this.guessdata.getLeftQuestionCount();
        this.guessedCount = 0;
        this.correctCount = 0;
        this.currentIndex = 0;
        this.currentScore = 0;
        this.totalScore = 0;
        this.isGuessing = false;

        /** @type {BeatmapSetInfo|null} */
        this.nowQuestion = null;

        this.tipLeft = 3;
        this.songShown = false;
        this.bgShown = false;
        this.artistShown = false;
        this.creatorShown = false;

        this.answerTimer = null;
    }

    startGuess() {
        this.isGuessing = true;
        this.tipLeft = 3;
        this.songShown = false;
        this.bgShown = false;
        this.artistShown = false;
        this.creatorShown = false;

        this.nowQuestion = this.guessdata.getQuestion();

        if (!this.nowQuestion) return;

        if (!this.nowQuestion.artist_unicode && !this.nowQuestion.artist) {
            // 某些谱面没有artist
            this.tipLeft -= 1;
            this.artistShown = true;
        }

        this.currentIndex += 1;

        // 原始得分1000,每2秒-10分,每个提示-100分,减到500停止
        this.currentScore = 1000;

        this.answerTimer = setInterval(() => {
            this.currentScore -= 10;
            if (this.currentScore <= 500) {
                this.currentScore = 500;
                clearInterval(this.answerTimer);
                this.answerTimer = null;
            }
        }, 2000);
    }

    showTip() {
        if (this.tipLeft <= 0) return null;
        this.tipLeft -= 1;

        let pool = [];
        if (!this.songShown && this.questionType === "bg") pool.push(1);
        if (!this.bgShown && this.questionType === "song") pool.push(2);
        if (!this.artistShown) pool.push(3);
        if (!this.creatorShown) pool.push(4);

        this.currentScore -= 100;
        if (this.currentScore <= 500) {
            this.currentScore = 500;
        }

        let index = Math.floor(Math.random() * pool.length);
        switch (pool[index]) {
            case 1: {
                this.songShown = true;
                return { type: "song", content: this.nowQuestion.preview_url };
            }
            case 2: {
                this.bgShown = true;
                return { type: "bg", content: this.nowQuestion.cover };
            }
            case 3: {
                this.artistShown = true;
                let tipText = (this.nowQuestion.artist_unicode && this.nowQuestion.artist_unicode !== this.nowQuestion.artist) ? this.nowQuestion.artist_unicode + " (" + this.nowQuestion.artist + ")" : this.nowQuestion.artist;
                return { type: "text", content: "曲师为:" + tipText };
            }
            case 4: {
                this.creatorShown = true;
                return { type: "text", content: "谱师为:" + this.nowQuestion.creator };
            }
        }
    }

    checkAnswer(answer) {
        const minDistancePercent = 0.5;

        function edit_distance(a, b) {
            let lena = a.length;
            let lenb = b.length;
            let d = new Array(lena + 1).fill(0).map(e => new Array(lenb + 1).fill(0));;
            let i, j;

            for (i = 0; i <= lena; i++) {
                d[i][0] = i;
            }
            for (j = 0; j <= lenb; j++) {
                d[0][j] = j;
            }

            for (i = 1; i <= lena; i++) {
                for (j = 1; j <= lenb; j++) {
                    if (a[i - 1] == b[j - 1]) {
                        d[i][j] = d[i - 1][j - 1];
                    } else {
                        d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + 1);
                    }
                }
            }

            return d[lena][lenb];
        }

        if (this.nowQuestion.title_unicode) {
            let _title = this.nowQuestion.title_unicode.replace(/[\/:*?"<>| ]/g, "").toLocaleLowerCase();
            if (_title.length <= 0) _title = this.nowQuestion.title_unicode.toLocaleLowerCase();
            let _answer = answer.replace(/[\/:*?"<>| ]/g, "").toLocaleLowerCase();
            if (_answer.length <= 0) _answer = answer.toLocaleLowerCase();
            let dp = edit_distance(_title, _answer) / Math.pow(_title.length, 1.1);
            if (dp <= minDistancePercent) return true;
        }
        let _title = this.nowQuestion.title.replace(/[\/:*?"<>| ]/g, "").replace(/[^a-zA-Z0-9]/g, "").toLocaleLowerCase();
        let _answer = answer.replace(/[\/:*?"<>| ]/g, "").replace(/[^a-zA-Z0-9]/g, "").toLocaleLowerCase();
        if (_title.length <= 0) return false;
        let dp = edit_distance(_title, _answer) / Math.pow(_title.length, 1.1);
        if (dp <= minDistancePercent) return true;
        return false;
    }

    passGuessOne() {
        this.isGuessing = false;
        clearInterval(this.answerTimer);
        this.answerTimer = null;
        this.guessedCount += 1;
        this.correctCount += 1;
        this.totalScore += this.currentScore;
    }

    abortGuessOne() {
        this.isGuessing = false;
        clearInterval(this.answerTimer);
        this.answerTimer = null;
        this.guessedCount += 1;
    }

    getLeftQuestionCount() {
        let bsisLeft = this.guessdata.getLeftQuestionCount();
        let thisRoundLeft = this.questionCount - this.guessedCount;
        return Math.min(bsisLeft, thisRoundLeft);
    }
}

function addCss() {
    if (!$(".song-guess-style").length) {
        $(document.head).append($("<style class='song-guess-style'></style>").html(
            `
            .guesslabel {font-size: 18px; margin-right: 10px;}
            .guessButton {margin: 0 10px; font-size: 24px; background-color: #5864ff; border: none; color: white;}
            .guessButton:hover {background-color: #7781ff;}
            .guessButton:disabled {background-color: #b7b7b7;}
            .guessCloseBtnDiv {position: absolute; right: 15px; top: 15px;}
            .guessPanel {height: 80%; overflow-y: auto; color: #000; width: 80%; max-width: 800px; position: fixed; display: none;z-index: 10000;padding: 15px 20px 10px;-webkit-border-radius: 10px;-moz-border-radius: 10px;border-radius: 10px;background: #fff; left: 50%; top:50%; transform: translate(-50%, -50%);}
            .guessOverlay {position: fixed;top: 0;left: 0;bottom:0;right:0;width: 100%;height: 100%;z-index: 9999;background: #000;display: none;-ms-filter: 'alpha(Opacity=50)';-moz-opacity: .5;-khtml-opacity: .5;opacity: .5;}
            `
        ));
    }
}

function openRankPanel(guessStat) {
    let guessContent = $("#guessContent");
    guessContent.empty();
    $("#guessOverlay").fadeIn(200);
    $("#guessPanel").fadeIn(200);

    let score = guessStat.totalScore;
    let max_score = guessStat.guessedCount * 1000;
    let percent = score / max_score;
    let rank = "";
    if (percent >= 0.9) rank = "举世无双";
    else if (percent >= 0.8) rank = "登峰造极";
    else if (percent >= 0.7) rank = "技冠群雄";
    else if (percent >= 0.6) rank = "炉火纯青";
    else if (percent >= 0.5) rank = "出类拔萃";
    else if (percent >= 0.4) rank = "略有小成";
    else if (percent >= 0.3) rank = "渐入佳境";
    else if (percent >= 0.2) rank = "略知一二";
    else if (percent >= 0.1) rank = "初窥门径";
    else rank = "未曾涉猎";

    let stars = new Array(Math.ceil(percent * 10)).fill("★").join("");

    guessContent.append(
        `
        <span style="display: grid;font-size: 32px;">猜歌结束</span>
        <br>
        <br>

        <span style="font-size: 24px;">您猜了</span>
        <span style="font-size: 36px;">${guessStat.guessedCount}</span>
        <span style="font-size: 24px;">首歌</span>
  
        <br>

        <span style="font-size: 24px;">共答对了</span>
        <span style="font-size: 48px;">${guessStat.correctCount}</span>
        <span style="font-size: 24px;">首</span>

        <br>

        <span style="font-size: 24px;">您的评价是...</span>
        <br>
        <br>
        <span style="font-size: 128px;">${rank}</span>
        <br>
        <span style="font-size: 32px;">${stars}</span>
        <br>
        <br>
        <br>

        <button id="guess-restart" class="guessButton" style="width: 60%; height: 80px">重新猜歌</button>
        </div>
        `
    );

    $("#guess-restart").click(() => {
        openSettingPanel();
    });
}

function openGuessPanel(guessStat) {
    let guessContent = $("#guessContent");
    guessContent.empty();
    $("#guessOverlay").fadeIn(200);
    $("#guessPanel").fadeIn(200);

    guessContent.append(
        `
        <div style="margin-top: 30px;display: flex;flex-wrap: wrap;justify-content: space-evenly;">
        <span id="guess-question-index">#</span>
        <span id="guess-question-score">得分:</span>
        <span id="guess-correct-count">答对题目:</span>
        <span id="guess-total-score">总得分:</span>
        </div>
        <br>
        <br>

        <span id="guess-stat-text" style="display: grid;font-size: 32px;">请输入歌曲名称</span>
        <br>
        <br>

        <div id="guess-question-main" style="display: flex;justify-content: center;flex-wrap: wrap;">
        </div>
        <br>
        <br>

        <div id="guess-question-tips" style="display: flex;justify-content: center;flex-wrap: wrap;">
        </div>
        <br>
        <br>

        <div style="display: flex;justify-content: space-evenly;bottom: 100px;position: absolute;width: 95%;font-size: 32px;">
        <input type="text" id="guess-question-answer" style="text-align: center; width: 100%;" autocomplete="off" autofocus></input>
        </div>
        <br>
        <br>

        <div style="display: flex;justify-content: space-evenly;bottom: 20px;position: absolute;width: 95%;">
        <button id="guess-abort" class="guessButton" style="width: 30%; height: 60px">放弃</button>
        <button id="guess-enter" class="guessButton" style="width: 40%; height: 60px">确定</button>
        <button id="guess-tip" class="guessButton" style="width: 30%; height: 60px">提示</button>
        </div>
        `
    );

    let dataUpdateTimer = setInterval(() => {
        $("#guess-question-index").text("#" + guessStat.currentIndex + "/" + guessStat.questionCount);
        $("#guess-question-score").text("得分:" + guessStat.currentScore);
        $("#guess-correct-count").text("答对题目:" + guessStat.correctCount + "/" + guessStat.guessedCount);
        $("#guess-total-score").text("总得分:" + guessStat.totalScore);
        if (!$("#guessPanel").is(":visible")) {
            clearInterval(dataUpdateTimer);
            dataUpdateTimer = null;
        }
    }, 1000);

    function showQuestion() {
        $("#guess-question-main").empty();
        $("#guess-question-tips").empty();
        $("#guess-question-answer").val("");
        $("#guess-question-answer").focus();

        guessStat.startGuess();
        if (!guessStat.nowQuestion) {
            $("#guess-enter").text("出错了!");
            return;
        }
        $("#guess-stat-text").text("请输入歌曲名称");

        $("#guess-abort").attr("disabled", false);
        $("#guess-enter").attr("disabled", false);
        $("#guess-enter").text("确定");
        $("#guess-tip").attr("disabled", false);
        $("#guess-tip").text("提示");

        if (guessStat.questionType === "song") {
            $("#guess-question-main").append(
                `
            <audio controls>
            <source src="${guessStat.nowQuestion.preview_url}" type="audio/mpeg">
            Your browser does not support the audio element.
            </audio>
            `
            );
        }
        else if (guessStat.questionType === "bg") {
            $("#guess-question-main").append(
                `
            <img src="${guessStat.nowQuestion.cover}" height="100px">
            `
            );
        }
    }

    function showTip() {
        let tip = guessStat.showTip();
        if (tip) {
            if (tip.type === "song") {
                $("#guess-question-tips").append(
                    `
                <audio controls>
                <source src="${tip.content}" type="audio/mpeg">
                Your browser does not support the audio element.
                </audio>
                `
                );
            }
            else if (tip.type === "bg") {
                $("#guess-question-tips").append(
                    `
                <img src="${tip.content}" height="100px">
                `
                );
            }
            else if (tip.type === "text") {
                $("#guess-question-tips").append(
                    `
                <span style="width: 100%;">${tip.content}</span>
                `
                );
            }
        }
        if (guessStat.tipLeft <= 0) {
            $("#guess-tip").attr("disabled", true);
            $("#guess-tip").text("没了");
        }
    }

    function showCorrectAnswer() {
        if (guessStat.nowQuestion.title_unicode) $("#guess-question-answer").val(guessStat.nowQuestion.title_unicode);
        else $("#guess-question-answer").val(guessStat.nowQuestion.title);
    }

    function waitForNext() {
        $("#guess-abort").attr("disabled", true);
        $("#guess-tip").attr("disabled", true);
        $("#guess-enter").attr("disabled", false);
        $("#guess-enter").text("下一首");
    }

    function finishGame() {
        clearInterval(dataUpdateTimer);
        dataUpdateTimer = null;
        $("#guess-abort").attr("disabled", true);
        $("#guess-tip").attr("disabled", true);
        $("#guess-enter").attr("disabled", true);
        $("#guess-enter").text("游戏结束");
        openRankPanel(guessStat);
    }

    $("#guess-abort").click(() => {
        $("#guess-stat-text").text("跳过该曲目");
        guessStat.abortGuessOne();
        showCorrectAnswer();
        if (guessStat.getLeftQuestionCount() > 0) waitForNext();
        else finishGame();
    });

    $("#guess-tip").click(() => {
        showTip();
    });

    $("#guess-question-answer").keydown((event) => {
        if (event.keyCode === 13) {
            $("#guess-enter").click();
        }
    });

    $("#guess-enter").click(() => {
        if (guessStat.isGuessing) {
            if (guessStat.checkAnswer($("#guess-question-answer").val())) {
                $("#guess-stat-text").text("恭喜您,答对了!");
                showCorrectAnswer();
                guessStat.passGuessOne();
                if (guessStat.getLeftQuestionCount() > 0) waitForNext();
                else finishGame();
            }
            else {
                $("#guess-enter").text("答案不对!");
            }
        }
        else {
            showQuestion();
        }
    });

    showQuestion();
}

function openSettingPanel() {
    let guessContent = $("#guessContent");
    guessContent.empty();
    $("#guessOverlay").fadeIn(200);
    $("#guessPanel").fadeIn(200);

    guessContent.append(
        `
        <span style="display: grid;font-size: 32px;">欢迎来到osu!猜歌</span>
        <br>
        <br>

        <span style="font-size: 24px;">选择题库来源:</span>
        <br>
        <input type="radio" id="guess-source-bp" name="guess-source" value="bp" checked>
        <label for="guess-source-bp" class="guesslabel">个人BP列表</label>
  
        <input type="radio" id="guess-source-mp" name="guess-source" value="mp">
        <label for="guess-source-mp" class="guesslabel">个人最多游玩</label>

        <input type="radio" id="guess-source-fav" name="guess-source" value="fav">
        <label for="guess-source-fav" class="guesslabel">个人收藏</label>

        <br>

        <input type="radio" id="guess-source-rmp" name="guess-source" value="rmp">
        <label for="guess-source-rmp" class="guesslabel">最多游玩谱面TOP50</label>

        <input type="radio" id="guess-source-rmf" name="guess-source" value="rmf">
        <label for="guess-source-rmf" class="guesslabel">最多收藏谱面TOP50</label>
        <br>
        <span>将按当前网页的玩家和模式获取谱面数据</span>
        <br>
        <br>

        <span style="font-size: 24px;">选择题库数量:</span>
        <br>
        <input type="radio" id="guess-count-10" name="guess-count" value="10" checked>
        <label for="guess-count-10" class="guesslabel">10</label>

        <input type="radio" id="guess-count-15" name="guess-count" value="15">
        <label for="guess-count-15" class="guesslabel">15</label>

        <input type="radio" id="guess-count-20" name="guess-count" value="20">
        <label for="guess-count-20" class="guesslabel">20</label>

        <input type="radio" id="guess-count-100" name="guess-count" value="100">
        <label for="guess-count-100" class="guesslabel">最大</label>

        <br>

        <input type="radio" id="guess-count-custom" name="guess-count" value="-1">
        <label for="guess-count-custom" class="guesslabel">自定义:</label>
        <input type="number" id="guess-count-custom-num" min="1" max="100" step="1" value="50">
        <br>
        <br>

        <span style="font-size: 24px;">选择猜歌方式:</span>
        <br>
        <input type="radio" id="guess-type-song" name="guess-type" value="song" checked>
        <label for="guess-type-song" class="guesslabel">音频猜歌</label>
  
        <input type="radio" id="guess-type-bg" name="guess-type" value="bg">
        <label for="guess-type-bg" class="guesslabel">图片猜歌</label>
        <br>
        <br>

        <div style="display: flex;justify-content: center;">
        <button id="guess-start" class="guessButton" style="width: 60%; height: 80px">开始猜歌</button>
        </div>
        `
    );

    $("#guess-count-custom-num").on("input", () => {
        $("#guess-count-custom").prop("checked", true);
    });

    $("#guess-start").click(async () => {
        let guessSource = $("input[name=guess-source]:checked").val();
        let questionCount = parseInt($("input[name=guess-count]:checked").val());
        if (questionCount <= 0) {
            questionCount = parseInt($("#guess-count-custom-num").val());
            if (questionCount <= 0) questionCount = 10;
            else if (questionCount > 100) questionCount = 100;
        }
        let guessType = $("input[name=guess-type]:checked").val();
        $("#guess-start").attr("disabled", true);
        $("#guess-start").text("正在获取题库...");
        let guessdata;
        try {
            if (guessSource === "bp") {
                guessdata = await BPInfo.getGuessData();
            }
            else if (guessSource === "mp") {
                guessdata = await MostPlayedInfo.getGuessData();
            }
            else if (guessSource === "fav") {
                guessdata = await FavouriteInfo.getGuessData();
            }
            else if (guessSource === "rmp") {
                guessdata = await BeatmapRankingInfo.getGuessData("plays_desc");
            }
            else if (guessSource === "rmf") {
                guessdata = await BeatmapRankingInfo.getGuessData("favourites_desc");
            }
        }
        catch (ex) {
            $("#guess-start").attr("disabled", false);
            $("#guess-start").text(ex);
            return;
        }
        let guessStat = new GuessStat(guessType, guessdata, questionCount);

        openGuessPanel(guessStat);
    });
}

function closeGuessPanel() {
    $("#guessOverlay").fadeOut(200);
    $("#guessPanel").fadeOut(200);
}

function startScrpit() {
    addCss();
    $("body").append("<div class='guessOverlay' id='guessOverlay' style='display:none;''></div>");
    $("body").append("<div class='guessPanel' id='guessPanel' style='display:none;'></div>");
    let guessContent = $("<div id='guessContent' style='text-align: center;'>");
    $("#guessPanel").append(
        "<div class='guessCloseBtnDiv' style='display: block;''><button class='guessCloseBtn'>x</button></div>",
        guessContent
    );
    $("#guessOverlay, .guessCloseBtn").click(function () {
        closeGuessPanel();
    });

    let openPanel = $("<div style='position: absolute;right: 40px;padding: 0 10px;cursor: pointer;'>开始猜歌</div>").appendTo($(".profile-info__details"));
    openPanel.click(function () {
        openSettingPanel();
    });
}

// 确保网页加载完成
function check() {
    let $script = $("#guessOverlay");
    let $modegroup = $(".game-mode-link");
    if ($script.length <= 0) {
        if ($modegroup.length > 0) {
            startScrpit();
            // 局部刷新重新加载
            let interval = setInterval(() => {
                // console.log("检查脚本框架");
                if ($("#guessOverlay").length <= 0) {
                    // console.log("检查到页面局部刷新");
                    clearInterval(interval);
                    check();
                }
            }, 5000);
        }
        else setTimeout(function () { check(); }, 2000);
    }

}

$(document).ready(() => {
    check();
});