AMQ Song Guess Rate

Display guess rates per song in side panel of the song. (Requires AMQ Detailed Song Info plugin: version 0.3.0 or higher)

当前为 2023-02-06 提交的版本,查看 最新版本

// ==UserScript==
// @name            AMQ Song Guess Rate
// @namespace       https://github.com/SlashNephy
// @version         0.3.0
// @author          SlashNephy
// @description     Display guess rates per song in side panel of the song. (Requires AMQ Detailed Song Info plugin: version 0.3.0 or higher)
// @description:ja  曲のサイドパネルに曲ごとの正答率を表示します。(0.3.0 以降の AMQ Detailed Song Info プラグインが必要です。)
// @homepage        https://scrapbox.io/slashnephy/AMQ_%E3%81%A7%E6%9B%B2%E3%81%94%E3%81%A8%E3%81%AE%E6%AD%A3%E7%AD%94%E7%8E%87%E3%82%92%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B_UserScript
// @homepageURL     https://scrapbox.io/slashnephy/AMQ_%E3%81%A7%E6%9B%B2%E3%81%94%E3%81%A8%E3%81%AE%E6%AD%A3%E7%AD%94%E7%8E%87%E3%82%92%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B_UserScript
// @icon            https://animemusicquiz.com/favicon-32x32.png
// @supportURL      https://github.com/SlashNephy/.github/issues
// @match           https://animemusicquiz.com/*
// @require         https://cdn.jsdelivr.net/gh/TheJoseph98/AMQ-Scripts@b97377730c4e8553d2dcdda7fba00f6e83d5a18a/common/amqScriptInfo.js
// @grant           GM_getValue
// @grant           GM_setValue
// @grant           unsafeWindow
// @grant           GM_deleteValue
// @grant           GM_listValues
// @license         MIT license
// ==/UserScript==

const awaitFor = async (predicate, timeout) => {
  return new Promise((resolve, reject) => {
    let timer
    const interval = window.setInterval(() => {
      if (predicate()) {
        clearInterval(interval)
        clearTimeout(timer)
        resolve()
      }
    }, 500)
    if (timeout !== undefined) {
      timer = setTimeout(() => {
        clearInterval(interval)
        clearTimeout(timer)
        reject(new Error('timeout'))
      }, timeout)
    }
  })
}

class LocalizableString {
  localization
  constructor(localization) {
    this.localization = localization
  }
  static _orEmpty(a, b) {
    return a !== undefined && a.length > 0 ? a : b
  }
  toString() {
    switch (navigator.language) {
      case 'ja':
        return LocalizableString._orEmpty(this.localization.ja, this.localization.en)
      default:
        return this.localization.en
    }
  }
  format(...args) {
    return toString().replace(/{(\d+)}/g, (match, index) => {
      return args[index]?.toString() ?? 'undefined'
    })
  }
  toError() {
    return new Error(toString())
  }
}

const message = new LocalizableString({
  en: 'Detailed Song Info could not be detected, either Detailed Song Info is not installed or this UserScript is loaded before Detailed Song Info.',
  ja: 'Detailed Song Info を検出できませんでした。Detailed Song Info がインストールされていないか、この UserScript が Detailed Song Info よりも先に読み込まれています。',
})
const getDetailedSongInfo = async () => {
  return awaitFor(() => unsafeWindow.detailedSongInfo !== undefined, 10000)
    .then(() => unsafeWindow.detailedSongInfo)
    .catch(() => {
      throw message.toError()
    })
}

const onReady = (callback) => {
  if (document.getElementById('startPage')) {
    return
  }
  awaitFor(() => document.getElementById('loadingScreen')?.classList.contains('hidden') === true)
    .then(callback)
    .catch(console.error)
}

class GM_Value {
  key
  defaultValue
  constructor(key, defaultValue, initialize = true) {
    this.key = key
    this.defaultValue = defaultValue
    const value = GM_getValue(key, null)
    if (initialize && value === null) {
      GM_setValue(key, defaultValue)
    }
  }
  get() {
    return GM_getValue(this.key, this.defaultValue)
  }
  set(value) {
    GM_setValue(this.key, value)
  }
  delete() {
    GM_deleteValue(this.key)
  }
}

const makeSha256HexDigest = async (message) => {
  const data = new TextEncoder().encode(message)
  const buffer = await crypto.subtle.digest('SHA-256', data)
  const arrayBuffer = Array.from(new Uint8Array(buffer))
  return arrayBuffer.map((b) => b.toString(16).padStart(2, '0')).join('')
}

const increment = async (key, isCorrect) => {
  const hashKey = await makeSha256HexDigest(key)
  const value = new GM_Value(hashKey, { correct: 0, total: 0 })
  const count = value.get()
  count.total++
  if (isCorrect) {
    count.correct++
  }
  value.set(count)
  return count
}
const migrate = async () => {
  const regex = /^[\da-f]{64}$/
  const oldKeys = GM_listValues().filter((k) => regex.exec(k) === null)
  await Promise.all(
    oldKeys.map(async (key) => {
      const hashKey = await makeSha256HexDigest(key)
      const value = new GM_Value(hashKey, { correct: 0, total: 0 })
      const count = value.get()
      const oldValue = new GM_Value(hashKey, { correct: 0, total: 0 }, false)
      const oldCount = oldValue.get()
      count.total += oldCount.total
      count.correct += oldCount.correct
      value.set(count)
      oldValue.delete()
    })
  )
}
onReady(() => {
  getDetailedSongInfo()
    .then(({ register }) => {
      register({
        id: 'guess-rate-row',
        title: 'Guess Rate',
        async content(event) {
          const self = Object.values(unsafeWindow.quiz.players).find((p) => p.isSelf && p._inGame)
          if (self === undefined) {
            return null
          }
          const isCorrect = event.players.find((p) => p.gamePlayerId === self.gamePlayerId)?.correct === true
          const count = await increment(`${event.songInfo.songName}_${event.songInfo.artist}`, isCorrect)
          return `${count.correct} / ${count.total} (${((count.correct / count.total) * 100).toFixed(1)} %)`
        },
      })
    })
    .catch(console.error)
  migrate().catch(console.error)
  AMQ_addScriptData({
    name: 'Song Guess Rate',
    author: 'SlashNephy <[email protected]>',
    description:
      'Display guess rates per song in side panel of the song. (Requires AMQ Detailed Song Info plugin: version 0.3.0 or higher)',
  })
})

QingJ © 2025

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