// ==UserScript== // @name 计时器掌控者|视频广告跳过|视频广告加速器 // @name:en TimerHooker // @namespace https://gitee.com/HGJing/everthing-hook/ // @version 1.0.62 // @description 控制网页计时器速度|加速跳过页面计时广告|视频快进(慢放)|跳过广告|支持几乎所有网页. // @description:en it can hook the timer speed to change. // @include * // @require https://greasyfork.org/scripts/372672-everything-hook/code/Everything-Hook.js?version=881251 // @author Cangshi // @match http://*/* // @run-at document-start // @grant none // @license GPL-3.0-or-later // ==/UserScript== /** * --------------------------- * Time: 2017/11/20 19:28. * Author: Cangshi * View: http://palerock.cn * --------------------------- */ /** * 1. hook Object.defineProperty | Object.defineProperties * 2. set configurable: true * 3. delete property * 4. can set property for onxx event method */ window.isDOMLoaded = false; window.isDOMRendered = false; document.addEventListener('readystatechange', function () { if (document.readyState === "interactive" || document.readyState === "complete") { window.isDOMLoaded = true; } }); ~function (global) { var workerURLs = []; var extraElements = []; var suppressEvents = {}; var helper = function (eHookContext, timerContext, util) { return { applyUI: function () { var style = '._th-container ._th-item{margin-bottom:3px;position:relative;width:0;height:0;cursor:pointer;opacity:.3;background-color:aquamarine;border-radius:100%;text-align:center;line-height:30px;-webkit-transition:all .35s;-o-transition:all .35s;transition:all .35s;right:30px}._th-container ._th-item,._th-container ._th-click-hover,._th_cover-all-show-times ._th_times{-webkit-box-shadow:-3px 4px 12px -5px black;box-shadow:-3px 4px 12px -5px black}._th-container:hover ._th-item._item-x2{margin-left:18px;width:40px;height:40px;line-height:40px}._th-container:hover ._th-item._item-x-2{margin-left:17px;width:38px;height:38px;line-height:38px}._th-container:hover ._th-item._item-xx2{width:36px;height:36px;margin-left:16px;line-height:36px}._th-container:hover ._th-item._item-xx-2{width:32px;height:32px;line-height:32px;margin-left:14px}._th-container:hover ._th-item._item-reset{width:30px;line-height:30px;height:30px;margin-left:10px}._th-click-hover{position:relative;-webkit-transition:all .5s;-o-transition:all .5s;transition:all .5s;height:45px;width:45px;cursor:pointer;opacity:.3;border-radius:100%;background-color:aquamarine;text-align:center;line-height:45px;right:0}._th-container:hover{left:-5px}._th-container{font-size:12px;-webkit-transition:all .5s;-o-transition:all .5s;transition:all .5s;left:-35px;top:20%;position:fixed;-webkit-box-sizing:border-box;box-sizing:border-box;z-index:100000;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}._th-container ._th-item:hover{opacity:.8;background-color:#5fb492;color:aliceblue}._th-container ._th-item:active{opacity:.9;background-color:#1b3a26;color:aliceblue}._th-container:hover ._th-click-hover{opacity:.8}._th-container:hover ._th-item{opacity:.6;right:0}._th-container ._th-click-hover:hover{opacity:.8;background-color:#5fb492;color:aliceblue}._th_cover-all-show-times{position:fixed;top:0;right:0;width:100%;height:100%;z-index:99999;opacity:1;font-weight:900;font-size:30px;color:#4f4f4f;background-color:rgba(0,0,0,0.1)}._th_cover-all-show-times._th_hidden{z-index:-99999;opacity:0;-webkit-transition:1s all;-o-transition:1s all;transition:1s all}._th_cover-all-show-times ._th_times{width:300px;height:300px;border-radius:50%;background-color:rgba(127,255,212,0.51);text-align:center;line-height:300px;position:absolute;top:50%;right:50%;margin-top:-150px;margin-right:-150px}'; var displayNum = (1 / timerContext._percentage).toFixed(2); // 在页面左边添加一个半圆便于修改 var html = '
\n' + '
\n' + ' x' + displayNum + '\n' + '
\n' + '
>
\n' + '
<
\n' + '
>>
\n' + '
<<
\n' + '
O
\n' + '
\n' + '
\n' + '
x' + displayNum + '
\n' + '
' + ''; var stylenode = document.createElement('style'); stylenode.setAttribute("type", "text/css"); if (stylenode.styleSheet) {// IE stylenode.styleSheet.cssText = style; } else {// w3c var cssText = document.createTextNode(style); stylenode.appendChild(cssText); } var node = document.createElement('div'); node.innerHTML = html; var clickMapper = { '_item-input': function () { changeTime(); }, '_item-x2': function () { changeTime(2, 0, true); }, '_item-x-2': function () { changeTime(-2, 0, true); }, '_item-xx2': function () { changeTime(0, 2); }, '_item-xx-2': function () { changeTime(0, -2); }, '_item-reset': function () { changeTime(0, 0, false, true); } }; Object.keys(clickMapper).forEach(function (className) { var exec = clickMapper[className]; var targetEle = node.getElementsByClassName(className)[0]; if (targetEle) { targetEle.onclick = exec; } }); if (!global.isDOMLoaded) { document.addEventListener('readystatechange', function () { if ((document.readyState === "interactive" || document.readyState === "complete") && !global.isDOMRendered) { document.head.appendChild(stylenode); document.body.appendChild(node); global.isDOMRendered = true; console.log('Time Hooker Works!'); } }); } else { document.head.appendChild(stylenode); document.body.appendChild(node); global.isDOMRendered = true; console.log('Time Hooker Works!'); } }, applyGlobalAction: function (timer) { // 界面半圆按钮点击的方法 timer.changeTime = function (anum, cnum, isa, isr) { if (isr) { global.timer.change(1); return; } if (!global.timer) { return; } var result; if (!anum && !cnum) { var t = prompt("输入欲改变计时器变化倍率(当前:" + 1 / timerContext._percentage + ")"); if (t == null) { return; } if (isNaN(parseFloat(t))) { alert("请输入正确的数字"); timer.changeTime(); return; } if (parseFloat(t) <= 0) { alert("倍率不能小于等于0"); timer.changeTime(); return; } result = 1 / parseFloat(t); } else { if (isa && anum) { if (1 / timerContext._percentage <= 1 && anum < 0) { return; } result = 1 / (1 / timerContext._percentage + anum); } else { if (cnum <= 0) { cnum = 1 / -cnum } result = 1 / ((1 / timerContext._percentage) * cnum); } } timer.change(result); }; global.changeTime = timer.changeTime; }, applyHooking: function () { var _this = this; // 劫持循环计时器 eHookContext.hookReplace(window, 'setInterval', function (setInterval) { return _this.getHookedTimerFunction('interval', setInterval); }); // 劫持单次计时 eHookContext.hookReplace(window, 'setTimeout', function (setTimeout) { return _this.getHookedTimerFunction('timeout', setTimeout) }); // 劫持循环计时器的清除方法 eHookContext.hookBefore(window, 'clearInterval', function (method, args) { _this.redirectNewestId(args); }); // 劫持循环计时器的清除方法 eHookContext.hookBefore(window, 'clearTimeout', function (method, args) { _this.redirectNewestId(args); }); var newFunc = this.getHookedDateConstructor(); eHookContext.hookClass(window, 'Date', newFunc, '_innerDate', ['now']); Date.now = function () { return new Date().getTime(); }; eHookContext.hookedToString(timerContext._Date.now, Date.now); var objToString = Object.prototype.toString; Object.prototype.toString = function toString() { 'use strict'; if (this instanceof timerContext._mDate) { return '[object Date]'; } else { return objToString.call(this); } }; eHookContext.hookedToString(objToString, Object.prototype.toString); eHookContext.hookedToString(timerContext._setInterval, setInterval); eHookContext.hookedToString(timerContext._setTimeout, setTimeout); eHookContext.hookedToString(timerContext._clearInterval, clearInterval); timerContext._mDate = window.Date; this.hookShadowRoot(); }, getHookedDateConstructor: function () { return function () { if (arguments.length === 1) { Object.defineProperty(this, '_innerDate', { configurable: false, enumerable: false, value: new timerContext._Date(arguments[0]), writable: false }); return; } else if (arguments.length > 1) { var definedValue; switch (arguments.length) { case 2: definedValue = new timerContext._Date( arguments[0], arguments[1] ); break; case 3: definedValue = new timerContext._Date( arguments[0], arguments[1], arguments[2], ); break; case 4: definedValue = new timerContext._Date( arguments[0], arguments[1], arguments[2], arguments[3], ); break; case 5: definedValue = new timerContext._Date( arguments[0], arguments[1], arguments[2], arguments[3], arguments[4] ); break; case 6: definedValue = new timerContext._Date( arguments[0], arguments[1], arguments[2], arguments[3], arguments[4], arguments[5] ); break; default: case 7: definedValue = new timerContext._Date( arguments[0], arguments[1], arguments[2], arguments[3], arguments[4], arguments[5], arguments[6] ); break; } Object.defineProperty(this, '_innerDate', { configurable: false, enumerable: false, value: definedValue, writable: false }); return; } var now = timerContext._Date.now(); var passTime = now - timerContext.__lastDatetime; var hookPassTime = passTime * (1 / timerContext._percentage); // console.log(__this.__lastDatetime + hookPassTime, now,__this.__lastDatetime + hookPassTime - now); Object.defineProperty(this, '_innerDate', { configurable: false, enumerable: false, value: new timerContext._Date(timerContext.__lastMDatetime + hookPassTime), writable: false }); }; }, getHookedTimerFunction: function (type, timer) { var property = '_' + type + 'Ids'; return function () { var uniqueId = timerContext.genUniqueId(); var callback = arguments[0]; if (typeof callback === 'string') { callback += ';timer.notifyExec(' + uniqueId + ')'; arguments[0] = callback; } if (typeof callback === 'function') { arguments[0] = function () { var returnValue = callback.apply(this, arguments); timerContext.notifyExec(uniqueId); return returnValue; } } // 储存原始时间间隔 var originMS = arguments[1]; // 获取变速时间间隔 arguments[1] *= timerContext._percentage; var resultId = timer.apply(window, arguments); // 保存每次使用计时器得到的id以及参数等 timerContext[property][resultId] = { args: arguments, originMS: originMS, originId: resultId, nowId: resultId, uniqueId: uniqueId, oldPercentage: timerContext._percentage, exceptNextFireTime: timerContext._Date.now() + originMS }; return resultId; }; }, redirectNewestId: function (args) { var id = args[0]; if (timerContext._intervalIds[id]) { args[0] = timerContext._intervalIds[id].nowId; // 清除该记录id delete timerContext._intervalIds[id]; } if (timerContext._timeoutIds[id]) { args[0] = timerContext._timeoutIds[id].nowId; // 清除该记录id delete timerContext._timeoutIds[id]; } }, registerShortcutKeys: function (timer) { // 快捷键注册 addEventListener('keydown', function (e) { switch (e.keyCode) { case 57: if (e.ctrlKey || e.altKey) { // custom timer.changeTime(); } break; // [=] case 190: case 187: { if (e.ctrlKey) { // console.log('+2'); timer.changeTime(2, 0, true); } else if (e.altKey) { // console.log('xx2'); timer.changeTime(0, 2); } break; } // [-] case 188: case 189: { if (e.ctrlKey) { // console.log('-2'); timer.changeTime(-2, 0, true); } else if (e.altKey) { // console.log('xx-2'); timer.changeTime(0, -2); } break; } // [0] case 48: { if (e.ctrlKey || e.altKey) { // console.log('reset'); timer.changeTime(0, 0, false, true); } break; } default: // console.log(e); } }); }, /** * 当计时器速率被改变时调用的回调方法 * @param percentage * @private */ percentageChangeHandler: function (percentage) { // 改变所有的循环计时 util.ergodicObject(timerContext, timerContext._intervalIds, function (idObj, id) { idObj.args[1] = Math.floor((idObj.originMS || 1) * percentage); // 结束原来的计时器 this._clearInterval.call(window, idObj.nowId); // 新开一个计时器 idObj.nowId = this._setInterval.apply(window, idObj.args); }); // 改变所有的延时计时 util.ergodicObject(timerContext, timerContext._timeoutIds, function (idObj, id) { var now = this._Date.now(); var exceptTime = idObj.exceptNextFireTime; var oldPercentage = idObj.oldPercentage; var time = exceptTime - now; if (time < 0) { time = 0; } var changedTime = Math.floor(percentage / oldPercentage * time); idObj.args[1] = changedTime; // 重定下次执行时间 idObj.exceptNextFireTime = now + changedTime; idObj.oldPercentage = percentage; // 结束原来的计时器 this._clearTimeout.call(window, idObj.nowId); // 新开一个计时器 idObj.nowId = this._setTimeout.apply(window, idObj.args); }); }, hookShadowRoot: function () { var origin = Element.prototype.attachShadow; eHookContext.hookAfter(Element.prototype, 'attachShadow', function (m, args, result) { extraElements.push(result); return result; }, false); eHookContext.hookedToString(origin, Element.prototype.attachShadow); }, hookDefine: function () { const _this = this; eHookContext.hookBefore(Object, 'defineProperty', function (m, args) { var option = args[2]; var ele = args[0]; var key = args[1]; var afterArgs = _this.hookDefineDetails(ele, key, option); afterArgs.forEach((arg, i) => { args[i] = arg; }) }); eHookContext.hookBefore(Object, 'defineProperties', function (m, args) { var option = args[1]; var ele = args[0]; if (ele && ele instanceof Element) { Object.keys(option).forEach(key => { var o = option[key]; var afterArgs = _this.hookDefineDetails(ele, key, o); args[0] = afterArgs[0]; delete option[key]; option[afterArgs[1]] = afterArgs[2] }) } }) }, hookDefineDetails: function (target, key, option) { if (option && target && target instanceof Element && typeof key === 'string' && key.indexOf('on') >= 0) { option.configurable = true; } if (target instanceof HTMLVideoElement && key === 'playbackRate') { option.configurable = true; console.warn('[Timer Hook]', '已阻止默认操作视频倍率'); key = 'playbackRate_hooked' } return [target, key, option]; }, suppressEvent: function (ele, eventName) { if (ele) { delete ele['on' + eventName]; delete ele['on' + eventName]; delete ele['on' + eventName]; ele['on' + eventName] = undefined; } if (!suppressEvents[eventName]) { eHookContext.hookBefore(EventTarget.prototype, 'addEventListener', function (m, args) { var eName = args[0]; if (eventName === eName) { console.warn(eventName, 'event suppressed.') args[0] += 'suppressed'; } }, false); suppressEvents[eventName] = true; } }, changePlaybackRate: function (ele, rate) { delete ele.playbackRate; delete ele.playbackRate; delete ele.playbackRate; ele.playbackRate = rate if (rate !== 1) { timerContext.defineProperty.call(Object, ele, 'playbackRate', { configurable: true, get: function () { return 1; }, set: function () { } }); } } } }; var normalUtil = { isInIframe: function () { let is = global.parent !== global; try { is = is && global.parent.document.body.tagName !== 'FRAMESET' } catch (e) { // ignore } return is; }, listenParentEvent: function (handler) { global.addEventListener('message', function (e) { var data = e.data; var type = data.type || ''; if (type === 'changePercentage') { handler(data.percentage || 0); } }) }, sentChangesToIframe: function (percentage) { var iframes = document.querySelectorAll('iframe') || []; var frames = document.querySelectorAll('frame'); if (iframes.length) { for (var i = 0; i < iframes.length; i++) { iframes[i].contentWindow.postMessage( {type: 'changePercentage', percentage: percentage}, '*'); } } if (frames.length) { for (var j = 0; j < frames.length; j++) { frames[j].contentWindow.postMessage( {type: 'changePercentage', percentage: percentage}, '*'); } } } }; var querySelectorAll = function (ele, selector, includeExtra) { var elements = ele.querySelectorAll(selector); elements = Array.prototype.slice.call(elements || []); if (includeExtra) { extraElements.forEach(function (element) { elements = elements.concat(querySelectorAll(element, selector, false)); }) } return elements; }; var generate = function () { return function (util) { // disable worker workerURLs.forEach(function (url) { if (util.urlMatching(location.href, 'http.*://.*' + url + '.*')) { window['Worker'] = undefined; console.log('Worker disabled'); } }); var eHookContext = this; var timerHooker = { // 用于储存计时器的id和参数 _intervalIds: {}, _timeoutIds: {}, _auoUniqueId: 1, // 计时器速率 __percentage: 1.0, // 劫持前的原始的方法 _setInterval: window['setInterval'], _clearInterval: window['clearInterval'], _clearTimeout: window['clearTimeout'], _setTimeout: window['setTimeout'], _Date: window['Date'], __lastDatetime: new Date().getTime(), __lastMDatetime: new Date().getTime(), videoSpeedInterval: 1000, defineProperty: Object.defineProperty, defineProperties: Object.defineProperties, genUniqueId: function () { return this._auoUniqueId++; }, notifyExec: function (uniqueId) { var _this = this; if (uniqueId) { // 清除 timeout 所储存的记录 var timeoutInfos = Object.values(this._timeoutIds).filter( function (info) { return info.uniqueId === uniqueId; } ); timeoutInfos.forEach(function (info) { _this._clearTimeout.call(window, info.nowId); delete _this._timeoutIds[info.originId] }) } // console.log(uniqueId, 'called') }, /** * 初始化方法 */ init: function () { var timerContext = this; var h = helper(eHookContext, timerContext, util); h.hookDefine(); h.applyHooking(); // 设定百分比属性被修改的回调 Object.defineProperty(timerContext, '_percentage', { get: function () { return timerContext.__percentage; }, set: function (percentage) { if (percentage === timerContext.__percentage) { return percentage; } h.percentageChangeHandler(percentage); timerContext.__percentage = percentage; return percentage; } }); if (!normalUtil.isInIframe()) { console.log('[TimeHooker]', 'loading outer window...'); h.applyUI(); h.applyGlobalAction(timerContext); h.registerShortcutKeys(timerContext); } else { console.log('[TimeHooker]', 'loading inner window...'); normalUtil.listenParentEvent((function (percentage) { console.log('[TimeHooker]', 'Inner Changed', percentage) this.change(percentage); }).bind(this)) } }, /** * 调用该方法改变计时器速率 * @param percentage */ change: function (percentage) { this.__lastMDatetime = this._mDate.now(); this.__lastDatetime = this._Date.now(); this._percentage = percentage; var oldNode = document.getElementsByClassName('_th-click-hover'); var oldNode1 = document.getElementsByClassName('_th_times'); var displayNum = (1 / this._percentage).toFixed(2); (oldNode[0] || {}).innerHTML = 'x' + displayNum; (oldNode1[0] || {}).innerHTML = 'x' + displayNum; var a = document.getElementsByClassName('_th_cover-all-show-times')[0] || {}; a.className = '_th_cover-all-show-times'; this._setTimeout.bind(window)(function () { a.className = '_th_cover-all-show-times _th_hidden'; }, 100); this.changeVideoSpeed(); normalUtil.sentChangesToIframe(percentage); }, changeVideoSpeed: function () { var timerContext = this; var h = helper(eHookContext, timerContext, util); var rate = 1 / this._percentage; rate > 16 && (rate = 16); rate < 0.065 && (rate = 0.065); var videos = querySelectorAll(document, 'video', true) || []; if (videos.length) { for (var i = 0; i < videos.length; i++) { h.changePlaybackRate(videos[i], rate); } } } }; // 默认初始化 timerHooker.init(); return timerHooker; } }; if (global.eHook) { global.eHook.plugins({ name: 'timer', /** * 插件装载 * @param util */ mount: generate() }); } }(window);