wxt2005
8/3/2017 - 10:20 AM

NetEase Music Like Button

NetEase Music Like Button

{
  class NetEaseMusicFucker {
    constructor() {
      this.emj = {
        色: "00e0b",
        流感: "509f6",
        这边: "259df",
        弱: "8642d",
        嘴唇: "bc356",
        亲: "62901",
        开心: "477df",
        呲牙: "22677",
        憨笑: "ec152",
        猫: "b5ff6",
        皱眉: "8ace6",
        幽灵: "15bb7",
        蛋糕: "b7251",
        发怒: "52b3a",
        大哭: "b17a8",
        兔子: "76aea",
        星星: "8a5aa",
        钟情: "76d2e",
        牵手: "41762",
        公鸡: "9ec4e",
        爱意: "e341f",
        禁止: "56135",
        狗: "fccf6",
        亲亲: "95280",
        叉: "104e0",
        礼物: "312ec",
        晕: "bda92",
        呆: "557c9",
        生病: "38701",
        钻石: "14af6",
        拜: "c9d05",
        怒: "c4f7f",
        示爱: "0c368",
        汗: "5b7a4",
        小鸡: "6bee2",
        痛苦: "55932",
        撇嘴: "575cc",
        惶恐: "e10b4",
        口罩: "24d81",
        吐舌: "3cfe4",
        心碎: "875d3",
        生气: "e8204",
        可爱: "7b97d",
        鬼脸: "def52",
        跳舞: "741d5",
        男孩: "46b8e",
        奸笑: "289dc",
        猪: "6935b",
        圈: "3ece0",
        便便: "462db",
        外星: "0a22b",
        圣诞: "8e7",
        流泪: "01000",
        强: "1",
        爱心: "0CoJU",
        女孩: "m6Qyw",
        惊恐: "8W8ju",
        大笑: "d"
      };

      this.emjStr = [
        ["流泪", "强"],
        [
          "色",
          "流感",
          "这边",
          "弱",
          "嘴唇",
          "亲",
          "开心",
          "呲牙",
          "憨笑",
          "猫",
          "皱眉",
          "幽灵",
          "蛋糕",
          "发怒",
          "大哭",
          "兔子",
          "星星",
          "钟情",
          "牵手",
          "公鸡",
          "爱意",
          "禁止",
          "狗",
          "亲亲",
          "叉",
          "礼物",
          "晕",
          "呆",
          "生病",
          "钻石",
          "拜",
          "怒",
          "示爱",
          "汗",
          "小鸡",
          "痛苦",
          "撇嘴",
          "惶恐",
          "口罩",
          "吐舌",
          "心碎",
          "生气",
          "可爱",
          "鬼脸",
          "跳舞",
          "男孩",
          "奸笑",
          "猪",
          "圈",
          "便便",
          "外星",
          "圣诞"
        ],
        ["爱心", "女孩", "惊恐", "大笑"]
      ];

      this.favListId = null;
      this.currentPlayingId = null;
      this.favedTracks = [];
      this.favButton = null;
      this.playbar = null;
      this.playbarObserver = null;
    }

    /**
     * Initialize
     * @return {Promise}
     */
    init() {
      this.playbar = document.querySelector('.m-playbar');
      this.syncCurrentPlayingId();
      this.listenPlaybar(() => {
        this.syncCurrentPlayingId();
        this.syncFavButton();
      });

      return Promise.resolve()
        .then(() => this.getPlayList())
        .then(playlist => playlist.find(item => item.specialType === 5))
        .then(({ id }) => (this.favListId = id))
        .then(() =>  this.getFavedTracks())
        .then(tracks => this.favedTracks = tracks)
        .then(() => this.injectFavButton());
    }

    /**
     * sync current playing id
     * @return {Number} track id
     */
    syncCurrentPlayingId() {
      this.currentPlayingId = this.getCurrentPlayingId();
      return this.currentPlayingId;
    }

    /**
     * Listen for mutaion of player
     */
    listenPlaybar(callback) {
      const target = this.playbar.querySelector('.words');
      const observer = new MutationObserver(() => callback());

      const config = { attributes: true, childList: true, characterData: true, subtree: true };

      observer.observe(target, config);

      this.playbarObserver = observer;
    }

    /**
   * Conver emoji string array to code string
   * @param  {Array}  [emjArr=[]] emoji string array
   * @return {String}             code string
   */
    convertEmjStr(emjArr = []) {
      if (!Array.isArray(emjArr)) {
        throw new TypeError(
          `emjArr should be an Array instead of ${typeof emjArr}`
        );
      }

      return emjArr.map(emj => this.emj[emj]).join("");
    }

    /**
   * Get csrf token from cookies
   * @return {String} csrf token
   */
    getCsrfToken() {
      const REGEXP = /__csrf=(.+?)(?:;|$)/;
      const result = REGEXP.exec(document.cookie);

      if (!result || !result[1]) {
        throw new Error("Get CSRF Token from cookies failed!");
      }

      return result[1];
    }

    /**
   * Encode request data
   * @param  {Any} data data
   * @return {Object}   encoded data as object
   */
    encodeRequestData(data) {
      if (typeof window.asrsea !== "function") {
        throw new Error("window.asrsea is not exist! we're all gonna die!");
      }

      const encodeParams = this.emjStr.map(this.convertEmjStr.bind(this));

      return window.asrsea(data, ...encodeParams);
    }

    /**
   * Request method
   * @param  {String} url      request url
   * @param  {Object} [data={}] request data
   * @return {Promise}
   */
    request({ url, data = {} } = {}) {
      if (typeof url !== "string") {
        throw new TypeError(`Url should be string, instead got ${typeof url}`);
      }

      if (!url) {
        throw new TypeError("Url should not be empty");
      }

      const csrfToken = this.getCsrfToken();

      data.csrf_token = csrfToken;

      // console.log(data);

      const encodedData = this.encodeRequestData(JSON.stringify(data));

      const myHeaders = new Headers();
      myHeaders.append("Content-Type", "application/x-www-form-urlencoded");

      url += (url.indexOf("?") === -1 ? "?" : "&") + `csrf_token=${csrfToken}`;

      const request = new Request(url, {
        method: "POST",
        body: `params=${encodeURIComponent(
          encodedData.encText
        )}&encSecKey=${encodeURIComponent(encodedData.encSecKey)}`,
        headers: myHeaders,
        credentials: "same-origin"
      });

      return fetch(request);
    }

    /**
   * Add fav
   * @param {Number} trackId track id
   * @param {String} operation "del" or "add"
   */
    manipulateFav(trackId, operation) {
      if (!this.favListId) {
        throw new Error("Fav list id is not provied");
      }

      if (operation !== 'add' && operation !== 'del') {
        throw new TypeError('Operation should be "add" or "del"');
      }

      const url = "http://music.163.com/weapi/playlist/manipulate/tracks";
      const data = {
        op: operation,
        pid: `${this.favListId}`,
        trackIds: `[${trackId}]`,
        tracks: "[object Object]"
      };

      return this.request({
        url,
        data
      })
      .then(response => response.json())
      .then(json => {
        if (operation === 'add') {
          this.favedTracks.unshift(trackId);
        } else {
          this.favedTracks = this.favedTracks.filter(id => id !== trackId);
        }

        return json;
      });
    }

    /**
   * Manipulate fav for current playing track
   */
    manipulateFavForCurrentTrack(operation) {
      if (!this.currentPlayingId) {
        throw new Error("Cannot found song id");
      }

      if (operation !== 'add' && operation !== 'del') {
        throw new TypeError('Operation should be "add" or "del"');
      }

      const manipulateText = (operation === 'add') ? '添加' : '取消';

      return this.manipulateFav(this.currentPlayingId, operation).then(({ code }) => {
        if (code !== 200) {
          console.info(`${manipulateText}收藏失败`);
        } else {
          console.info(`${manipulateText}收藏成功`);
        }
      });
    }

    /**
     * Get current playing id
     * @return {Number} track id
     */
    getCurrentPlayingId() {
      const ele = this.playbar.querySelector(".play .name");
      if (!ele || ele.tagName.toLowerCase() !== "a") {
        return null;
      }

      const REGEXP = /id=(\d+?)(?:&|$)/;
      const result = REGEXP.exec(ele);

      if (!result || !result[1]) {
        return null;
      }

      return parseInt(result[1], 10);
    }

    /**
   * Get playlist for user
   * @return {Promise}
   */
    getPlayList() {
      const url = "http://music.163.com/weapi/user/playlist";
      const data = {
        limit: "1001",
        offset: "0",
        uid: `${window.GAccount.id}`
      };

      return this.request({
        url,
        data
      })
        .then(response => response.json())
        .then(({ code, playlist }) => {
          if (code !== 200) {
            throw new Error("Get play list failed");
          }

          return playlist;
        });
    }

    /**
     * Get faved track ids
     * @return {Promise}
     */
    getFavedTracks() {
      if (!this.favListId) {
        throw new Error("Fav list id is not provied");
      }

      const url = 'http://music.163.com/weapi/v3/playlist/detail';
      const data = {
        id: `${this.favListId}`,
        limit: '1000',
        n: '1000',
        offset: '0',
        total: 'true'
      };

      return this.request({
        url,
        data
      })
        .then(response => response.json())
        .then(({ code, playlist }) => {
          if (code !== 200) {
            throw new Error("Get fav track failed");
          }

          return playlist.trackIds.map(track => track.id);
        });
    }

    /**
   * Add fav button to DOM
   */
    injectFavButton() {
      const container = this.playbar.querySelector(".words");
      const button = document.createElement("a");
      const isFaved = this.isTrackFaved(this.currentPlayingId);
      const buttonText = isFaved ? '已喜欢' : '喜欢';

      button.innerText = buttonText;
      button.setAttribute("href", "#");
      Object.assign(button.style, {
        color: "#fff",
        position: "absolute",
        right: "32px",
        top: "0",
        lineHeight: "28px"
      });

      button.addEventListener("click", event => {
        event.preventDefault();
        const isFaved = this.isTrackFaved(this.currentPlayingId);
        this.manipulateFavForCurrentTrack(isFaved ? 'del' : 'add')
          .then(() => this.syncFavButton());
      });

      this.favButton = button;

      container.insertAdjacentElement("afterend", this.favButton);
    }

    /**
     * Sync fav button status
     */
    syncFavButton() {
      if (!this.favButton) {
        this.injectFavButton();
      }

      const isFaved = this.isTrackFaved(this.currentPlayingId);
      const buttonText = isFaved ? '已喜欢' : '喜欢';

      this.favButton.innerText = buttonText;
    }

    /**
     * Is track faved
     * @param  {Number}  trackId track id
     * @return {Boolean}
     */
    isTrackFaved(trackId) {
      if (!trackId) {
        return false;
      }

      return !!this.favedTracks.find(id => trackId == id);
    }
  }

  const instance = new NetEaseMusicFucker();
  // init
  instance.init();
}