MidiShowDownload

MidiShow免积分下载 (修复版)

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

// ==UserScript==
// @name         MidiShowDownload
// @namespace     https://greasyfork.org/zh-CN/users/1593463-nander
// @namespace2    https://lgc2333.top/
// @version      0.2.1
// @description  MidiShow免积分下载 (修复版)
// @author       LgCookie & mod nander
// @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 $ JZZ PNotify */

;(function () {
  'use strict'

  const NAME = 'MidiShowDownload'

  /** @type {string | null} */
  let cachedDataURL = null

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

  const originalSMF = JZZ.MIDI.SMF
  const patchedSMF = /** @type {JZZ.MIDI.SMFConstructor} */ (
    /** @this {JZZ.MIDI.SMF} */
    function (data) {
      const blob = new Blob([transformData(data)], { type: 'audio/midi' })
      const url = URL.createObjectURL(blob)
      if (cachedDataURL) URL.revokeObjectURL(cachedDataURL)
      cachedDataURL = url
      PNotify.success(`[${NAME}] 成功截获文件`)
      return originalSMF.apply(this, [data])
    }
  )
  JZZ.MIDI.SMF = patchedSMF

  /**
   * @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 download() {
    const e = $('.ms-player-container')
    const player = /** @type {JZZ.gui.Player | undefined} */ (
      e.JzzPlayer().data('plugin_JzzPlayer')
    )
    if (!player) {
      PNotify.error(`[${NAME}] 无法获取播放器实例`)
      return
    }

    if (!cachedDataURL) {
      await player.loadUrl()
    }
    if (!cachedDataURL) {
      PNotify.error(`[${NAME}] 截获文件失败`)
      return
    }

    const id = /** @type {string} */ (e.data('id'))
    let title = e.find('h1.pl-md-player').text().trim()
    if (!title) {
      // 备用获取标题逻辑
      title = $('.midi-title, h1').first().text().trim() || 'midi_file'
    }
    openSaveDialog(cachedDataURL, `${id || 'download'} - ${title}.mid`)
  }

  function setup() {
    // 兼容新旧版布局的按钮查找器
    const downloadArea = document.getElementById('download') || document.querySelector('.download-area, .midi-download-btn-group')
    let targetBtn = downloadArea?.firstElementChild || document.querySelector('.btn-download, a[href*="download"]')

    // 如果还是找不到具体按钮,尝试直接挂载到播放器下方或详情区域
    if (!targetBtn) {
      targetBtn = document.querySelector('.ms-player-container, .midi-info-box')
    }

    if (!targetBtn) {
      console.error(`[${NAME}] 无法定位到可以挂载按钮的元素`)
      return
    }

    GM_addStyle(`a.btn-midi-download-custom { background-color: #28a745; color: #fff; border-color: #28a745; } a.btn.disabled { filter: grayscale(1); pointer-events: none; }`)

    // 创建一个高亮独立按钮,避免样式冲突
    const btnHtml =
      `<a class="btn btn-primary btn-midi-download-custom btn-sm mb-3 mr-2" href="javascript:void(0)" style="margin-left: 5px;">` +
      `<span class="fa fa-download"></span> 免积分下载` +
      `</a>`

    targetBtn.insertAdjacentHTML('afterend', btnHtml)

    const btn = /** @type {HTMLAnchorElement} */ (targetBtn.nextElementSibling)
    if (!btn) return

    btn.addEventListener('click', async (e) => {
      e.preventDefault()
      if (btn.classList.contains('disabled')) return
      btn.classList.add('disabled')
      try {
        await download()
      } catch (err) {
        PNotify.error(`[${NAME}] 出现意外错误\n${/** @type {any} */ (err).toString()}`)
      }
      btn.classList.remove('disabled')
    })
  }

  // 使用更保险的定时轮询确保页面元素加载完毕后挂载按钮
  const checkTimer = setInterval(() => {
    if (document.readyState === 'complete' || document.querySelector('.ms-player-container')) {
      clearInterval(checkTimer)
      setup()
    }
  }, 500)
})()