AMQ Result Exporter

Export song results to Google Spreadsheet!

目前為 2022-08-16 提交的版本,檢視 最新版本

// ==UserScript==
// @name         AMQ Result Exporter
// @namespace    https://tampermonkey.net/
// @version      0.2.0
// @description  Export song results to Google Spreadsheet!
// @author       SlashNephy <[email protected]>
// @match        https://animemusicquiz.com/
// @license      MIT license
// @grant        GM_xmlhttpRequest
// @icon         https://animemusicquiz.com/favicon-32x32.png
// @connect      script.google.com
// @connect      raw.githubusercontent.com
// ==/UserScript==
const executeXhr = async (request) => {
    return new Promise((resolve, reject) => {
        GM_xmlhttpRequest({
            ...request,
            onload: (response) => {
                resolve(response);
            },
            onerror: (error) => {
                reject(error);
            },
        });
    });
};

const fetchArmEntries = async () => {
    const response = await executeXhr({
        method: 'GET',
        url: 'https://raw.githubusercontent.com/kawaiioverflow/arm/master/arm.json',
    });
    return JSON.parse(response.responseText);
};

class AmqAnswerTimesUtility {
    songStartTime = 0;
    playerTimes = [];
    constructor() {
        if (typeof Listener === 'undefined') {
            return;
        }
        new Listener('play next song', () => {
            this.songStartTime = Date.now();
            this.playerTimes = [];
        }).bindListener();
        new Listener('player answered', (data) => {
            const time = Date.now() - this.songStartTime;
            data.forEach((gamePlayerId) => {
                this.playerTimes[gamePlayerId] = time;
            });
        }).bindListener();
        new Listener('Join Game', (data) => {
            const quizState = data.quizState;
            if (quizState) {
                this.songStartTime = Date.now() - quizState.songTimer * 1000;
            }
        }).bindListener();
    }
}
const amqAnswerTimesUtility = new AmqAnswerTimesUtility();

const AMQ_createInstalledWindow = () => {
    if (!window.setupDocumentDone)
        return;
    if ($('#installedModal').length === 0) {
        $('#gameContainer').append($(`
            <div class="modal fade" id="installedModal" tabindex="-1" role="dialog">
                <div class="modal-dialog" role="document">
                    <div class="modal-content">
                        <div class="modal-header">
                            <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                                <span aria-hidden="true">×</span>
                            </button>
                            <h2 class="modal-title">Installed Userscripts</h2>
                        </div>
                        <div class="modal-body" style="overflow-y: auto;max-height: calc(100vh - 150px);">
                            <div id="installedContainer">
                                You have the following scripts installed (click on each of them to learn more)<br>
                                This window can also be opened by going to AMQ settings (the gear icon on bottom right) and clicking "Installed Userscripts"
                                <div id="installedListContainer"></div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        `));
        $('#mainMenu')
            .prepend($(`
            <div class="button floatingContainer mainMenuButton" id="mpInstalled" data-toggle="modal" data-target="#installedModal">
                <h1>Installed Userscripts</h1>
            </div>
        `))
            .css('margin-top', '20vh');
        $('#optionsContainer > ul').prepend($(`
            <li class="clickAble" data-toggle="modal" data-target="#installedModal">Installed Userscripts</li>
        `));
        AMQ_addStyle(`
            .descriptionContainer {
                width: 95%;
                margin: auto;
            }
            .descriptionContainer img {
                width: 80%;
                margin: 10px 10%;
            }
        `);
    }
};
const AMQ_addScriptData = (metadata) => {
    AMQ_createInstalledWindow();
    $('#installedListContainer').append($('<div></div>')
        .append($('<h4></h4>')
        .html(`<i class="fa fa-caret-right"></i> ${metadata.name !== undefined ? metadata.name : 'Unknown'} by ${metadata.author !== undefined ? metadata.author : 'Unknown'}`)
        .css('font-weight', 'bold')
        .css('cursor', 'pointer')
        .click(function () {
        const selector = $(this).next();
        if (selector.is(':visible')) {
            selector.slideUp();
            $(this).find('.fa-caret-down').addClass('fa-caret-right').removeClass('fa-caret-down');
        }
        else {
            selector.slideDown();
            $(this).find('.fa-caret-right').addClass('fa-caret-down').removeClass('fa-caret-right');
        }
    }))
        .append($('<div></div>')
        .addClass('descriptionContainer')
        .html(metadata.description !== undefined ? metadata.description : 'No description provided')
        .hide()));
};
const AMQ_addStyle = (css) => {
    const head = document.head;
    const style = document.createElement('style');
    head.appendChild(style);
    style.appendChild(document.createTextNode(css));
};

const GAS_URL = 'https://script.google.com/macros/s/xxx/exec';
const armEntries = [];
fetchArmEntries()
    .then((entries) => armEntries.push(...entries))
    .catch(console.error);
const executeGas = async (row) => {
    await executeXhr({
        url: GAS_URL,
        method: 'POST',
        data: JSON.stringify(row),
    });
};
const handle = (payload) => {
    const self = Object.values(quiz.players).find((p) => p.isSelf && p._inGame);
    if (!self) {
        return;
    }
    const result = {
        time: Date.now(),
        number: parseInt($('#qpCurrentSongCount').text()),
        game_mode: quiz.gameMode,
        song: {
            name: payload.songInfo.songName,
            anime: {
                answer: {
                    english: payload.songInfo.animeNames.english,
                    romaji: payload.songInfo.animeNames.romaji,
                    alt_answers: [...new Set(payload.songInfo.altAnimeNames.concat(payload.songInfo.altAnimeNamesAnswers))],
                },
                vintage: payload.songInfo.vintage,
                tags: payload.songInfo.animeTags,
                genre: payload.songInfo.animeGenre,
                mal_id: payload.songInfo.siteIds.malId,
                annict_id: armEntries.find((e) => e.mal_id === payload.songInfo.siteIds.malId)?.annict_id,
                type: payload.songInfo.animeType,
                score: payload.songInfo.animeScore,
            },
            artist: payload.songInfo.artist,
            difficulty: payload.songInfo.animeDifficulty.toFixed(1),
            type: payload.songInfo.type === 3
                ? 'Insert Song'
                : payload.songInfo.type === 2
                    ? `Ending ${payload.songInfo.typeNumber}`
                    : `Opening ${payload.songInfo.typeNumber}`,
            file: {
                sample_point: quizVideoController.moePlayers[quizVideoController.currentMoePlayerId].startPoint,
                video_length: parseFloat(quizVideoController.moePlayers[quizVideoController.currentMoePlayerId].$player
                    .find('video')[0]
                    .duration.toFixed(2)),
                video_url: payload.songInfo.urlMap.catbox
                    ? payload.songInfo.urlMap.catbox['720'] || payload.songInfo.urlMap.catbox['480']
                    : payload.songInfo.urlMap.openingsmoe
                        ? payload.songInfo.urlMap.openingsmoe['720'] || payload.songInfo.urlMap.openingsmoe['480']
                        : null,
                audio_url: payload.songInfo.urlMap.catbox
                    ? payload.songInfo.urlMap.catbox['0']
                    : payload.songInfo.urlMap.openingsmoe
                        ? payload.songInfo.urlMap.openingsmoe['0']
                        : null,
            },
        },
        players: {
            count: Object.values(quiz.players).length,
            active_count: Object.values(quiz.players).filter((player) => !player.avatarSlot._disabled).length,
            correct_count: payload.players.filter((player) => player.correct).length,
            items: Object.values(payload.players)
                .sort((a, b) => {
                if (a.answerNumber !== undefined && b.answerNumber !== undefined) {
                    return a.answerNumber - b.answerNumber;
                }
                const p1name = quiz.players[a.gamePlayerId]._name;
                const p2name = quiz.players[b.gamePlayerId]._name;
                return p1name.localeCompare(p2name);
            })
                .map((p) => ({
                status: p.listStatus,
                id: p.gamePlayerId,
                name: quiz.players[p.gamePlayerId]._name,
                score: p.score,
                correctGuesses: quiz.gameMode !== 'Standard' && quiz.gameMode !== 'Ranked' ? p.correctGuesses : p.score,
                correct: p.correct,
                answer: quiz.players[p.gamePlayerId].avatarSlot.$answerContainerText.text(),
                guessTime: amqAnswerTimesUtility.playerTimes[p.gamePlayerId],
                active: !quiz.players[p.gamePlayerId].avatarSlot._disabled,
                position: p.position,
                positionSlot: p.positionSlot,
            })),
        },
    };
    const selfResult = result.players.items.find((p) => p.id === self.gamePlayerId);
    const selfAnswer = selfResult?.answer.replace('...', '').replace(/ \(\d+ms\)$/, '') || '';
    const row = [
        result.time,
        result.number,
        result.game_mode,
        selfResult?.correct ?? false,
        selfAnswer,
        selfResult?.guessTime ?? 0,
        result.song.anime.answer.romaji,
        result.song.anime.answer.english,
        result.song.anime.answer.alt_answers.join('\n'),
        result.song.difficulty,
        result.song.type,
        result.song.anime.vintage,
        result.song.anime.type,
        result.song.anime.score,
        result.song.anime.mal_id,
        result.song.anime.annict_id ?? '',
        result.song.name,
        result.song.artist,
        result.song.anime.genre.join('\n'),
        result.song.anime.tags.join('\n'),
        result.song.file.video_url ?? '',
        result.song.file.audio_url ?? '',
        result.song.file.video_length,
        result.song.file.sample_point,
        result.players.correct_count,
        result.players.active_count,
        result.players.items
            .filter((p) => p.correct)
            .map((p) => p.name)
            .join('\n'),
        result.players.items.map((p) => p.name).join('\n'),
        selfResult?.status ?? 0,
        result.players.items
            .filter((p) => p.status)
            .map((p) => p.name)
            .join('\n'),
    ];
    executeGas(row).catch(console.error);
};
const listener = new Listener('answer results', handle);
listener.bindListener();
AMQ_addScriptData({
    name: 'Result Exporter',
    author: 'SlashNephy &lt;[email protected]&gt;',
    description: '<p>Export song results to Google Spreadsheet!</p>',
});

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址