MetingJS 是如何配合 Aplayer 載入歌單的?

guangzan發表於2021-07-02

Meting.js 介紹

image

Meting.js 依賴 APlayer.js,擴充套件了 APlayer.js 的功能,能夠使 APlayer.js 載入網易雲音樂、QQ 音樂、蝦米音樂中的歌單。

安裝

<!-- require APlayer -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/aplayer/dist/APlayer.min.css">
<script src="https://cdn.jsdelivr.net/npm/aplayer/dist/APlayer.min.js"></script>
<!-- require MetingJS -->
<script src="https://cdn.jsdelivr.net/npm/meting@2/dist/Meting.min.js"></script>

使用

載入歌單

<meting-js
	server="netease"
	type="playlist"
	id="60198">
</meting-js>

通過 auto 屬性載入單曲:

<meting-js
	auto="https://y.qq.com/n/yqq/song/001RGrEX3ija5X.html">
</meting-js>

通過 url 載入單曲:

<meting-js
	name="rainymood"
	artist="rainymood"
	url="https://rainymood.com/audio1110/0.m4a"
	cover="https://rainymood.com/i/badge.jpg">
</meting-js>

載入託管在其他伺服器上的單曲:

<meting-js
	name="rainymood"
	artist="rainymood"
	url="https://rainymood.com/audio1110/0.m4a"
	cover="https://rainymood.com/i/badge.jpg"
	fixed="true">
	<pre hidden>
		[00:00.00]This
		[00:04.01]is
		[00:08.02]lyric
	</pre>
</meting-js>

原始碼解析

class MetingJSElement extends HTMLElement {
  /**
   * 當自定義元素第一次被連線到文件 DOM 時被呼叫
   * connectedCallback
   * https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements#using_the_lifecycle_callbacks
   */
  connectedCallback() {
    if (window.APlayer && window.fetch) {
      this._init()
      this._parse()
    }
  }

  /**
   * 與 connectedCallback 反
   */
  disconnectedCallback() {
    if (!this.lock) {
      this.aplayer.destroy()
    }
  }

  /**
   * 駝峰化
   * @param { string } str
   * @returns { string } str
   */
  _camelize(str) {
    return str
      .replace(/^[_.\- ]+/, '')
      .toLowerCase()
      .replace(/[_.\- ]+(\w|$)/g, (m, p1) => p1.toUpperCase())
  }

  /**
   * 初始化
   */
  _init() {
    let config = {}

    // attributes -> NamedNodeMap
    // https://developer.mozilla.org/zh-CN/docs/Web/API/NamedNodeMap
    for (let i = 0; i < this.attributes.length; i += 1) {
      config[this._camelize(this.attributes[i].name)] = this.attributes[i].value
    }

    let keys = [
      'server',
      'type',
      'id',
      'api',
      'auth',
      'auto',
      'lock',
      'name',
      'title',
      'artist',
      'author',
      'url',
      'cover',
      'pic',
      'lyric',
      'lrc',
    ]

    this.meta = {}

    // 構建 meta
    // config 保留 keys 陣列中沒有的屬性
    // keys 中有 config 中也有的屬性給 meta 賦值,沒有的先設為 undefined
    for (let key of keys) {
      this.meta[key] = config[key]
      delete config[key]
    }

    this.config = config
    this.api =
      this.meta.api ||
      window.meting_api ||
      'https://api.i-meto.com/meting/api?server=:server&type=:type&id=:id&r=:r'

    if (this.meta.auto) this._parse_link()
  }

  /**
   * 解析 auto 屬性的值
   * 將解析後的結果賦值給 meta 物件的 server、type、id
   */
  _parse_link() {
    let rules = [
      ['music.163.com.*song.*id=(\\d+)', 'netease', 'song'],
      ['music.163.com.*album.*id=(\\d+)', 'netease', 'album'],
      ['music.163.com.*artist.*id=(\\d+)', 'netease', 'artist'],
      ['music.163.com.*playlist.*id=(\\d+)', 'netease', 'playlist'],
      ['music.163.com.*discover/toplist.*id=(\\d+)', 'netease', 'playlist'],
      ['y.qq.com.*song/(\\w+).html', 'tencent', 'song'],
      ['y.qq.com.*album/(\\w+).html', 'tencent', 'album'],
      ['y.qq.com.*singer/(\\w+).html', 'tencent', 'artist'],
      ['y.qq.com.*playsquare/(\\w+).html', 'tencent', 'playlist'],
      ['y.qq.com.*playlist/(\\w+).html', 'tencent', 'playlist'],
      ['xiami.com.*song/(\\w+)', 'xiami', 'song'],
      ['xiami.com.*album/(\\w+)', 'xiami', 'album'],
      ['xiami.com.*artist/(\\w+)', 'xiami', 'artist'],
      ['xiami.com.*collect/(\\w+)', 'xiami', 'playlist'],
    ]

    for (let rule of rules) {
      // 返回匹配
      // eg: "https://y.qq.com/n/yqq/song/001RGrEX3ija5X.html"
      // ["y.qq.com/n/yqq/song/001RGrEX3ija5X.html", "001RGrEX3ija5X"]
      let patt = new RegExp(rule[0])
      let res = patt.exec(this.meta.auto)

      if (res !== null) {
        this.meta.server = rule[1]
        this.meta.type = rule[2]
        this.meta.id = res[1]
        return
      }
    }
  }

  /**
   * 對不同 url 僅行處理
   * 生成配置並載入 APlayer
   */
  _parse() {
    if (this.meta.url) {
      // 直接構建 APlayer 配置並載入 APlayer
      let result = {
        name: this.meta.name || this.meta.title || 'Audio name',
        artist: this.meta.artist || this.meta.author || 'Audio artist',
        url: this.meta.url,
        cover: this.meta.cover || this.meta.pic,
        lrc: this.meta.lrc || this.meta.lyric || '',
        type: this.meta.type || 'auto',
      }
      if (!result.lrc) {
        this.meta.lrcType = 0
      }
      if (this.innerText) {
        result.lrc = this.innerText
        this.meta.lrcType = 2
      }
      this._loadPlayer([result])
      return
    }

    // 1. 通過 meta 拼湊介面引數獲得完整介面 (_init 中存放的預設 api)
    // 2. 請求介面,得到播放列表資料
    // 3. 載入 APlayer
    let url = this.api
      .replace(':server', this.meta.server)
      .replace(':type', this.meta.type)
      .replace(':id', this.meta.id)
      .replace(':auth', this.meta.auth)
      .replace(':r', Math.random())

    fetch(url)
      .then(res => res.json())
      .then(result => this._loadPlayer(result))
  }

  _loadPlayer(data) {
    let defaultOption = {
      audio: data,
      mutex: true,
      lrcType: this.meta.lrcType || 3,
      storageName: 'metingjs',
    }

    if (!data.length) return

    let options = {
      ...defaultOption,
      ...this.config,
    }

    //TODO
    for (let optkey in options) {
      if (options[optkey] === 'true' || options[optkey] === 'false') {
        options[optkey] = options[optkey] === 'true'
      }
    }

    let div = document.createElement('div')
    options.container = div

    this.appendChild(div)
    this.aplayer = new APlayer(options)
  }
}

// 建立標籤
// customElements -> https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements
if (window.customElements && !window.customElements.get('meting-js')) {
  window.MetingJSElement = MetingJSElement
  window.customElements.define('meting-js', MetingJSElement)
}

總結

Meting.js 支援載入歌單的的核心在於路徑解析以及通過請求內建介面返回歌單列表資料。關鍵一點, Meting.js 使用了 JavaScript customElements API,可能為了使用者方便,但這導致沒有顯式向外暴露任何 APlayer 例項,對於稍複雜的場景可能無法處理,只能進行魔改。Meting.js 比較適合較簡單的 SPA 應用。關於相容性,IE11 不支援 customElements API。

image

理想情況是:Meting.js 與 APlayer.js 解耦,Meting.js 以工具函式的形式存在,僅處理 url 並丟擲歌單資料,供 APlayer.js 使用。

相關文章