AMQ Result Exporter

Export song results to Google Spreadsheet!

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

// ==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或关注我们的公众号极客氢云获取最新地址