/* eslint-disable no-underscore-dangle */ /* eslint-disable no-unused-vars */ /* global getParameterByName forge */ /* global isElectron cookieSet cookieGet cookieRemove async */ class netease { static _create_secret_key(size) { const result = []; const choice = '012345679abcdef'.split(''); for (let i = 0; i < size; i += 1) { const index = Math.floor(Math.random() * choice.length); result.push(choice[index]); } return result.join(''); } static _aes_encrypt(text, sec_key, algo) { const cipher = forge.cipher.createCipher(algo, sec_key); cipher.start({ iv: '0102030405060708' }); cipher.update(forge.util.createBuffer(text)); cipher.finish(); return cipher.output; } static _rsa_encrypt(text, pubKey, modulus) { text = text.split('').reverse().join(''); // eslint-disable-line no-param-reassign const n = new forge.jsbn.BigInteger(modulus, 16); const e = new forge.jsbn.BigInteger(pubKey, 16); const b = new forge.jsbn.BigInteger(forge.util.bytesToHex(text), 16); const enc = b.modPow(e, n).toString(16).padStart(256, '0'); return enc; } static weapi(text) { const modulus = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b72' + '5152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbd' + 'a92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe48' + '75d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7'; const nonce = '0CoJUm6Qyw8W8jud'; const pubKey = '010001'; text = JSON.stringify(text); // eslint-disable-line no-param-reassign const sec_key = this._create_secret_key(16); const enc_text = btoa( this._aes_encrypt( btoa(this._aes_encrypt(text, nonce, 'AES-CBC').data), sec_key, 'AES-CBC' ).data ); const enc_sec_key = this._rsa_encrypt(sec_key, pubKey, modulus); const data = { params: enc_text, encSecKey: enc_sec_key, }; return data; } // refer to https://github.com/Binaryify/NeteaseCloudMusicApi/blob/master/util/crypto.js static eapi(url, object) { const eapiKey = 'e82ckenh8dichen8'; const text = typeof object === 'object' ? JSON.stringify(object) : object; const message = `nobody${url}use${text}md5forencrypt`; const digest = forge.md5 .create() .update(forge.util.encodeUtf8(message)) .digest() .toHex(); const data = `${url}-36cd479b6b5-${text}-36cd479b6b5-${digest}`; return { params: this._aes_encrypt(data, eapiKey, 'AES-ECB').toHex().toUpperCase(), }; } static ne_show_toplist(offset) { if (offset !== undefined && offset > 0) { return { success: (fn) => fn({ result: [] }), }; } const url = 'https://music.163.com/weapi/toplist/detail'; const data = this.weapi({}); return { success: (fn) => { axios.post(url, new URLSearchParams(data)).then((response) => { const result = []; response.data.list.forEach((item) => { const playlist = { cover_img_url: item.coverImgUrl, id: `neplaylist_${item.id}`, source_url: `https://music.163.com/#/playlist?id=${item.id}`, title: item.name, }; result.push(playlist); }); return fn({ result }); }); }, }; } static show_playlist(url) { const order = 'hot'; const offset = getParameterByName('offset', url); const filterId = getParameterByName('filter_id', url); if (filterId === 'toplist') { return this.ne_show_toplist(offset); } let filter = ''; if (filterId !== '') { filter = `&cat=${filterId}`; } let target_url = ''; if (offset != null) { target_url = `https://music.163.com/discover/playlist/?order=${order}${filter}&limit=35&offset=${offset}`; } else { target_url = `https://music.163.com/discover/playlist/?order=${order}${filter}`; } return { success: (fn) => { axios.get(target_url).then((response) => { const { data } = response; const list_elements = Array.from( new DOMParser() .parseFromString(data, 'text/html') .getElementsByClassName('m-cvrlst')[0].children ); const result = list_elements.map((item) => ({ cover_img_url: item.getElementsByTagName('img')[0].src, title: item .getElementsByTagName('div')[0] .getElementsByTagName('a')[0].title, id: `neplaylist_${getParameterByName( 'id', item.getElementsByTagName('div')[0].getElementsByTagName('a')[0] .href )}`, source_url: `https://music.163.com/#/playlist?id=${getParameterByName( 'id', item.getElementsByTagName('div')[0].getElementsByTagName('a')[0] .href )}`, })); return fn({ result, }); }); }, }; } static ne_ensure_cookie(callback) { const domain = 'https://music.163.com'; const nuidName = '_ntes_nuid'; const nnidName = '_ntes_nnid'; cookieGet( { url: domain, name: nuidName, }, (cookie) => { if (cookie == null) { const nuidValue = this._create_secret_key(32); const nnidValue = `${nuidValue},${new Date().getTime()}`; // netease default cookie expire time: 100 years const expire = (new Date().getTime() + 1e3 * 60 * 60 * 24 * 365 * 100) / 1000; cookieSet( { url: domain, name: nuidName, value: nuidValue, expirationDate: expire, }, () => { cookieSet( { url: domain, name: nnidName, value: nnidValue, expirationDate: expire, }, () => { callback(null); } ); } ); } else { callback(null); } } ); } static async_process_list( data_list, handler, handler_extra_param_list, callback ) { const fnDict = {}; data_list.forEach((item, index) => { fnDict[index] = (cb) => handler(index, item, handler_extra_param_list, cb); }); async.parallel(fnDict, (err, results) => callback( null, data_list.map((item, index) => results[index]) ) ); } static ng_render_playlist_result_item(index, item, callback) { const target_url = 'https://music.163.com/weapi/v3/song/detail'; const queryIds = [item.id]; const d = { c: `[${queryIds.map((id) => `{"id":${id}}`).join(',')}]`, ids: `[${queryIds.join(',')}]`, }; const data = this.weapi(d); axios .post(target_url, new URLSearchParams(data).toString()) .then((response) => { const track_json = response.data.songs[0]; const track = { id: `netrack_${track_json.id}`, title: track_json.name, artist: track_json.ar[0].name, artist_id: `neartist_${track_json.ar[0].id}`, album: track_json.al.name, album_id: `nealbum_${track_json.al.id}`, source: 'netease', source_url: `https://music.163.com/#/song?id=${track_json.id}`, img_url: track_json.al.picUrl, // url: `netrack_${track_json.id}`, }; return callback(null, track); }); } static ng_parse_playlist_tracks(playlist_tracks, callback) { const target_url = 'https://music.163.com/weapi/v3/song/detail'; const track_ids = playlist_tracks.map((i) => i.id); const d = { c: `[${track_ids.map((id) => `{"id":${id}}`).join(',')}]`, ids: `[${track_ids.join(',')}]`, }; const data = this.weapi(d); axios.post(target_url, new URLSearchParams(data)).then((response) => { const tracks = response.data.songs.map((track_json) => ({ id: `netrack_${track_json.id}`, title: track_json.name, artist: track_json.ar[0].name, artist_id: `neartist_${track_json.ar[0].id}`, album: track_json.al.name, album_id: `nealbum_${track_json.al.id}`, source: 'netease', source_url: `https://music.163.com/#/song?id=${track_json.id}`, img_url: track_json.al.picUrl, // url: `netrack_${track_json.id}`, })); return callback(null, tracks); }); } static split_array(myarray, size) { const count = Math.ceil(myarray.length / size); const result = []; for (let i = 0; i < count; i += 1) { result.push(myarray.slice(i * size, (i + 1) * size)); } return result; } static ne_get_playlist(url) { // special thanks for @Binaryify // https://github.com/Binaryify/NeteaseCloudMusicApi return { success: (fn) => { const list_id = getParameterByName('list_id', url).split('_').pop(); const target_url = 'https://music.163.com/weapi/v3/playlist/detail'; const d = { id: list_id, offset: 0, total: true, limit: 1000, n: 1000, csrf_token: '', }; const data = this.weapi(d); this.ne_ensure_cookie(() => { axios.post(target_url, new URLSearchParams(data)).then((response) => { const { data: res_data } = response; const info = { id: `neplaylist_${list_id}`, cover_img_url: res_data.playlist.coverImgUrl, title: res_data.playlist.name, source_url: `https://music.163.com/#/playlist?id=${list_id}`, }; const max_allow_size = 1000; const trackIdsArray = this.split_array( res_data.playlist.trackIds, max_allow_size ); function ng_parse_playlist_tracks_wrapper(trackIds, callback) { return netease.ng_parse_playlist_tracks(trackIds, callback); } async.concat( trackIdsArray, ng_parse_playlist_tracks_wrapper, (err, tracks) => { fn({ tracks, info }); } ); // request every tracks to fetch song info // async_process_list(res_data.playlist.trackIds, ng_render_playlist_result_item, // (err, tracks) => fn({ // tracks, // info, // })); }); }); }, }; } static bootstrap_track(track, success, failure) { const sound = {}; const target_url = `https://interface3.music.163.com/eapi/song/enhance/player/url`; let song_id = track.id; const eapiUrl = '/api/song/enhance/player/url'; song_id = song_id.slice('netrack_'.length); const d = { ids: `[${song_id}]`, br: 999000, }; const data = this.eapi(eapiUrl, d); const expire = (new Date().getTime() + 1e3 * 60 * 60 * 24 * 365 * 100) / 1000; cookieSet( { url: 'https://interface3.music.163.com', name: 'os', value: 'pc', expirationDate: expire, }, (cookie) => { axios.post(target_url, new URLSearchParams(data)).then((response) => { const { data: res_data } = response; const { url, br } = res_data.data[0]; if (url != null) { sound.url = url; const bitrate = `${(br / 1000).toFixed(0)}kbps`; sound.bitrate = bitrate; sound.platform = 'netease'; success(sound); } else { failure(sound); } }); } ); } static is_playable(song) { return song.fee !== 4 && song.fee !== 1; } static search(url) { // use chrome extension to modify referer. const target_url = 'https://music.163.com/api/search/pc'; const keyword = getParameterByName('keywords', url); const curpage = getParameterByName('curpage', url); const searchType = getParameterByName('type', url); let ne_search_type = '1'; if (searchType === '1') { ne_search_type = '1000'; } const req_data = { s: keyword, offset: 20 * (curpage - 1), limit: 20, type: ne_search_type, }; return { success: (fn) => { axios .post(target_url, new URLSearchParams(req_data)) .then((response) => { const { data } = response; let result = []; let total = 0; if (searchType === '0') { result = data.result.songs.map((song_info) => ({ id: `netrack_${song_info.id}`, title: song_info.name, artist: song_info.artists[0].name, artist_id: `neartist_${song_info.artists[0].id}`, album: song_info.album.name, album_id: `nealbum_${song_info.album.id}`, source: 'netease', source_url: `https://music.163.com/#/song?id=${song_info.id}`, img_url: song_info.album.picUrl, // url: `netrack_${song_info.id}`, url: !this.is_playable(song_info) ? '' : undefined, })); total = data.result.songCount; } else if (searchType === '1') { result = data.result.playlists.map((info) => ({ id: `neplaylist_${info.id}`, title: info.name, source: 'netease', source_url: `https://music.163.com/#/playlist?id=${info.id}`, img_url: info.coverImgUrl, url: `neplaylist_${info.id}`, author: info.creator.nickname, count: info.trackCount, })); total = data.result.playlistCount; } return fn({ result, total, type: searchType, }); }) .catch(() => fn({ result: [], total: 0, type: searchType, }) ); }, }; } static ne_album(url) { const album_id = getParameterByName('list_id', url).split('_').pop(); // use chrome extension to modify referer. const target_url = `https://music.163.com/api/album/${album_id}`; return { success: (fn) => { axios.get(target_url).then((response) => { const { data } = response; const info = { cover_img_url: data.album.picUrl, title: data.album.name, id: `nealbum_${data.album.id}`, source_url: `https://music.163.com/#/album?id=${data.album.id}`, }; const tracks = data.album.songs.map((song_info) => ({ id: `netrack_${song_info.id}`, title: song_info.name, artist: song_info.artists[0].name, artist_id: `neartist_${song_info.artists[0].id}`, album: song_info.album.name, album_id: `nealbum_${song_info.album.id}`, source: 'netease', source_url: `https://music.163.com/#/song?id=${song_info.id}`, img_url: song_info.album.picUrl, url: !this.is_playable(song_info) ? '' : undefined, })); return fn({ tracks, info, }); }); }, }; } static ne_artist(url) { const artist_id = getParameterByName('list_id', url).split('_').pop(); // use chrome extension to modify referer. const target_url = `https://music.163.com/api/artist/${artist_id}`; return { success: (fn) => { axios.get(target_url).then((response) => { const { data } = response; const info = { cover_img_url: data.artist.picUrl, title: data.artist.name, id: `neartist_${data.artist.id}`, source_url: `https://music.163.com/#/artist?id=${data.artist.id}`, }; const tracks = data.hotSongs.map((song_info) => ({ id: `netrack_${song_info.id}`, title: song_info.name, artist: song_info.artists[0].name, artist_id: `neartist_${song_info.artists[0].id}`, album: song_info.album.name, album_id: `nealbum_${song_info.album.id}`, source: 'netease', source_url: `https://music.163.com/#/song?id=${song_info.id}`, img_url: song_info.album.picUrl, // url: `netrack_${song_info.id}`, url: !this.is_playable(song_info) ? '' : undefined, })); return fn({ tracks, info, }); }); }, }; } static lyric(url) { const track_id = getParameterByName('track_id', url).split('_').pop(); // use chrome extension to modify referer. const target_url = 'https://music.163.com/weapi/song/lyric?csrf_token='; const csrf = ''; const d = { id: track_id, lv: -1, tv: -1, csrf_token: csrf, }; const data = this.weapi(d); return { success: (fn) => { axios.post(target_url, new URLSearchParams(data)).then((response) => { const { data: res_data } = response; let lrc = ''; let tlrc = ''; if (res_data.lrc != null) { lrc = res_data.lrc.lyric; } if (res_data.tlyric != null && res_data.tlyric.lyric != null) { // eslint-disable-next-line no-control-regex tlrc = res_data.tlyric.lyric.replace(/(|\\)/g, ''); tlrc = tlrc.replace(/[\u2005]+/g, ' '); } return fn({ lyric: lrc, tlyric: tlrc, }); }); }, }; } static parse_url(url) { let result; let id = ''; // eslint-disable-next-line no-param-reassign url = url.replace( 'music.163.com/#/discover/toplist?', 'music.163.com/#/playlist?' ); // eslint-disable-line no-param-reassign url = url.replace('music.163.com/#/my/m/music/', 'music.163.com/'); // eslint-disable-line no-param-reassign url = url.replace('music.163.com/#/m/', 'music.163.com/'); // eslint-disable-line no-param-reassign url = url.replace('music.163.com/#/', 'music.163.com/'); // eslint-disable-line no-param-reassign if (url.search('//music.163.com/playlist') !== -1) { const match = /\/\/music.163.com\/playlist\/([0-9]+)/.exec(url); id = match ? match[1] : getParameterByName('id', url); result = { type: 'playlist', id: `neplaylist_${id}`, }; } else if (url.search('//music.163.com/artist') !== -1) { result = { type: 'playlist', id: `neartist_${getParameterByName('id', url)}`, }; } else if (url.search('//music.163.com/album') !== -1) { const match = /\/\/music.163.com\/album\/([0-9]+)/.exec(url); id = match ? match[1] : getParameterByName('id', url); result = { type: 'playlist', id: `nealbum_${id}`, }; } return { success: (fn) => { fn(result); }, }; } static get_playlist(url) { const list_id = getParameterByName('list_id', url).split('_')[0]; switch (list_id) { case 'neplaylist': return this.ne_get_playlist(url); case 'nealbum': return this.ne_album(url); case 'neartist': return this.ne_artist(url); default: return null; } } static get_playlist_filters() { const recommend = [ { id: '', name: '全部' }, { id: 'toplist', name: '排行榜' }, { id: '流行', name: '流行' }, { id: '民谣', name: '民谣' }, { id: '电子', name: '电子' }, { id: '舞曲', name: '舞曲' }, { id: '说唱', name: '说唱' }, { id: '轻音乐', name: '轻音乐' }, { id: '爵士', name: '爵士' }, { id: '乡村', name: '乡村' }, ]; const all = [ { category: '语种', filters: [ { id: '华语', name: '华语' }, { id: '欧美', name: '欧美' }, { id: '日语', name: '日语' }, { id: '韩语', name: '韩语' }, { id: '粤语', name: '粤语' }, ], }, { category: '风格', filters: [ { id: '流行', name: '流行' }, { id: '民谣', name: '民谣' }, { id: '电子', name: '电子' }, { id: '舞曲', name: '舞曲' }, { id: '说唱', name: '说唱' }, { id: '轻音乐', name: '轻音乐' }, { id: '爵士', name: '爵士' }, { id: '乡村', name: '乡村' }, { id: 'R%26B%2FSoul', name: 'R&B/Soul' }, { id: '古典', name: '古典' }, { id: '民族', name: '民族' }, { id: '英伦', name: '英伦' }, { id: '金属', name: '金属' }, { id: '朋克', name: '朋克' }, { id: '蓝调', name: '蓝调' }, { id: '雷鬼', name: '雷鬼' }, { id: '世界音乐', name: '世界音乐' }, { id: '拉丁', name: '拉丁' }, { id: 'New Age', name: 'New Age' }, { id: '古风', name: '古风' }, { id: '后摇', name: '后摇' }, { id: 'Bossa Nova', name: 'Bossa Nova' }, ], }, { category: '场景', filters: [ { id: '清晨', name: '清晨' }, { id: '夜晚', name: '夜晚' }, { id: '学习', name: '学习' }, { id: '工作', name: '工作' }, { id: '午休', name: '午休' }, { id: '下午茶', name: '下午茶' }, { id: '地铁', name: '地铁' }, { id: '驾车', name: '驾车' }, { id: '运动', name: '运动' }, { id: '旅行', name: '旅行' }, { id: '散步', name: '散步' }, { id: '酒吧', name: '酒吧' }, ], }, { category: '情感', filters: [ { id: '怀旧', name: '怀旧' }, { id: '清新', name: '清新' }, { id: '浪漫', name: '浪漫' }, { id: '伤感', name: '伤感' }, { id: '治愈', name: '治愈' }, { id: '放松', name: '放松' }, { id: '孤独', name: '孤独' }, { id: '感动', name: '感动' }, { id: '兴奋', name: '兴奋' }, { id: '快乐', name: '快乐' }, { id: '安静', name: '安静' }, { id: '思念', name: '思念' }, ], }, { category: '主题', filters: [ { id: '综艺', name: '综艺' }, { id: '影视原声', name: '影视原声' }, { id: 'ACG', name: 'ACG' }, { id: '儿童', name: '儿童' }, { id: '校园', name: '校园' }, { id: '游戏', name: '游戏' }, { id: '70后', name: '70后' }, { id: '80后', name: '80后' }, { id: '90后', name: '90后' }, { id: '网络歌曲', name: '网络歌曲' }, { id: 'KTV', name: 'KTV' }, { id: '经典', name: '经典' }, { id: '翻唱', name: '翻唱' }, { id: '吉他', name: '吉他' }, { id: '钢琴', name: '钢琴' }, { id: '器乐', name: '器乐' }, { id: '榜单', name: '榜单' }, { id: '00后', name: '00后' }, ], }, ]; return { success: (fn) => fn({ recommend, all }), }; } static login(url) { // use chrome extension to modify referer. let target_url = 'https://music.163.com/weapi/login'; const loginType = getParameterByName('type', url); const password = getParameterByName('password', url); let req_data = {}; if (loginType === 'email') { const email = getParameterByName('email', url); req_data = { username: email, password: forge.md5 .create() .update(forge.util.encodeUtf8(password)) .digest() .toHex(), rememberLogin: 'true', }; } else if (loginType === 'phone') { target_url = `https://music.163.com/weapi/login/cellphone`; const countrycode = getParameterByName('countrycode', url); const phone = getParameterByName('phone', url); req_data = { phone, countrycode, password: forge.md5 .create() .update(forge.util.encodeUtf8(password)) .digest() .toHex(), rememberLogin: 'true', }; } const encrypt_req_data = this.weapi(req_data); const expire = (new Date().getTime() + 1e3 * 60 * 60 * 24 * 365 * 100) / 1000; cookieSet( { url: 'https://music.163.com', name: 'os', value: 'pc', expirationDate: expire, }, (cookie) => {} ); return { success: (fn) => { axios .post(target_url, new URLSearchParams(encrypt_req_data)) .then((response) => { const { data } = response; const result = { is_login: true, user_id: data.account.id, user_name: data.account.userName, nickname: data.profile.nickname, avatar: data.profile.avatarUrl, platform: 'netease', data, }; return fn({ status: 'success', data: result, }); }) .catch(() => fn({ status: 'fail', data: {}, }) ); }, }; } static get_user_playlist(url, playlistType) { const user_id = getParameterByName('user_id', url); const target_url = 'https://music.163.com/api/user/playlist'; const req_data = { uid: user_id, limit: 1000, offset: 0, includeVideo: true, }; return { success: (fn) => { axios .post(target_url, new URLSearchParams(req_data)) .then((response) => { const playlists = []; response.data.playlist.forEach((item) => { if (playlistType === 'created' && item.subscribed !== false) { return; } if (playlistType === 'favorite' && item.subscribed !== true) { return; } const playlist = { cover_img_url: item.coverImgUrl, id: `neplaylist_${item.id}`, source_url: `https://music.163.com/#/playlist?id=${item.id}`, title: item.name, }; playlists.push(playlist); }); return fn({ status: 'success', data: { playlists, }, }); }); }, }; } static get_user_created_playlist(url) { return this.get_user_playlist(url, 'created'); } static get_user_favorite_playlist(url) { return this.get_user_playlist(url, 'favorite'); } static get_recommend_playlist() { const target_url = 'https://music.163.com/weapi/personalized/playlist'; const req_data = { limit: 30, total: true, n: 1000, }; const encrypt_req_data = this.weapi(req_data); return { success: (fn) => { axios .post(target_url, new URLSearchParams(encrypt_req_data)) .then((response) => { const playlists = []; response.data.result.forEach((item) => { const playlist = { cover_img_url: item.picUrl, id: `neplaylist_${item.id}`, source_url: `https://music.163.com/#/playlist?id=${item.id}`, title: item.name, }; playlists.push(playlist); }); return fn({ status: 'success', data: { playlists, }, }); }); }, }; } static get_user() { const url = `https://music.163.com/api/nuser/account/get`; const encrypt_req_data = this.weapi({}); return { success: (fn) => { axios.post(url, new URLSearchParams(encrypt_req_data)).then((res) => { let result = { is_login: false }; let status = 'fail'; if (res.data.account !== null) { status = 'success'; const { data } = res; result = { is_login: true, user_id: data.account.id, user_name: data.account.userName, nickname: data.profile.nickname, avatar: data.profile.avatarUrl, platform: 'netease', data, }; } return fn({ status, data: result, }); }); }, }; } static get_login_url() { return `https://music.163.com/#/login`; } static logout() { cookieRemove( { url: 'https://music.163.com', name: 'MUSIC_U', }, (cookie) => {} ); } }