// ==UserScript==
// @name My Free MP3+
// @namespace http://tampermonkey.net/My Free MP3 Plus
// @version 0.2.5
// @description 解锁MyFreeMP3的QQ音乐、酷狗音乐、酷我音乐,过广告拦截器检测,所有下载全部转为页面内直链下载
// @author PY-DNG
// @require https://gf.qytechs.cn/scripts/456034-basic-functions-for-userscripts/code/script.js?version=1226884
// @match http*://tool.liumingye.cn/music_old/*
// @match http*://tools.liumingye.cn/music_old/*
// @match http*://tool.liumingye.cn/music/*
// @match http*://tools.liumingye.cn/music/*
// @connect kugou.com
// @connect *
// @grant GM_xmlhttpRequest
// @grant GM_download
// @run-at document-start
// ==/UserScript==
/* global LogLevel DoLog Err $ $All $CrE $AEL $$CrE addStyle detectDom destroyEvent copyProp copyProps parseArgs escJsStr replaceText getUrlArgv dl_browser dl_GM AsyncManager */
/* global pop */
(function() {
'use strict';
// Main loader
main();
function main() {
// Collect all funcs from page objs
const pages = [music, music_old].map(f => f());
const func_immediate = [], func_load = [];
for (const page of pages) {
page.regurl.test(location.href) &&
page.funcs.forEach(funcobj => (funcobj.onload ? func_load : func_immediate).push(funcobj.func));
}
// Exec
const exec = funcs => funcs.forEach(func => func());
exec(func_immediate);
$AEL(window, 'load', exec.bind(null, func_load));
}
// 新版页面
function music() {
return {
regurl: /^https?:\/\/tools?\.liumingye\.cn\/music\//,
funcs: [{
func: downloadInPage,
onload: false
}]
}
function downloadInPage() {
const hooker = new Hooker();
const xhrs = [];
const hookedURLs = ['https://api.liumingye.cn/m/api/search', 'https://api.liumingye.cn/m/api/home/recommend', 'https://api.liumingye.cn/m/api/top/song'];
const openHooerId = hooker.hook(XMLHttpRequest.prototype, 'open', false, false, {
dealer(_this, args) {
if (hookedURLs.some(url => args[1].includes(url))) {
xhrs.push(_this);
}
return [_this, args];
}
});
const sendHooerId = hooker.hook(XMLHttpRequest.prototype, 'send', false, false, {
dealer(_this, args) {
if (xhrs.includes(_this)) {
const callbackName = 'onloadend' in _this ? 'onloadend' : 'onreadystatechange';
const callback = _this[callbackName];
_this[callbackName] = function() {
const json = JSON.parse(this.response);
json.data.list.forEach(song => song.quality.forEach((q, i) => typeof q !== 'number' && (song.quality[i] = parseInt(q.name, 10))));
rewriteResponse(this, json);
callback.apply(this, arguments);
}
xhrs.splice(xhrs.indexOf(_this), 1);
}
return [_this, args];
}
});
DoLog(`XMLHttpRequest Hooked: ${openHooerId}, ${sendHooerId}`);
}
}
// 旧版页面
function music_old() {
return {
regurl: /^https?:\/\/tools?\.liumingye\.cn\/music_old\//,
funcs: [{
func: unlockTencent,
onload: true
}, {
func: downloadInPage,
onload: true
}, {
func: bypassAdkillerDetector,
onload: false
}]
};
// 解锁QQ音乐、酷狗音乐、酷我音乐函数
function unlockTencent() {
// 模拟双击
const search_title = $('#search .home-title');
const eDblclick = new Event('dblclick');
search_title.dispatchEvent(eDblclick);
// 去除双击事件
const p = search_title.parentElement;
const new_search_title = $CrE('div');
new_search_title.className = search_title.className;
new_search_title.innerHTML = search_title.innerHTML;
p.removeChild(search_title);
p.insertBefore(new_search_title, p.children[0]);
}
// Hook掉下载按钮实现全部下载均采用页面内下载方式(重写下载逻辑)
function downloadInPage() {
$AEL(document.body, 'click', onclick, {capture: true});
function onclick(e) {
const elm = e.target;
const parent = elm ? elm.parentElement : null;
match(elm);
match(parent);
function match(elm) {
const tag = elm.tagName.toUpperCase();
const clList = [...elm.classList];
if (tag === 'A' && clList.includes('download') || clList.includes('pic_download')) {
e.stopPropagation();
e.preventDefault();;
download(elm);
}
}
}
function download(a) {
const elm_data = a.parentElement.previousElementSibling;
const url = elm_data.value;
const name = $("#name").value;
const pop_id = pop.download(name, 'download');
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))
},
onerror: function(e) {
console.log(e);
window.open(url);
},
onload: function(response) {
const blob = response.response;
const dataUrl = URL.createObjectURL(blob);
const ext = getExtname(elm_data.id, blob.type.split(';')[0]);
saveFile(dataUrl, `${name}.${ext}`);
setTimeout(URL.revokeObjectURL.bind(URL, dataUrl), 1000);
pop.finished(pop_id);
setTimeout(pop.close.bind(pop, pop_id), 2000);
}
});
function getExtname(...args) {
const map = {
url_dsd: "flac",
url_flac: "flac",
url_ape: "ape",
url_320: "mp3",
url_128: "mp3",
url_m4a: "m4a",
url_lrc: "lrc",
'image/png': 'png',
'image/jpg': 'jpg',
'image/gif': 'gif',
'image/bmp': 'bmp',
'image/jpeg': 'jpeg',
'image/webp': 'webp',
'image/tiff': 'tiff',
'image/vnd.microsoft.icon': 'ico',
};
return map[args.find(a => map[a])];
}
function bytesToSize(a) {
if (0 === a) {
return "0 B";
}
var b = 1024
, 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 bypassAdkillerDetector() {
/*
// 拦截广告拦截检测器的setTimeout延迟启动器
// 优点:不用考虑#music_tool是否存在,不用反复执行;缺点:需要在setTimeout启动器注册(不可用)前执行,如果脚本加载缓慢,就来不及了
const setTimeout = unsafeWindow.setTimeout;
unsafeWindow.setTimeout = function(func, time) {
if (func && func.toString().includes('$("#music_tool").html()')) {
func = function() {};
}
setTimeout.call(this, func, time);
}
*/
/*
// 拦截广告拦截检测器的innerHTML检测
// 优点:对浏览器API没有影响,对DOM影响极小,在检测前执行即可;缺点:需要#music_tool存在,需要反复检测执行,影响性能,稳定性差
const bypasser = () => {
const elm = $('#music_tool');
elm && Object.defineProperty($('#music_tool'), 'innerHTML', {get: () => '<iframe></iframe>'});
};
setTimeout(bypasser, 2000);
bypasser();
*/
// 在页面添加干扰元素
// 优点:对浏览器API没有影响,对DOM几乎没有影响,在检测前执行即可,不用考虑#music_tool是否存在,不用反复执行;缺点:可能影响广告功能(乐
document.body.firstChild.insertAdjacentHTML('beforebegin', '<ins id="music_tool" style="display: none !important;">sometext</ins>');
}
}
// Save dataURL to file
function saveFile(dataURL, filename) {
const a = $CrE('a');
a.href = dataURL;
a.download = filename;
a.click();
}
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 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
});
}
})();