MidiShowDownload

MidiShow免积分下载

当前为 2025-07-30 提交的版本,查看 最新版本

// ==UserScript==
// @name         MidiShowDownload
// @namespace    https://lgc2333.top/
// @version      0.1.0
// @description  MidiShow免积分下载
// @author       LgCookie
// @homepage     https://github.com/lgc2333/GM/blob/main/packages/MidiShowDownload
// @match        https://www.midishow.com/midi/*
// @match        https://www.midishow.com/zh-tw/midi/*
// @match        https://www.midishow.com/en/midi/*
// @license      MIT
// @grant        GM_addStyle
// ==/UserScript==

/* global kBase sBase $ PNotify */

;(function () {
  'use strict'

  const name = 'MidiShowDownload'
  /** @type {Record<string, string>} */
  const fetchedDataUrls = {}

  /**
   * @param {string} hexString
   * @returns {string}
   */
  function decodeHexToString(hexString) {
    let decodedString = ''
    for (let i = 0; i < hexString.length; i += 2) {
      const hexPair = hexString.substring(i, i + 2)
      if (hexPair === '00') break
      decodedString += String.fromCharCode(Number.parseInt(hexPair, 16))
    }
    return decodedString
  }

  /**
   * @param {string} encoded
   * @param {string} key
   * @returns {string}
   */
  function midiShowDecode(encoded, key) {
    let result = ''
    for (let i = 0; i < encoded.length; ) {
      const char1_val = key.indexOf(encoded.charAt(i++))
      const char2_val = key.indexOf(encoded.charAt(i++))
      const char3_val = key.indexOf(encoded.charAt(i++))
      const char4_val = key.indexOf(encoded.charAt(i++))

      const byte1 = (char1_val << 2) | (char2_val >> 4)
      const byte2 = ((char2_val & 15) << 4) | (char3_val >> 2)
      const byte3 = ((char3_val & 3) << 6) | char4_val

      result += String.fromCharCode(byte1)
      if (char3_val !== 64) result += String.fromCharCode(byte2)
      if (char4_val !== 64) result += String.fromCharCode(byte3)
    }
    return result
  }

  /**
   * @param {string} str
   * @returns {Uint8Array}
   */
  function strDataToUint8Array(str) {
    const arr = new Uint8Array(str.length)
    for (let i = 0; i < str.length; i++) {
      arr[i] = str.charCodeAt(i)
    }
    return arr
  }

  /**
   * @param {string} fileId
   * @param {string} fileMId
   * @returns {Promise<Blob | undefined>}
   */
  async function downloadMidi(fileId, fileMId) {
    const req1 = $.ajax({
      method: 'GET',
      url: fileMId
        .replace(/^tokeno#:@!/, 'token')
        .replace(kBase, sBase)
        .replace('.mid?', '.js?'),
      dataType: 'jsonp',
      cache: true,
      jsonp: false,
      jsonpCallback: 'e',
    })
    const req2 = $.ajax({
      method: 'POST',
      url: `${$.MS.langUrl('/midi/new-file')}?id=${fileId}`,
      dataType: 'text',
      data: { id: fileId },
    })

    /** @typedef {[string, JQuery.Ajax.SuccessTextStatus, JQuery.jqXHR]} Resp */
    const [[res1], [res2, , xhr2]] = /** @type {[Resp, Resp]} */ (
      await $.when(req1, req2).then((p1, p2) => [p1, p2])
    )

    const eTag = xhr2.getResponseHeader('ETag')
    if (!eTag) {
      PNotify.error({ title: name, text: '未找到 ETag' })
      return
    }
    const key = decodeHexToString(eTag) + res2.substring(56)
    const dataStr =
      midiShowDecode(res2.substring(28, 56), key) + // .substr(from: 28, length: 28)
      midiShowDecode(res1, key) +
      midiShowDecode(res2.substring(0, 28), key) // .substr(from: 0, length: 28)
    return new Blob([strDataToUint8Array(dataStr)], { type: 'audio/midi' })
  }

  /**
   * @param {string} url
   * @param {string} filename
   */
  function openSaveDialog(url, filename) {
    const el = document.createElement('a')
    el.href = url
    el.download = filename
    el.target = '_blank'
    el.click()
  }

  async function saveCurrentMidi() {
    const el = /** @type {HTMLDivElement | null} */ (
      document.querySelector('.ms-player-container[data-id][data-mid]')
    )
    if (!el) {
      PNotify.error({ title: name, text: '找不到播放器' })
      return
    }
    const fileId = /** @type {string} */ (el.dataset.id)

    let url = fetchedDataUrls[fileId]
    if (!url) {
      const fileMId = /** @type {string} */ (el.dataset.mid)
      const blob = await downloadMidi(fileId, fileMId)
      if (!blob) return
      url = URL.createObjectURL(blob)
      fetchedDataUrls[fileId] = url
    }

    const filenameEl = /** @type {HTMLHeadingElement | null} */ (
      el.querySelector('h1.pl-md-player')
    )
    // eslint-disable-next-line unicorn/prefer-dom-node-text-content
    const fileBaseName = filenameEl ? `${fileId} - ${filenameEl.innerText}` : fileId
    openSaveDialog(url, fileBaseName)
  }

  window.addEventListener('load', () => {
    const downloadArea = /** @type {HTMLDivElement | null} */ (
      document.getElementById('download')
    )
    const originalDownBtn = downloadArea?.firstElementChild
    if (!originalDownBtn) {
      PNotify.error({ title: name, text: '添加下载按钮失败:定位不到目标元素' })
      return
    }

    GM_addStyle(`a.btn.btn-primary.disabled { filter: grayscale(1); }`)
    const btnHtml =
      `<a class="btn btn-primary btn-sm mb-3 mr-2" href="javascript:void">` +
      `<span class="fa fa-download"></span> ${name}` +
      `</a>`
    originalDownBtn.insertAdjacentHTML('afterend', btnHtml)

    const btn = /** @type {HTMLAnchorElement} */ (originalDownBtn.nextElementSibling)
    btn.addEventListener('click', async () => {
      if (btn.classList.contains('disabled')) return
      btn.classList.add('disabled')
      try {
        await saveCurrentMidi()
      } catch (e) {
        PNotify.error({ title: name, text: `出现意外错误\n${e}` })
      }
      btn.classList.remove('disabled')
    })
  })

  window.addEventListener('beforeunload', () => {
    Object.values(fetchedDataUrls).forEach((v) => URL.revokeObjectURL(v))
  })
})()

QingJ © 2025

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