利用MyFreeMP3扩展网易云音乐功能
/* 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 2.1 // @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 liumingye.cn // @connect * // @connect music.163.net // @connect music.126.net // @require https://greasyfork.org/scripts/456034-basic-functions-for-userscripts/code/script.js?version=1226884 // @require https://greasyfork.org/scripts/457199/code/script.js?version=1132840 // @require https://greasyfork.org/scripts/457244/code/script.js?version=1132550 // @require https://update.greasyfork.org/scripts/474021/1465958/MyFreeMP3%20API.js // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAABYZJREFUWEfFl3lQ1VUUxz+/92SRfRPZRFxK0UA0U3LSsAz33cqSVDIn0zR1nDQap6Yxtz9s0dEoR53E3JXUMKXScdBKS0kkNEUFHoKyySrw4P2ae+E9RX7vgTYN56/f755z7/2ee8/5nnMVWikZBHo7oE5QYSgo4UAXwKVxegVwA9SLCpyoQUkMJbeoNUsrLRldJyhcRf0AmAzYtWTfqDcC+xWUVV0xXLQ1xyqAy/i42uGwFngbaBGolU1UIN5Izfs9KSzXstFc+CrBvXXUJwLdW+lxS2bXTOgnPEF2+sOGzQDcIDDShHIMVLeWVn00vVKmQx3ehdzfHpzXBECD56Yzj7O5XY9uOAyIwFR8l3vJp1BrRRg087fMhG7QgydhAdB45+cf59g9P1mC5/KFlt2MV2+Q98Ir1BnytA7pmpGafuaYsADIJHATMMfWseq9PXGbO0OalKz4AlQVx0H9CTgtwgVq0y5j1z0Epb0jxR+u4e7K9daW+6obue8IpQTQmGqptqLdZep4fDauROfpLhfNCuhHfd4dPOLm4/XpUqp//ZNbg8Zj3+tJXGZMofTzzVJvLTsUlAiRohJAJoE7ganWrD2WzcNrlaACqL9dQMmKLynbsFX+u82bic+GFdReuoIh7MVHictd3ch9TREMZw/isjRJxnXGy3TY9plcuPLAUQpiF2Mqa0hpu67BuC2cjfv8WPlfvnU31SnnqNz3g8XGBiJjLfgr1wmYpaJs1jLU+/vS6copdK4uVHyXyJ2Y+fLeFXs7vFbH4fZuLIpdu2ZT64tKKFqwXM6xJQrqW0omAQmgTNMy9F73Ee6LZlOXZSCn11DUqnvSrOPeeJynjJbf1SlnG4Jx8EDExopej86jgUIK58ZRtulbGxjUHUomgYKrw5pZ6fV0zjuPvoM3hXOWURafIE2cJ4+i476vUevqKIhZQMXuQziNHIpf0nbq8wvICX2eDlvW4TxxBNTXY+g/itrUZgRo3i5NABAXaq5qFhz2T/UgKO1n+Z/lEya9E+KfvJP2wwZzd+1GipeulGMi7UKK01EcHTBERGPMuEpASiIOz/ShKukX8kdPt3YKFQKAKBjNxGnMMPwOb6Mu5xbZwQMs+s6FaQg+yB04hpqzInMbJODkPhyfj6Rg1hLKt+ySIAVYTCayfPtYHHh4I6sAXKZNxDdhPcbL18gJjboP4HYqel8fbj03kerT5yzjHQ9uxnnCCIoWfSw5AL2eLqUZKM5O5EW/LulZS6xegdOIKPyOJmAqLeemR6hlrt/3W3AaF03lnsPcflWSmYyToL9PoPfxIn9kDFU/npTjnTLPyFQVdsJeQ+QVaAZhu0A/gg1/yDmGvsMtgeQQ2Y9AQb06nRyrSU3HaXgUImWNVzIxhA+zFKKQokvovDxkDIhY0BARhNbTMOivZOzDQyldv1XmtVlcp0/BJ36NDDqz1N3MIX/sTMmIQuwjehN04Zj8zg7qT11uvsb+6g6bROQ25w18Nq1CNdaR2zea2vR/LIsIj50nj0bv5SE9rzx0HPVe9f2Y2P8NzpNGykAVAat5/4KIbFGxYLnA1OOywBivZ5M3ZJIVT5oub64PYjR/XCxVh5O19m+gYqGxVYzsw3oSkHIQnZurJJqCNxdTdfSEtkfOTrIyur83S+rLt+2RtcOKNBQjoWypHIvA80/abinFNb9foHJ/ErVpGTJLxHU4DonEJWaS5AghlXuPyNqh3RmhNinHjadgsyFpF+Qv+wGnsS9Z80iOC0DFcasbaoCqyXHCrGlDIkZa25I5PB2OICnHqGexC+mE4mBPfUmpTMmqIz9RkXAAU0WlLZDaLZmY8V+aUpvHYlEq1ptSs02btuVmEG36MDGDaNOn2YP32WaP04eD6v96nv8LNmVLjadPDdEAAAAASUVORK5CYII= // @grant GM_xmlhttpRequest // @grant GM_download // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @run-at document-start // @noframes // ==/UserScript== /* global LogLevel DoLog Err $ $All $CrE $AEL $$CrE addStyle detectDom destroyEvent copyProp copyProps parseArgs escJsStr replaceText getUrlArgv dl_browser dl_GM AsyncManager */ /* global md5 pop */ /* global Mfapi */ (function __MAIN__() { 'use strict'; const CONST = { Text: { V5NOCANQU: '要听龙叔的话,V5不能屈,听完歌快去给你网易爸爸充VIP吧', SongNotFound: '没有找到歌曲资源', ErrorOccured: `<span style="color: orange;">pageFunc Error</span>`, DownloadSetting: { Lrc: { Text: ['[x]同时下载歌词', '[√]同时下载歌词'], Tip: ['已关闭:下载歌曲时自动下载歌词', '已开启:下载歌曲时自动下载歌词'] }, Cover: { Text: ['[x]同时下载封面', '[√]同时下载封面'], Tip: ['已关闭:下载歌曲时自动下载封面', '已开启:下载歌曲时自动下载封面'] } } }, Number: { Interval_Fastest: 1, Interval_Fast: 50, Interval_Balanced: 500, MaxSearchPage: 3, }, TYPE_INFO: { 2000: 'flac', 320: 'mp3', 128: 'mp3' } } // Prepare const WEAPI = new Weapi(); const PV = new Privileger(); // function DoLog() [} // Arguments: level=LogLevel.Info, logContent, trace=false const [LogLevel, DoLog] = (function() { const LogLevel = { None: 0, Error: 1, Success: 2, Warning: 3, Info: 4, }; return [LogLevel, DoLog]; function DoLog() { // Get window const win = (typeof(unsafeWindow) === 'object' && unsafeWindow !== null) ? unsafeWindow : window; const LogLevelMap = {}; LogLevelMap[LogLevel.None] = { prefix: '', color: 'color:#ffffff' } LogLevelMap[LogLevel.Error] = { prefix: '[Error]', color: 'color:#ff0000' } LogLevelMap[LogLevel.Success] = { prefix: '[Success]', color: 'color:#00aa00' } LogLevelMap[LogLevel.Warning] = { prefix: '[Warning]', color: 'color:#ffa500' } LogLevelMap[LogLevel.Info] = { prefix: '[Info]', color: 'color:#888888' } 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); // Get args let [level, logContent, trace] = parseArgs([...arguments], [ [2], [1,2], [1,2,3] ], [LogLevel.Info, 'DoLog initialized.', false]); // Log when log level permits if (level <= DoLog.logLevel) { let msg = '%c' + LogLevelMap[level].prefix + (typeof GM_info === 'object' ? `[${GM_info.script.name}]` : '') + (LogLevelMap[level].prefix ? ' ' : ''); let subst = LogLevelMap[level].color; switch (typeof(logContent)) { case 'string': msg += '%s'; break; case 'number': msg += '%d'; break; default: msg += '%o'; break; } if (++DoLog.logCount > 512) { console.clear(); DoLog.logCount = 0; } console[trace ? 'trace' : 'log'](msg, subst, logContent); } } }) (); main(); function main() { // Wait for document.body if (!document.body) { setTimeout(main, CONST.Number.Interval_Fast); return false; } // Commons hookPlay(); playlistDownload(); // Page functions const ITM = unsafeWindow.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) { ITM.removeTask(ITM.tasks.indexOf(pageChangeDetecter)); callback(href, new_href); href = new_href; ITM.addTask(inject_iframe); } } }) (deliverPageFuncs, true); ITM.time = CONST.Number.Interval_Fast; ITM.addTask(inject_iframe); ITM.start(); function inject_iframe() { const ifr = $('#g_iframe') || {}; const oWin = ifr.contentWindow; const oDoc = ifr.contentDocument; if (oWin && oDoc && oWin.location && oWin.location.host === 'music.163.com') { const AEL = getPureAEL(); AEL.call(oWin, 'unload', function() { ITM.addTask(pageChangeDetecter); }); ITM.removeTask(ITM.tasks.indexOf(inject_iframe)); } } 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\/#\/(my\/m\/music\/)?playlist\?.+$/, func: replacePredata_encoded, 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\/#\/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; } },{ reg: /^https?:\/\/music\.163\.com\//, func: settings }]; for (const pageFunc of pageFuncs) { wrap(pageFunc); } for (const pageFunc of pageFuncs) { test_exec(pageFunc); } function wrap(pageFunc) { pageFunc.name = pageFunc.name || pageFunc.func.name; pageFunc.func = (function(func) { return function wrapper() { try { return func(); } catch(err) { DoLog(LogLevel.Error, `Error executing pageFunc ${pageFunc.name}`); DoLog(LogLevel.Error, err, true); showErr(CONST.Text.ErrorOccured, true); } } }) (pageFunc.func); } 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}, ${pageFunc.name}`), false)) && (DoLog('Exec ' + pageFunc.name), pageFunc.func(href, new_href)); } } } function hookPlay() { // Access Checker: core_fbc43dc690327907cf6fdad6d52f7c31.js?:formatted:8988('l6f.tt2x = function(bi7b, action) {') // Play const APIH = new APIHooker(); APIH.hook(/\/weapi\/v3\/song\/detail(\?[a-zA-Z0-9=_]+)?$/, function(xhr) { const json = JSON.parse(xhr.response); json.privileges.forEach(privilege => PV.fix(privilege)); rewriteResponse(xhr, json); return true; }); APIH.hook(/\/weapi\/v1\/play\/record(\?[a-zA-Z0-9=_]+)?$/, function(xhr) { const json = JSON.parse(xhr.response); (json.allData || []).concat(json.weekData || []).forEach(data => PV.fix(data.song.privilege)); rewriteResponse(xhr, json); return true; }); APIH.hook(/\/weapi\/v6\/playlist\/detail(\/?\?[a-zA-Z0-9=_]+)?$/, function(xhr) { const json = JSON.parse(xhr.response); json.privileges.forEach(privilege => PV.fix(privilege)); rewriteResponse(xhr, json); 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]; // Only hook unplayable songs if (data['url']) {return true}; search(data.id, function(song) { song ? reqSong(song) : showTip(CONST.Text.SongNotFound); function reqSong(song) { const qualities = Object.keys(CONST.TYPE_INFO).map(n => parseInt(n, 10)).sort((q1, q2) => q2 - q1); const q = qualities.find(q => song.quality.includes(q)); const abort = GM_xmlhttpRequest({ method: 'GET', url: song.url[q], onprogress: load, onload: load }).abort; function load(e) { // Abort request first abort(); // Check if finalUrl differ from original url if (song.url === e.finalUrl) { DoLog(LogLevel.Warning, 'Searched song returned a useless url'); showTip(CONST.Text.SongNotFound); } // modify xhr and continue stack data['code'] = 200; data['br'] = PV.levelData[data.id].plRate; data['level'] = PV.levelData[data.id].plLevel; data['type'] = 'mp3'; data['url'] = e.finalUrl; rewriteResponse(xhr, json); continueStack(); } } }); // Suspend stack until search & find the song return false; function continueStack() { onreadystatechange.apply(_this, args);; } }); function rewriteResponse(xhr, json) { const response = JSON.stringify(json); const propDesc = { value: response, writable: false, configurable: true, enumerable: true }; Object.defineProperties(xhr, { 'response': propDesc, 'responseText': propDesc }); } } 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(); downloadSong(elm.getAttribute('data-res-id') * 1); } }, {capture: true}); function $T(elm, selector) { const e = $(elm, selector); return e ? e.innerText : null; } } function playlistDownload() { const AEL = getPureAEL(); AEL.call(document.body, 'click', function(e) { const elm = e.target; if (elm.getAttribute('data-action') === 'download') { e.stopPropagation(); downloadSong(elm.getAttribute('data-id') * 1); } }, {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')) { // vip song const content_operation = $(oDoc, '#content-operation'); const vip_group = $(content_operation, '.u-vip-btn-group'); const vip_play = $(vip_group || content_operation, 'a[data-res-action="play"]'); const vip_add = $(vip_group || content_operation, 'a[data-res-action="addto"]'); const vip_download = $(content_operation, '.u-btn-vip-download'); // Style vip_play.classList.remove('u-btni-vipply'); vip_play.classList.remove('u-btni-openvipply'); vip_play.classList.add('u-btni-addply'); vip_add && vip_add.classList.remove('u-btni-vipadd'); vip_add && vip_add.classList.add('u-btni-add'); vip_download.classList.remove('u-btn-vip-download'); if (vip_group) { vip_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 = '播放'; } if ($(oDoc, '.u-btni-play-dis')) { // Copyright song // Data const cpr_play = $(oDoc, '.u-btni-play-dis'); const cpr_fav = cpr_play.nextElementSibling; cpr_play.setAttribute('data-res-id', cpr_fav.getAttribute('data-res-id')); cpr_play.setAttribute('data-res-type', cpr_fav.getAttribute('data-res-type')); cpr_play.setAttribute('data-res-action', 'play'); // Style cpr_play.classList.remove('u-btni-play-dis'); } // Download const dlButton = $(oDoc, '#content-operation>a[data-res-action="download"]'); AEL.call(dlButton, 'click', dlOnclick, {useCapture: true}); function dlOnclick(e) { e.stopPropagation(); downloadSong(dlButton.getAttribute('data-res-id') * 1); } } 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 || content_operation, 'a[data-res-action="play"]'); const vip_add = $(vip_group || content_operation, 'a[data-res-action="addto"]'); // Style vip_play.classList.remove('u-btni-vipply'); vip_play.classList.remove('u-btni-openvipply'); vip_play.classList.add('u-btni-addply'); vip_add && vip_add.classList.remove('u-btni-vipadd'); vip_add && vip_add.classList.add('u-btni-add'); if (vip_group) { vip_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 settings() { const DS = CONST.Text.DownloadSetting makeBooleanMenu(DS.Lrc.Text, DS.Lrc.Tip, 'lrc'); makeBooleanMenu(DS.Cover.Text, DS.Cover.Tip, 'cover'); function makeBooleanMenu(texts, tips, key) { const initialText = texts[GM_getValue(key, false) + 0]; let id = GM_registerMenuCommand(initialText, onClick/*, { autoClose: false }*/); function onClick() { const newValue = !GM_getValue(key, false); const newText = texts[newValue + 0]; GM_setValue(key, newValue); GM_unregisterMenuCommand(id); id = GM_registerMenuCommand(newText, onClick/*, { autoClose: false }*/); pop.info(tips[newValue + 0]); } } } 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 list = JSON.parse(elmData.value); list.forEach(song => PV.fix(song.privilege)); elmData.value = JSON.stringify(list); DoLog(LogLevel.Success, 'Predata replaced'); } } function replacePredata_encoded() { 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_encoded, CONST.Number.Interval_Fastest); } } return false; } else { // elmData Found! Go change value directly. DoLog('Changing value directly'); changeValue(elmData); } function changeValue(elmData) { // Decrypt text const decode = Object.values(unsafeWindow.NEJ.P('nej.u')).find(f => f.toString().match(/function\([a-z0-9]+,[a-z0-9]+\)\{return [a-z0-9]+\.[a-z0-9]+\([a-z0-9]+\.[a-z0-9]+\([a-z0-9]+\),[a-z0-9]+\)\}/i)); const decrypt = (str, key) => decode(str, key); const request = Object.values(unsafeWindow.NEJ.P('nej.j')).find(f => f.toString().includes('.replace("api","weapi")')); let encrypStr, position; request("/m/api/encryption/param/get", { sync: true, type: "json", query: {}, method: "get", onload: function(data) { encrypStr = data.encrypStr; position = parseInt(data.position, 10); } }); const str = elmData.value.slice(0, position) + elmData.value.slice(position + encrypStr.length); const key = 'undefined' + $(oDoc, '#m-playlist .j-img').dataset.key + $(oDoc, '#song-list-pre-cache a').getAttribute('href').slice(9,12); const text = decodeURIComponent(decrypt(str, key)); // Parse & modify json data const data = JSON.parse(text); data.forEach(song => PV.fix(song.pv)) // Hook JSON.parse const hooker = new Hooker(); const id = hooker.hook(oWin, 'JSON.parse', false, false, { dealer: function(_this, args) { if (args[0] === text) { hooker.map[id].config.hook_return.value = data; hooker.unhook(id); DoLog('Value changed, JSON.parse unhooked...'); DoLog(data); } return [_this, args]; } }).id; DoLog(LogLevel.Success, 'JSON.parse Hooked...'); /*Object.defineProperty(elmData, 'value', { get: e => {debugger;} });*/ } } function Privileger() { const P = this; const levelData = {}; P.levelData = MakeReadonlyObj(levelData); P.fix = fix; function fix(privilege) { const RATES = { 'none': 0, 'standard': 128000, 'exhigh': 320000, 'lossless': 999000, }; const dlLevel = privilege.downloadMaxBrLevel; const dlRate = RATES[dlLevel]; const plLevel = privilege.playMaxBrLevel; const plRate = RATES[plLevel]; privilege.dlLevel = dlLevel; // Download privilege.dl = dlRate; // Download privilege.plLevel = plLevel; // Play privilege.pl = plRate; // Play privilege.st = 0; // Copyright levelData[privilege.id] = {dlLevel, dlRate, plLevel, plRate}; } } function downloadSong(id) { const qualities = Object.keys(CONST.TYPE_INFO).map(n => parseInt(n, 10)).sort((q1, q2) => q2 - q1); search(id, function(song) { if (song) { const q = qualities.find(q => song.quality.includes(q)); const fname = `${song.name} - ${song.artist.join(',')}`; const ext = CONST.TYPE_INFO[q]; const coverPath = new URL(song.cover).pathname; const coverExt = coverPath.match(/\.[a-zA-Z]+?$/) ? coverPath.match(/\.[a-zA-Z]+?$/)[0] : '.jpg'; song.url[q] && dl(song.url[q], `${fname}.${ext}`, false); song.lrc && GM_getValue('lrc', false) && dl(song.lrc, `${fname}.lrc`, false); song.cover && GM_getValue('cover', false) && dl(song.cover, fname + coverExt, false); } else { showTip(CONST.Text.SongNotFound); DoLog(LogLevel.Warning, 'No search r###lt matched.'); } }); } function search(id, callback) { // Get NeateaseMusic music info WEAPI.song_detail(id, function(data) { // Get info const song = data.songs[0]; const name = song.name || ''; const artist = song.ar.map((ar) => (ar.name)).join(',') || ''; const cover = song.al.picUrl || ''; // Gather info const fname = replaceText('{$NAME} - {$ARTIST}', {'{$NAME}': name, '{$ARTIST}': artist}); const cpath = getUrlPath(cover); // Search MyFreeMP3 search_song(); function search_song(page=1, api='new') { const fullList = []; doSearch(page, api); function doSearch(page, api) { Mfapi.search({ text: fname, page: page, type: { old: 'YQB', new: 'YQD' }, callback: onsearch, api }); } function onsearch(json) { fullList.push.apply(fullList, json.list); const song = get_song(json.list, json.noMore || page >= 3); song ? callback(song) : doSearch(page+1, json.api); function get_song(list, force=false) { const exact = list.find(song => getUrlPath(song.cover) === cpath); const bestMatch = fullList.reduce((best, song) => { const nameMed = calcMed(song.name, name); const artistMed = calcMed(song.artist.join(','), artist); const med = nameMed + artistMed; if (med < best.med) { best.med = med; best.songs = [song]; } else if (med === best.med) { best.songs.push(song); } return best; }, { med: Infinity, songs: [] }); if (exact) { DoLog(['exact matched', exact]); return exact; } else if (bestMatch.med === 0) { DoLog(['name and artist matched', bestMatch.songs]); return getBestQualitySong(bestMatch); } else if (force) { DoLog(['matched', bestMatch]); return getBestQualitySong(bestMatch); } else { DoLog('not found'); return null; } function getBestQualitySong(bestMatch) { return bestMatch.songs.reduce((best, cur) => Math.max(...best.quality) > Math.max(...cur.quality) ? best : cur); } } } } }); } function Weapi() { const W = this; W.song_detail = song_detail; W.encrypt = encrypt; function song_detail(id, callback, onerror) { const data = {c: JSON.stringify([{id: id}]), csrfToken: ''}; const xhr = new XMLHttpRequest(); xhr.open('POST', 'https://music.163.com/weapi/v3/song/detail?csrf_token='); xhr.setRequestHeader('content-type', 'application/x-www-form-urlencoded'); xhr.onerror = onerror; xhr.onload = function(e) { try { callback(JSON.parse(xhr.responseText)); } catch(err) { if (onerror) { onerror(err); } else { throw err; } } }; xhr.send(encrypt(data)); }; function encrypt(data) { const json = JSON.stringify(data); const encryted = unsafeWindow.asrsea(json, "010001", "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7", "0CoJUm6Qyw8W8jud"); const xhr_text = 'params=' + encodeURIComponent(encryted.encText) + '&encSecKey=' + encodeURIComponent(encryted.encSecKey); return xhr_text; } } function dl(url, name) { const pop_id = pop.download(name, 'download'); $('#pop-container').style.bottom = ($('.m-playbar').style.top.match(/\d+/)[0]*1 + 10).toString() + 'px'; GM_xmlhttpRequest({ method: 'GET', url: url, responseType: 'blob', onprogress: function(e) { e.lengthComputable /*&& c*/ && (pop.size(pop_id, bytesToSize(e.loaded) + " / " + bytesToSize(e.total)), pop.percent(pop_id, 100 * (e.loaded / e.total) >> 0)) }, 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); pop.finished(pop_id); setTimeout(pop.close.bind(pop, pop_id), 2000); } }); function bytesToSize(a) { if (0 === a) {return "0 B";} var b = #### , c = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"] , d = Math.floor(Math.log(a) / Math.log(b)); return (a / Math.pow(b, d)).toFixed(2) + " " + c[d]; } } function showErr(text, html) { unsafeWindow.isPY_DNG && showTip(text, html); } function showTip(text, html=false) { const elm = $('span.tip'); elm[html ? 'innerHTML' : 'innerText'] = text; elm.style.display = ''; setTimeout(e => (elm.style.display = 'none'), 3000); } function dl_browser(url, name) { const a = $CrE('a'); a.href = url; a.download = name; a.click(); } function dl_GM(url, name, path=true) { name = path ? name : replaceOSSep(name); GM_download({ url: url, name: name }); } function replaceOSSep(text) { const sep = getOSSep(); const rpl = ({'\\': '\', '/': '/'})[sep]; return text.replaceAll(sep, rpl); } function getOSSep() { return ({ 'Windows': '\\', 'Mac': '/', 'Linux': '/', 'Null': '-' })[getOS()]; } function getOS() { const info = (navigator.platform || navigator.userAgent).toLowerCase(); const test = (s) => (info.includes(s)); const map = { 'Windows': ['window', 'win32', 'win64', 'win86'], 'Mac': ['mac', 'os x'], 'Linux': ['linux'] } for (const [sys, strs] of Object.entries(map)) { if (strs.some(test)) { return sys; } } return 'Null'; } function MakeReadonlyObj(val) { return isObject(val) ? new Proxy(val, { get: function(target, property, receiver) { return MakeReadonlyObj(target[property]); }, set: function(target, property, value, receiver) { return true; }, has: function(target, prop) {} }) : val; function isObject(value) { return ['object', 'function'].includes(typeof value) && value !== null; } } // Get the pathname of a given url function getUrlPath(url) { return typeof url === 'string' ? new URL(url).pathname : null; } 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\//, '').replace('/m/', '/').replace('/#/', '/'); const ifr_path = oDoc.URL.replace(/^https?:\/\/music\.163\.com\/?/, '').replace(/^my\/#\//, '').replace('/m/', '/').replace('/#/', '/').replace(/^discover$/, ''); 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 removeEventListener 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); } // Calculate 2 strings' similarity, return number lower means more similarity // MED: Minimal Edit Distance function calcMed(str1, str2) { // Create metrix const metrix = []; for (let i = 0; i < str1.length+1; i++) { metrix[i] = []; } // Fill metrix headers for (let i = 0; i < str1.length+1; i++) { metrix[i][0] = i; } for (let j = 0; j < str2.length+1; j++) { metrix[0][j] = j; } // Calc metrix grids for (let i = 1; i < str1.length+1; i++) { for (let j = 1; j < str2.length+1; j++) { const d1 = metrix[i-1][j] + 1; const d2 = metrix[i][j-1] + 1; const d3 = metrix[i-1][j-1] + (str1.charAt(i-1) === str2.charAt(j-1) ? 0 : 2); metrix[i][j] = Math.min(d1, d2, d3); } } return metrix[str1.length][str2.length]; } })();