Greasy Fork

Greasy Fork is available in English.

Netease Music - MyFreeMP3 Extender

Extend netease music with MyFreeMP3

当前为 2022-10-27 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

/* eslint-disable no-multi-spaces */
/* eslint-disable dot-notation */

// ==UserScript==
// @name               Netease Music - MyFreeMP3 Extender
// @name:zh-CN         网易云音乐-MyFreeMP3扩展
// @name:en            Netease Music - MyFreeMP3 Extender
// @namespace          163Music-MyFreeMP3-Extender
// @version            0.2
// @description        Extend netease music with 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
// @noframes
// ==/UserScript==

(function __MAIN__() {
    'use strict';
	// 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);
	const addEventListener = getPureAEL();

	// Arguments: level=LogLevel.Info, logContent, asObject=false
    // Needs one call "DoLog();" to get it initialized before using it!
    function DoLog() {
        // Global log levels set
        unsafeWindow.LogLevel = {
            None: 0,
            Error: 1,
            Success: 2,
            Warning: 3,
            Info: 4,
        }
        unsafeWindow.LogLevelMap = {};
        unsafeWindow.LogLevelMap[LogLevel.None]     = {prefix: ''          , color: 'color:#ffffff'}
        unsafeWindow.LogLevelMap[LogLevel.Error]    = {prefix: '[Error]'   , color: 'color:#ff0000'}
        unsafeWindow.LogLevelMap[LogLevel.Success]  = {prefix: '[Success]' , color: 'color:#00aa00'}
        unsafeWindow.LogLevelMap[LogLevel.Warning]  = {prefix: '[Warning]' , color: 'color:#ffa500'}
        unsafeWindow.LogLevelMap[LogLevel.Info]     = {prefix: '[Info]'    , color: 'color:#888888'}
        unsafeWindow.LogLevelMap[LogLevel.Elements] = {prefix: '[Elements]', color: 'color:#000000'}

        // Current log level
        DoLog.logLevel = (unsafeWindow ? unsafeWindow.isPY_DNG : window.isPY_DNG) ? 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();

	addEventListener(unsafeWindow, 'load', function() {
		const ITM = new IntervalTaskManager();
		const pageChangeDetecter = (function(callback) {
			let href = location.href;
			return function detecter() {
				const new_href = location.href;
				if (href !== new_href) {
					callback(href, new_href);
					href = new_href;
				}
			}
		}) (deliverPageFuncs);
		ITM.addTask(pageChangeDetecter);
		ITM.start();
		deliverPageFuncs();

		function deliverPageFuncs() {
			const pageFuncs = [
				[/^https?:\/\/music\.163\.com\/#\/song\?.+$/, pageSong, function() {
					const ifr = $('#g_iframe'); if (!ifr) {return false;}
					const oDoc = ifr.contentDocument; if (!oDoc) {return false;}
					const test = $(oDoc, '.cnt>.m-info');
					return test;
				}]
			];
			for (const pageFunc of pageFuncs) {
				test_exec(pageFunc);
			}

			function test_exec(pageFunc) {
				pageFunc[0].test(location.href) && ((pageFunc[2] ? ({
					'string': () => ($(pageFunc[2])),
					'function': pageFunc[2],
				})[typeof pageFunc[2]]() : true) ? true : (setTimeout(test_exec.bind(null, pageFunc), 500), DoLog('waiting...'), false)) && pageFunc[1]();
			}
		}
	});


	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 fname = replaceText('{$NAME} - {$ARTIST}', {'{$NAME}': name, '{$ARTIST}': artist});
		const cover = $(oDoc, '.u-cover>img.j-img').src;
		const cpath = getUrlPath(cover);

		// 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 = '要听龙叔的话,V5不能屈,听完歌快去给你网易爸爸充VIP吧';
			vip_play.children[0].childNodes[1].nodeValue = '播放';
		}

		// 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 = $(oDoc, '.tit>em').innerText;
				const artist = $(oDoc, '.cnt>.des>span>a').innerText;
				const fname = replaceText('{$NAME} - {$ARTIST}', {'{$NAME}': name, '{$ARTIST}': artist});
				const cover = $(oDoc, '.u-cover>img.j-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');
		}

		// Download
		const dlButton = $(oDoc, '#content-operation>a[data-res-action="download"]');
		addEventListener(dlButton, 'click', dlOnclick, {useCapture: true});

		function dlOnclick(e) {
			e.stopPropagation();
			search({
				text: fname,
				callback: onsearch,
			});
		}

		function onsearch(json) {
			const list = json.data.list;
			for (const song of list) {
				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) {
					dl_GM(s_url, s_fname + '.' + s_ftype);
					dl(s_lrc, s_fname + '.lrc');
					dl(s_cover, s_fname + s_cpath.match(/\.[a-zA-Z]+?$/)[0]);
					break;
				}
			}
		}
	}

	// Get unpolluted addEventListener
	function getPureAEL() {
		const ifr = document.createElement('iframe');
		ifr.srcdoc = '<html></html>';
		ifr.style.width = ifr.style.height = ifr.style.border = ifr.style.padding = ifr.style.margin = '0';
		document.body.appendChild(ifr);

		const oWin = ifr.contentWindow;
		const oDoc = ifr.contentDocument;

		const AEL = oWin.EventTarget.prototype.addEventListener;
		return AEL.call.bind(AEL);
	}

	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) {
		const a = $CrE('a');
		a.href = url;
		return a.pathname;
	}

	// 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 APIHooker() {
		const AH = this;
		const hooker = new Hooker();
		const hooks = [];
		const addEventListener = (function() {
			const AEL = getPureAEL();
			return function() {
				const args = Array.from(arguments);
				const _this = args.shift();
				AEL.apply(_this, args);
			}
		}) ();

		AH.hook = hook;
		AH.unhook = unhook;

		init();

		function hook(urlMatcher, xhrDealer) {
			return hooks.push({
				matcher: urlMatcher,
				dealer: xhrDealer,
				xhrs: []
			}) - 1;
		}

		function unhook(id) {
			hooks.splice(id, 1);
		}

		function init() {
			const iframe = $('#g_iframe');
			const oWin = iframe.contentWindow;

			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];
				}
			}

			// Hook open: filter all xhr that should be hooked
			hooker.hook(window, 'XMLHttpRequest.prototype.open', false, false, {
				dealer: hook_dealers.open
			});
			hooker.hook(oWin, 'XMLHttpRequest.prototype.open', false, false, {
				dealer: hook_dealers.open
			});

			// Hook send: change eventListeners for each hooked xhr, and apply xhr dealer
			hooker.hook(window, 'XMLHttpRequest.prototype.send', false, false, {
				dealer: hook_dealers.send
			});
			hooker.hook(oWin, 'XMLHttpRequest.prototype.send', false, false, {
				dealer: hook_dealers.send
			});
		}

		function matchUrl(url, matcher) {
			if (matcher instanceof RegExp) {
				return !!url.match(matcher);
			}
			if (typeof matcher === 'function') {
				return matcher(url);
			}
		}

		// 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;
		}

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

				// hook functions
				map[id].config.log && console.log([base, path.join('.')], _this, args);
				if (map[id].config.apply_debugger) {debugger;}
				if (typeof map[id].config.hook_return.dealer === 'function') {
					[_this, args] = map[id].config.hook_return.dealer(_this, args);
				}

				// continue stack
				return map[id].config.hook_return && map[id].config.hook_return.hasOwnProperty('value') ? map[id].config.hook_return.value : target.apply(_this, args);
			}
			parent[prop] = hooker;

			// Id
			const id = makeid();
			map[id] = {
				id: id,
				parent: parent,
				target: target,
				hooker: hooker,
				config: {
					log: log,
					apply_debugger: apply_debugger,
					hook_return: hook_return
				}
			};

			return map[id];
		}

		function unhook(id) {
			// unhook
			map[id].parent = map[id].target;
			delete map[id];
		}

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