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();
}