- /* eslint-disable no-multi-spaces */
- /* eslint-disable dot-notation */
-
- // ==UserScript==
- // @name 网易云音乐-MyFreeMP3扩展
- // @name:zh-CN 网易云音乐-MyFreeMP3扩展
- // @name:en Netease Music - MyFreeMP3 Extender
- // @namespace 163Music-MyFreeMP3-Extender
- // @version 1.0
- // @description 利用MyFreeMP3扩展网易云音乐功能
- // @description:zh-CN 利用MyFreeMP3扩展网易云音乐功能
- // @description:en Extend netease music with MyFreeMP3
- // @author PY-DNG
- // @license GPL-v3
- // @match http*://music.163.com/*
- // @connect 59.110.45.28
- // @connect music.163.net
- // @connect music.126.net
- // @icon https://s1.music.126.net/style/favicon.ico
- // @grant GM_xmlhttpRequest
- // @grant GM_download
- // @run-at document-start
- // @noframes
- // ==/UserScript==
-
- (function __MAIN__() {
- 'use strict';
- const CONST = {
- Text: {
- V5NOCANQU: '要听龙叔的话,V5不能屈,听完歌快去给你网易爸爸充VIP吧'
- },
- Number: {
- Interval_Fastest: 1,
- Interval_Fast: 50,
- Interval_Balanced: 500,
- MaxSearchPage: 3,
- }
- }
-
- // Prepare
- const md5Script = document.createElement('script');
- md5Script.src = 'https://cdn.bootcdn.net/ajax/libs/blueimp-md5/2.18.0/js/md5.js';
- document.head.appendChild(md5Script);
-
- // Arguments: level=LogLevel.Info, logContent, asObject=false
- // Needs one call "DoLog();" to get it initialized before using it!
- function DoLog() {
- const win = typeof unsafeWindow === 'object' ? unsafeWindow : window;
-
- // Global log levels set
- win.LogLevel = {
- None: 0,
- Error: 1,
- Success: 2,
- Warning: 3,
- Info: 4,
- }
- win.LogLevelMap = {};
- win.LogLevelMap[LogLevel.None] = {prefix: '' , color: 'color:#ffffff'}
- win.LogLevelMap[LogLevel.Error] = {prefix: '[Error]' , color: 'color:#ff0000'}
- win.LogLevelMap[LogLevel.Success] = {prefix: '[Success]' , color: 'color:#00aa00'}
- win.LogLevelMap[LogLevel.Warning] = {prefix: '[Warning]' , color: 'color:#ffa500'}
- win.LogLevelMap[LogLevel.Info] = {prefix: '[Info]' , color: 'color:#888888'}
- win.LogLevelMap[LogLevel.Elements] = {prefix: '[Elements]', color: 'color:#000000'}
-
- // Current log level
- DoLog.logLevel = (win.isPY_DNG && win.userscriptDebugging) ? LogLevel.Info : LogLevel.Warning; // Info Warning Success Error
-
- // Log counter
- DoLog.logCount === undefined && (DoLog.logCount = 0);
- if (++DoLog.logCount > 512) {
- console.clear();
- DoLog.logCount = 0;
- }
-
- // Get args
- let level, logContent, asObject;
- switch (arguments.length) {
- case 1:
- level = LogLevel.Info;
- logContent = arguments[0];
- asObject = false;
- break;
- case 2:
- level = arguments[0];
- logContent = arguments[1];
- asObject = false;
- break;
- case 3:
- level = arguments[0];
- logContent = arguments[1];
- asObject = arguments[2];
- break;
- default:
- level = LogLevel.Info;
- logContent = 'DoLog initialized.';
- asObject = false;
- break;
- }
-
- // Log when log level permits
- if (level <= DoLog.logLevel) {
- let msg = '%c' + LogLevelMap[level].prefix;
- let subst = LogLevelMap[level].color;
-
- if (asObject) {
- msg += ' %o';
- } else {
- switch(typeof(logContent)) {
- case 'string': msg += ' %s'; break;
- case 'number': msg += ' %d'; break;
- case 'object': msg += ' %o'; break;
- }
- }
-
- console.log(msg, subst, logContent);
- }
- }
- DoLog();
-
- main();
- function main() {
- // Wait for document.body
- if (!document.body) {
- setTimeout(main, CONST.Number.Interval_Fast);
- return false;
- }
-
- // Commons
- hookPlay();
-
- // Page functions
- const ITM = new IntervalTaskManager();
- const pageChangeDetecter = (function(callback, emitOnInit=false) {
- let href = location.href;
- emitOnInit && callback(null, href);
- return function detecter() {
- const new_href = location.href;
- if (href !== new_href) {
- callback(href, new_href);
- href = new_href;
- }
- }
- }) (deliverPageFuncs, true);
- ITM.time = CONST.Number.Interval_Fast;
- ITM.addTask(pageChangeDetecter);
- ITM.start();
-
- function deliverPageFuncs(href, new_href) {
- const pageFuncs = [{
- reg: /^https?:\/\/music\.163\.com\/#\/song\?.+$/,
- func: pageSong,
- checker: function() {
- const ifr = $('#g_iframe'); if (!ifr) {return false;}
- const oDoc = ifr.contentDocument; if (!oDoc) {return false;}
- const elm = !!$(oDoc, '.cnt>.m-info');
- return elm;
- }
- },{
- reg: /^https?:\/\/music\.163\.com\/#\/(artist|album|discover\/toplist)\?.+$/,
- func: replacePredata,
- sync: false
- },{
- reg: /^https?:\/\/music\.163\.com\//,
- func: listDownload,
- checker: function() {
- const ifr = $('#g_iframe'); if (!ifr) {return false;}
- const oDoc = ifr.contentDocument; if (!oDoc) {return false;}
- return !!oDoc.body;
- }
- },{
- reg: /^https?:\/\/music\.163\.com\//,
- func: playlistDownload
- },{
- reg: /^https?:\/\/music\.163\.com\/#\/album\?.+$/,
- func: pageAlbum,
- checker: function() {
- const ifr = $('#g_iframe'); if (!ifr) {return false;}
- const oDoc = ifr.contentDocument; if (!oDoc) {return false;}
- const elm = !!$(oDoc, '#content-operation');
- return elm;
- }
- }];
- for (const pageFunc of pageFuncs) {
- test_exec(pageFunc);
- }
-
- function test_exec(pageFunc) {
- pageFunc.reg.test(location.href) && ((((pageFunc.sync || !pageFunc.hasOwnProperty('sync')) ? iframeDocSync() : true) && (pageFunc.checker ? ({
- 'string': () => ($(pageFunc.checker)),
- 'function': pageFunc.checker,
- })[typeof pageFunc.checker]() : true)) ? true : (setTimeout(test_exec.bind(null, pageFunc), CONST.Number.Interval_Balanced), DoLog('waiting: ' + location.href), false)) && pageFunc.func(href, new_href);
- }
- }
- }
-
- function hookPlay() {
- // Play
- try {
- const RATES = {
- 'none': 0,
- 'standard': 128000,
- 'lossless': 999000,
- };
- const APIH = new APIHooker();
-
- let dlLevel, dlRate, plLevel, plRate;
- APIH.hook(/^https?:\/\/music\.163\.com\/weapi\/v3\/song\/detail(\?[a-zA-Z0-9=_]+)?$/, function(xhr) {
- const json = JSON.parse(xhr.response);
- const privilege = json['privileges'][0];
- dlLevel = privilege['downloadMaxBrLevel'];
- dlRate = RATES[dlLevel];
- plLevel = privilege['playMaxBrLevel'];
- plRate = RATES[plLevel];
- privilege['dlLevel'] = dlLevel;
- privilege['dl'] = dlRate;
- privilege['plLevel'] = plLevel;
- privilege['pl'] = plRate;
- const response = JSON.stringify(json)
- const propDesc = {
- value: response,
- writable: false,
- configurable: false,
- enumerable: true
- }
- Object.defineProperties(xhr, {
- 'response': propDesc,
- 'responseText': propDesc
- })
- return true;
- });
- APIH.hook(/^\/weapi\/song\/enhance\/player\/url\/v1(\?[a-zA-Z0-9=_]+)?$/, function(xhr, _this, args, onreadystatechange) {
- const ifr = $('#g_iframe');
- const oDoc = ifr.contentDocument;
-
- // Get data
- const json = JSON.parse(xhr.response);
- const data = json['data'][0];
- const name = $('.play a.name').innerText;
- const artist = $('.play .by>span').children[0].innerText;
- const fname = replaceText('{$NAME} - {$ARTIST}', {'{$NAME}': name, '{$ARTIST}': artist});
- const cover = $('.m-playbar .head img').src;
- const cpath = getUrlPath(cover);
-
- // Only hook unplayable songs
- if (data['url']) {return true};
-
- search({
- text: fname,
- callback: function(s_json) {
- const list = s_json.data.list;
- const song = list.find(function(song) {
- // Search result
- const qualities = [2000, 320, 128];
- const q = qualities.find((q) => (song.quality.includes(q)));
- const s_url = song[({
- 2000: 'url_flac',
- 320: 'url_320',
- 128: 'url_128'
- })[q]];
- const s_ftype = ({
- 2000: 'flac',
- 320: 'mp3',
- 128: 'mp3'
- })[q];
- const s_lrc = song.lrc;
- const s_cover = song.cover;
- const s_name = song.name;
- const s_artist = song.artist;
- const s_fname = name + ' - ' + artist;
- const s_cpath = getUrlPath(s_cover);
-
- if (s_cpath === cpath) {
- // Song found, request final url
- song.url = s_url;
- return true;
- }
- }) || list[0];
- const abort = GM_xmlhttpRequest({
- method: 'GET',
- url: song.url,
- onprogress: function(e) {
- abort();
- // modify xhr and continue stack
- data['code'] = 200;
- data['br'] = plRate;
- data['level'] = plLevel;
- data['type'] = 'mp3';
- data['url'] = e.finalUrl;
- const response = JSON.stringify(json);
- const propDesc = {
- value: response,
- writable: false,
- configurable: true,
- enumerable: true
- };
- Object.defineProperties(xhr, {
- 'response': propDesc,
- 'responseText': propDesc
- });
- continueStack();
- }
- }).abort
- },
- });
-
- // Suspend stack until search & find the song
- return false;
-
- function continueStack() {
- onreadystatechange.apply(_this, args);;
- }
- });
- } catch (err) {
- console.error(err);
- DoLog(LogLevel.Error, 'hooking error');
- }
- }
-
- function listDownload() {
- const iframe = $('#g_iframe');
- const oDoc = iframe.contentDocument;
- const body = oDoc.body;
- if (!body) {
- DoLog(LogLevel.Warning, 'listDownload: list not found');
- return false;
- }
-
- const AEL = getPureAEL();
- AEL.call(body, 'click', function(e) {
- const elm = e.target;
- if (elm.getAttribute('data-res-action') === 'download') {
- e.stopPropagation();
- const elm_share = elm.previousElementSibling;
-
- const name = elm_share.getAttribute('data-res-name');
- const artist = elm_share.getAttribute('data-res-author');
- const cover = elm_share.getAttribute('data-res-pic');
- downloadSong(name, artist, cover);
- }
- }, {capture: true});
- }
-
- function playlistDownload() {
- const AEL = getPureAEL();
- AEL.call(document.body, 'click', function(e) {
- const elm = e.target;
- if (elm.getAttribute('data-action') === 'download') {
- e.stopPropagation();
- const li = elm.parentElement.parentElement.parentElement;
-
- const name = $(li, '.col-2').innerText;
- const artist = $(li, '.col-4').innerText;
- downloadSong(name, artist);
- }
- }, {capture: true});
- }
-
- function pageSong() {
- const ifr = $('#g_iframe');
- const oDoc = ifr.contentDocument;
- const name = $(oDoc, '.tit>em').innerText;
- const artist = $(oDoc, '.cnt>.des>span>a').innerText;
- const cover = $(oDoc, '.u-cover>img.j-img').src;
- const AEL = getPureAEL();
-
- // GUI
- if ($(oDoc, '.vip-song')) {
- const content_operation = $(oDoc, '#content-operation');
- const vip_group = $(content_operation, '.u-vip-btn-group');
- const vip_play = $(vip_group, 'a[data-res-action="play"]');
- const vip_add = $(vip_group, 'a[data-res-action="addto"]');
-
- // Style
- vip_play.classList.remove('u-btni-vipply');
- vip_play.classList.add('u-btni-addply');
- vip_add.classList.remove('u-btni-vipadd');
- vip_add.classList.add('u-btni-add');
- content_operation.insertAdjacentElement('afterbegin', vip_add);
- content_operation.insertAdjacentElement('afterbegin', vip_play);
- content_operation.removeChild(vip_group);
-
- // Text
- vip_play.title = CONST.Text.V5NOCANQU;
- vip_play.children[0].childNodes[1].nodeValue = '播放';
- }
-
- // Download
- const dlButton = $(oDoc, '#content-operation>a[data-res-action="download"]');
- AEL.call(dlButton, 'click', dlOnclick, {useCapture: true});
-
- function dlOnclick(e) {
- e.stopPropagation();
- downloadSong(name, artist, cover);
- }
- }
-
- function pageAlbum() {
- const iframe = $('#g_iframe');
- const oDoc = iframe.contentDocument;
- const oWin = iframe.contentWindow;
-
- // GUI
- if ($(oDoc, '.vip-album')) {
- const content_operation = $(oDoc, '#content-operation');
- const vip_group = $(content_operation, '.u-vip-btn-group');
- const vip_play = $(vip_group, 'a[data-res-action="play"]');
- const vip_add = $(vip_group, 'a[data-res-action="addto"]');
-
- // Style
- vip_play.classList.remove('u-btni-vipply');
- vip_play.classList.add('u-btni-addply');
- vip_add.classList.remove('u-btni-vipadd');
- vip_add.classList.add('u-btni-add');
- content_operation.insertAdjacentElement('afterbegin', vip_add);
- content_operation.insertAdjacentElement('afterbegin', vip_play);
- content_operation.removeChild(vip_group);
-
- // Text
- vip_play.title = CONST.Text.V5NOCANQU;
- vip_play.children[0].childNodes[1].nodeValue = '播放';
- }
- }
-
- function replacePredata() {
- const iframe = $('#g_iframe');
- const oDoc = iframe.contentDocument;
- const oWin = iframe.contentWindow;
- const envReady = oDoc && iframeDocSync();
- const elmData = oDoc && $(oDoc, '#song-list-pre-data');
- if (!elmData) {
- // No elmData found.
- if (envReady && $(oDoc, '#song-list-pre-cache table')) {
- // Too late. Data has already been dealed.
- DoLog(LogLevel.Error, 'Predata hook failed.');
- DoLog([$(oDoc, '#song-list-pre-cache table'), oDoc.URL, oWin.location.href]);
- } else {
- // Data has not been loaded!
- DoLog('No predata found');
- if (envReady) {
- // Hook Element.prototype.getElementsByTagName to make changeValue called.
- DoLog('Environment ready, hooking getElementsByTagName...');
- const hooker = new Hooker();
- const id = hooker.hook(oWin, 'Element.prototype.getElementsByTagName', false, false, {
- dealer: function(_this, args) {
- if (_this.id === 'song-list-pre-cache' && args[0] === 'textarea') {
- const elmData = $(_this, 'textarea');
- changeValue(elmData);
- hooker.unhook(id);
- DoLog('Value changed, getElementsByTagName unhooked...');
- }
- return [_this, args];
- }
- }).id;
- DoLog(LogLevel.Success, 'getElementsByTagName Hooked...');
- } else {
- // Environment not ready yet, wait for it
- DoLog('Environment not ready, waiting...');
- setTimeout(replacePredata, CONST.Number.Interval_Fastest);
- }
- }
- return false;
- } else {
- // elmData Found! Go change value directly.
- DoLog('Changing value directly');
- changeValue(elmData);
- }
-
- function changeValue(elmData) {
- const RATES = {
- 'none': 0,
- 'standard': 128000,
- 'lossless': 999000,
- };
-
- const list = JSON.parse(elmData.value);
- for (const song of list) {
- const privilege = song.privilege;
- const dlLevel = privilege.downloadMaxBrLevel;
- const dlRate = RATES[dlLevel];
- const plLevel = privilege.playMaxBrLevel;
- const plRate = RATES[plLevel];
- privilege.dlLevel = dlLevel;
- privilege.dl = dlRate;
- privilege.plLevel = plLevel;
- privilege.pl = plRate;
- }
- elmData.value = JSON.stringify(list);
-
- DoLog(LogLevel.Success, 'Predata replaced');
- }
- }
-
- function downloadSong(name, artist, cover) {
- // Check arguments
- if (!name || !artist) {
- DoLog(LogLevel.Error, 'downloadSong: name or artist missing');
- return false;
- }
- !cover && DoLog('downloadSong: cover not provided');
-
- // Gather info
- const fname = replaceText('{$NAME} - {$ARTIST}', {'{$NAME}': name, '{$ARTIST}': artist});
- const cpath = getUrlPath(cover);
-
- search_song();
-
- function search_song(page=1) {
- search({
- text: fname,
- page: page,
- callback: onsearch,
- });
-
- function onsearch(json) {
- const isLastPage = (page === CONST.Number.MaxSearchPage || json.data.more === '0');
- !get_song(json, isLastPage) && search_song(page+1);
- }
- }
-
- function get_song(json, force=false) {
- const list = json.data.list;
- const song = choose(list, force);
- if (song) {
- dl_GM(song.url, song.fname + '.' + song.ftype);
- dl(song.lrc, song.fname + '.lrc');
- dl(song.cover, song.fname + song.cpath.match(/\.[a-zA-Z]+?$/)[0]);
- return true;
- } else {
- return false;
- }
-
- function choose(list, force) {
- const my_list = list.map((song) => {
- const qualities = [2000, 320, 128];
- const q = qualities.find((q) => (song.quality.includes(q)));
- const s_url = song[({
- 2000: 'url_flac',
- 320: 'url_320',
- 128: 'url_128'
- })[q]];
- const s_ftype = ({
- 2000: 'flac',
- 320: 'mp3',
- 128: 'mp3'
- })[q];
- const s_lrc = song.lrc;
- const s_cover = song.cover;
- const s_name = song.name;
- const s_artist = song.artist;
- const s_fname = name + ' - ' + artist;
- const s_cpath = getUrlPath(s_cover);
-
- return {
- ftype: s_ftype,
- url: s_url,
- lrc: s_lrc,
- cover: s_cover,
- artist: s_artist,
- fname: s_fname,
- cpath: s_cpath
- }
- })
- return my_list.find((song) => (song.cpath === cpath || !cpath)) || (force ? my_list[0] : null);
- }
- }
- }
-
- function search(details, retry=3) {
- const text = details.text;
- const page = details.page || '1';
- const type = details.type || 'YQD';
- const callback = details.callback;
- if (!text || !callback) {
- throw new Error('Argument text or callback missing');
- }
-
- const url = 'http://59.110.45.28/m/api/search';
- GM_xmlhttpRequest({
- method: 'POST',
- url: url,
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded',
- 'Referer': 'https://tools.liumingye.cn/music_old/'
- },
- data: encode('text='+text+'&page='+page+'&type='+type),
- onload: function(res) {
- let json;
- try {
- json = JSON.parse(res.responseText);
- if (json.code !== 200) {
- throw new Error('dataerror');
- } else {
- callback(json);
- }
- } catch(e) {
- --retry >= 0 && search(details, retry);
- return false;
- }
- }
- });
- }
-
- function encode(plainText) {
- const now = new Date().getTime();
- const md5Data = md5('<G6sX,Lk~^2:Y%4Z');
- let left = md5(md5Data.substr(0, 16));
- let right = md5(md5Data.substr(16, 32));
- let nowMD5 = md5(now).substr(-4);
- let Var_10 = (left + md5((left + nowMD5)));
- let Var_11 = Var_10['length'];
- let Var_12 = ((((now / 1000 + 86400) >> 0) + md5((plainText + right)).substr(0, 16)) + plainText);
- let Var_13 = '';
- for (let i = 0, Var_15 = Var_12.length;
- (i < Var_15); i++) {
- let Var_16 = Var_12.charCodeAt(i);
- if ((Var_16 < 128)) {
- Var_13 += String['fromCharCode'](Var_16);
- } else if ((Var_16 > 127) && (Var_16 < 2048)) {
- Var_13 += String['fromCharCode'](((Var_16 >> 6) | 192));
- Var_13 += String['fromCharCode'](((Var_16 & 63) | 128));
- } else {
- Var_13 += String['fromCharCode'](((Var_16 >> 12) | 224));
- Var_13 += String['fromCharCode']((((Var_16 >> 6) & 63) | 128));
- Var_13 += String['fromCharCode'](((Var_16 & 63) | 128));
- }
- }
- let Var_17 = Var_13.length;
- let Var_18 = [];
- for (let i = 0; i <= 255; i++) {
- Var_18[i] = Var_10[(i % Var_11)].charCodeAt();
- }
- let Var_19 = [];
- for (let Var_04 = 0;
- (Var_04 < 256); Var_04++) {
- Var_19.push(Var_04);
- }
- for (let Var_20 = 0, Var_04 = 0;
- (Var_04 < 256); Var_04++) {
- Var_20 = (((Var_20 + Var_19[Var_04]) + Var_18[Var_04]) % 256);
- let Var_21 = Var_19[Var_04];
- Var_19[Var_04] = Var_19[Var_20];
- Var_19[Var_20] = Var_21;
- }
- let Var_22 = '';
- for (let Var_23 = 0, Var_20 = 0, Var_04 = 0;
- (Var_04 < Var_17); Var_04++) {
- let Var_24 = '0|2|4|3|5|1'.split('|'),
- Var_25 = 0;
- while (true) {
- switch (Var_24[Var_25++]) {
- case '0':
- Var_23 = ((Var_23 + 1) % 256);
- continue;
- case '1':
- Var_22 += String.fromCharCode(Var_13[Var_04].charCodeAt() ^ Var_19[((Var_19[Var_23] + Var_19[Var_20]) % 256)]);
- continue;
- case '2':
- Var_20 = ((Var_20 + Var_19[Var_23]) % 256);
- continue;
- case '3':
- Var_19[Var_23] = Var_19[Var_20];
- continue;
- case '4':
- var Var_21 = Var_19[Var_23];
- continue;
- case '5':
- Var_19[Var_20] = Var_21;
- continue;
- }
- break;
- }
- }
- let Var_26 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
- for (var Var_27, Var_28, Var_29 = 0, Var_30 = Var_26, Var_31 = ''; Var_22.charAt((Var_29 | 0)) || (Var_30 = '=', (Var_29 % 1)); Var_31 += Var_30.charAt((63 & (Var_27 >> (8 - ((Var_29 % 1) * 8)))))) {
- Var_28 = Var_22['charCodeAt'](Var_29 += 0.75);
- Var_27 = ((Var_27 << 8) | Var_28);
- }
- Var_22 = (nowMD5 + Var_31.replace(/=/g, '')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '.');
- return (('data=' + Var_22) + '&v=2');
- }
-
- function dl(url, name) {
- GM_xmlhttpRequest({
- method: 'GET',
- url: url,
- responseType: 'blob',
- onload: function(res) {
- const ourl = URL.createObjectURL(res.response);
- const a = document.createElement('a');
- a.download = name;
- a.href = ourl;
- a.click();
- setTimeout(function() {
- URL.revokeObjectURL(ourl);
- }, 0);
- }
- });
- }
-
- function dl_browser(url, name) {
- const a = $CrE('a');
- a.href = url;
- a.download = name;
- a.click();
- }
-
- function dl_GM(url, name) {
- GM_download(url, name);
- }
-
- // Basic functions
- // querySelector
- function $() {
- switch(arguments.length) {
- case 2:
- return arguments[0].querySelector(arguments[1]);
- break;
- default:
- return document.querySelector(arguments[0]);
- }
- }
- // querySelectorAll
- function $All() {
- switch(arguments.length) {
- case 2:
- return arguments[0].querySelectorAll(arguments[1]);
- break;
- default:
- return document.querySelectorAll(arguments[0]);
- }
- }
- // createElement
- function $CrE() {
- switch(arguments.length) {
- case 2:
- return arguments[0].createElement(arguments[1]);
- break;
- default:
- return document.createElement(arguments[0]);
- }
- }
-
- // Get the pathname of a given url
- function getUrlPath(url) {
- if (typeof url === 'string') {
- const a = $CrE('a');
- a.href = url;
- return a.pathname;
- } else {
- return null;
- }
- }
-
- // Replace model text with no mismatching of replacing replaced text
- // e.g. replaceText('aaaabbbbccccdddd', {'a': 'b', 'b': 'c', 'c': 'd', 'd': 'e'}) === 'bbbbccccddddeeee'
- // replaceText('abcdAABBAA', {'BB': 'AA', 'AAAAAA': 'This is a trap!'}) === 'abcdAAAAAA'
- // replaceText('abcd{AAAA}BB}', {'{AAAA}': '{BB', '{BBBB}': 'This is a trap!'}) === 'abcd{BBBB}'
- // replaceText('abcd', {}) === 'abcd'
- /* Note:
- replaceText will replace in sort of replacer's iterating sort
- e.g. currently replaceText('abcdAABBAA', {'BBAA': 'TEXT', 'AABB': 'TEXT'}) === 'abcdAATEXT'
- but remember: (As MDN Web Doc said,) Although the keys of an ordinary Object are ordered now, this was
- not always the case, and the order is complex. As a result, it's best not to rely on property order.
- So, don't expect replaceText will treat replacer key-values in any specific sort. Use replaceText to
- replace irrelevance replacer keys only.
- */
- function replaceText(text, replacer) {
- if (Object.entries(replacer).length === 0) {return text;}
- const [models, targets] = Object.entries(replacer);
- const len = models.length;
- let text_arr = [{text: text, replacable: true}];
- for (const [model, target] of Object.entries(replacer)) {
- text_arr = replace(text_arr, model, target);
- }
- return text_arr.map((text_obj) => (text_obj.text)).join('');
-
- function replace(text_arr, model, target) {
- const result_arr = [];
- for (const text_obj of text_arr) {
- if (text_obj.replacable) {
- const splited = text_obj.text.split(model);
- for (const part of splited) {
- result_arr.push({text: part, replacable: true});
- result_arr.push({text: target, replacable: false});
- }
- result_arr.pop();
- } else {
- result_arr.push(text_obj);
- }
- }
- return result_arr;
- }
- }
-
- function iframeDocSync() {
- const iframe = $('#g_iframe');
- const oDoc = iframe && iframe.contentDocument;
- if (oDoc) {
- const top_path = document.URL.replace(/^https?:\/\/music\.163\.com\/#\//, '').replace(/^my\/m\//, '');
- const ifr_path = oDoc.URL.replace(/^https?:\/\/music\.163\.com\//, '').replace(/^my\/#\//, '');
- return top_path === ifr_path;
- } else {
- return false;
- }
- }
-
- // Get unpolluted addEventListener
- function getPureAEL(parentDocument=document) {
- const ifr = makeIfr(parentDocument);
-
- const oWin = ifr.contentWindow;
- const oDoc = ifr.contentDocument;
-
- const AEL = oWin.XMLHttpRequest.prototype.addEventListener;
- return AEL;
- }
-
- // Get unpolluted addEventListener
- function getPureREL(parentDocument=document) {
- const ifr = makeIfr(parentDocument);
-
- const oWin = ifr.contentWindow;
- const oDoc = ifr.contentDocument;
-
- const REL = oWin.XMLHttpRequest.prototype.removeEventListener;
- return REL;
- }
-
- function makeIfr(parentDocument=document) {
- const ifr = $CrE(parentDocument, 'iframe');
- ifr.srcdoc = '<html></html>';
- ifr.style.width = ifr.style.height = ifr.style.border = ifr.style.padding = ifr.style.margin = '0';
- parentDocument.body.appendChild(ifr);
- return ifr;
- }
-
- function APIHooker() {
- const AH = this;
- const hooker = new Hooker();
- const hooker_hooks = [];
- const hooks = [];
- const addEventListener = (function() {
- const AEL = getPureAEL();
- return function() {
- const args = Array.from(arguments);
- const _this = args.shift();
- AEL.apply(_this, args);
- }
- }) ();
- const removeEventListener = (function() {
- const REL = getPureREL();
- return function() {
- const args = Array.from(arguments);
- const _this = args.shift();
- REL.apply(_this, args);
- }
- }) ();
-
- AH.hook = hook;
- AH.unhook = unhook;
- AH.pageOnchange = recover;
-
- inject();
- setInterval(inject, CONST.Number.Interval_Balanced);
-
- function hook(urlMatcher, xhrDealer) {
- return hooks.push({
- id: hooks.length,
- matcher: urlMatcher,
- dealer: xhrDealer,
- xhrs: []
- }) - 1;
- }
-
- function unhook(id) {
- hooks.splice(id, 1);
- }
-
- function inject() {
- const iframe = $('#g_iframe');
- const oWin = iframe ? iframe.contentWindow : null;
-
- const hook_dealers = {
- open: function(_this, args) {
- const xhr = _this;
- for (const hook of hooks) {
- matchUrl(args[1], hook.matcher) && hook.xhrs.push(xhr);
- }
- return [_this, args];
- },
- send: function(_this, args) {
- const xhr = _this;
- for (const hook of hooks) {
- if (hook.xhrs.includes(xhr)) {
- // After first readystatechange event, change onreadystatechange to our onProgress function
- let onreadystatechange;
- addEventListener(xhr, 'readystatechange', function(e) {
- onreadystatechange = xhr.onreadystatechange;
- xhr.onreadystatechange = onProgress;
- }, {
- capture: false,
- passive: true,
- once: true
- });
-
- // Recieves last 3 readystatechange event, apply dealer function, and continue onreadystatechange stack
- function onProgress(e) {
- let args = Array.from(arguments);
-
- // When onload, apply xhr dealer
- let continueStack = true;
- if (xhr.status === 200 && xhr.readyState === 4) {
- continueStack = hook.dealer(xhr, this, args, onreadystatechange);
- }
-
- continueStack && typeof onreadystatechange === 'function' && onreadystatechange.apply(this, args);
- }
- }
- }
- return [_this, args];
- },
- }
- let do_inject = false;
-
- // Hook open: filter all xhr that should be hooked
- try {
- if (window.XMLHttpRequest.prototype.open.name !== 'hooker') {
- hooker_hooks.push(hooker.hook(window, 'XMLHttpRequest.prototype.open', false, false, {
- dealer: hook_dealers.open
- }));
- do_inject = true;
- }
- if (oWin && oWin.XMLHttpRequest.prototype.open.name !== 'hooker') {
- hooker_hooks.push(hooker.hook(oWin, 'XMLHttpRequest.prototype.open', false, false, {
- dealer: hook_dealers.open
- }));
- do_inject = true;
- }
-
- // Hook send: change eventListeners for each hooked xhr, and apply xhr dealer
- if (window.XMLHttpRequest.prototype.send.name !== 'hooker') {
- hooker_hooks.push(hooker.hook(window, 'XMLHttpRequest.prototype.send', false, false, {
- dealer: hook_dealers.send
- }));
- do_inject = true;
- }
- if (oWin && oWin.XMLHttpRequest.prototype.send.name !== 'hooker') {
- hooker_hooks.push(hooker.hook(oWin, 'XMLHttpRequest.prototype.send', false, false, {
- dealer: hook_dealers.send
- }));
- do_inject = true;
- }
- } catch(err) {}
-
- do_inject && DoLog(LogLevel.Success, 'Hooker injected');
- }
-
- function recover() {
- hooker_hooks.forEach((hook) => (hooker.unhook(hook.id)));
-
- DoLog(LogLevel.Success, 'Hooker removed');
- }
-
- function matchUrl(url, matcher) {
- if (matcher instanceof RegExp) {
- return !!url.match(matcher);
- }
- if (typeof matcher === 'function') {
- return matcher(url);
- }
- }
-
- function idmaker() {
- let i = 0;
- return function() {
- return i++;
- }
- }
- }
-
- function Hooker() {
- const H = this;
- const makeid = idmaker();
- const map = H.map = {};
- H.hook = hook;
- H.unhook = unhook;
-
- function hook(base, path, log=false, apply_debugger=false, hook_return=false) {
- // target
- path = arrPath(path);
- let parent = base;
- for (let i = 0; i < path.length - 1; i++) {
- const prop = path[i];
- parent = parent[prop];
- }
- const prop = path[path.length-1];
- const target = parent[prop];
-
- // Only hook functions
- if (typeof target !== 'function') {
- throw new TypeError('hooker.hook: Hook functions only');
- }
- // Check args valid
- if (hook_return) {
- if (typeof hook_return !== 'object' || hook_return === null) {
- throw new TypeError('hooker.hook: Argument hook_return should be false or an object');
- }
- if (!hook_return.hasOwnProperty('value') && typeof hook_return.dealer !== 'function') {
- throw new TypeError('hooker.hook: Argument hook_return should contain one of following properties: value, dealer');
- }
- if (hook_return.hasOwnProperty('value') && typeof hook_return.dealer === 'function') {
- throw new TypeError('hooker.hook: Argument hook_return should not contain both of following properties: value, dealer');
- }
- }
-
- // hooker function
- const hooker = function hooker() {
- let _this = this === H ? null : this;
- let args = Array.from(arguments);
- const config = map[id].config;
- const hook_return = config.hook_return;
-
- // hook functions
- config.log && console.log([base, path.join('.')], _this, args);
- if (config.apply_debugger) {debugger;}
- if (hook_return && typeof hook_return.dealer === 'function') {
- [_this, args] = hook_return.dealer(_this, args);
- }
-
- // continue stack
- return hook_return && hook_return.hasOwnProperty('value') ? hook_return.value : target.apply(_this, args);
- }
- parent[prop] = hooker;
-
- // Id
- const id = makeid();
- map[id] = {
- id: id,
- prop: prop,
- parent: parent,
- target: target,
- hooker: hooker,
- config: {
- log: log,
- apply_debugger: apply_debugger,
- hook_return: hook_return
- }
- };
-
- return map[id];
- }
-
- function unhook(id) {
- // unhook
- try {
- const hookObj = map[id];
- hookObj.parent[hookObj.prop] = hookObj.target;
- delete map[id];
- } catch(err) {
- console.error(err);
- DoLog(LogLevel.Error, 'unhook error');
- }
- }
-
- function arrPath(path) {
- return Array.isArray(path) ? path : path.split('.')
- }
-
- function idmaker() {
- let i = 0;
- return function() {
- return i++;
- }
- }
- }
-
- function IntervalTaskManager() {
- const tasks = this.tasks = [];
- this.time = 500;
- this.interval = -1;
- defineProperty(this, 'working', {
- get: () => (this.interval >= 0)
- });
-
- this.addTask = function(fn) {
- tasks.push(fn);
- }
-
- this.removeTask = function(fn_idx) {
- const idx = typeof fn_idx === 'number' ? fn_idx : tasks.indexOf(fn_idx)
- tasks.splice(idx, 1)
- }
-
- this.clearTasks = function() {
- tasks.splice(0, Infinity)
- }
-
- this.start = function() {
- if (!this.working) {
- this.interval = setInterval(this.do, this.time);
- return true;
- } else {
- return false;
- }
- }
-
- this.stop = function() {
- if (this.working) {
- clearInterval(this.interval);
- this.interval = -1;
- return true;
- } else {
- return false;
- }
- }
-
- this.do = function() {
- for (const task of tasks) {
- task();
- }
- }
- }
-
- function defineProperty(obj, prop, desc) {
- desc.configurable = false;
- desc.enumerable = true;
- Object.defineProperty(obj, prop, desc);
- }
- })();