Patches YouTube to bypass some limitations
// ==UserScript== // @name YTBetter // @namespace YTBetter // @match https://*.youtube.com/* // @run-at document-start // @grant none // @version 1.3 // @author hop-step-pokosan // @description Patches YouTube to bypass some limitations // @license MIT // ==/UserScript== (() => { const _DEBUG = false; const debug = (...msg) => { if (_DEBUG) { console.log("[YTBetter]", ...msg); } } const PatchPlayerResponse = (playerResponse) => { try { // Patch to allow DVR to work on all streams if (playerResponse.videoDetails) { playerResponse.videoDetails.isLiveDvrEnabled = true; } } catch (err) { debug("Failed to patch playerResponse", err); } }; const GetPlayerResponse = (videoInfo) => { return videoInfo.raw_player_response || videoInfo.embedded_player_response || videoInfo.player_response; }; const TrapLoadVideoByPlayerVars = (value) => new Proxy(value, { apply: (target, thisArg, argumentsList) => { (() => { if (argumentsList.length !== 5) { return; } let videoInfo = argumentsList[0]; if (typeof videoInfo === "undefined") { return; } let playerResponse = GetPlayerResponse(videoInfo); if (typeof playerResponse === "undefined") { return; } if (typeof playerResponse === "string") { playerResponse = JSON.parse(playerResponse); delete videoInfo.player_response; delete videoInfo.embedded_player_response; } PatchPlayerResponse(playerResponse); })(); debug("TrapLoadVideoByPlayerVars", thisArg, argumentsList); return Reflect.apply(target, thisArg, argumentsList); }, }); const TrapConstructorPrototype = (value) => new Proxy(value, { defineProperty: (target, property, descriptor) => { (() => { if (property !== "loadVideoByPlayerVars") { return; } descriptor.value = TrapLoadVideoByPlayerVars(descriptor.value); })(); return Reflect.defineProperty(target, property, descriptor); }, }); const TrapConstructorCreate = (value) => new Proxy(value, { apply: (target, thisArg, argumentsList) => { debug("TrapConstructorCreate", thisArg, argumentsList); (() => { if (argumentsList.length !== 3) { return; } let videoInfo = argumentsList[1]?.args; if (typeof videoInfo === "undefined") { return; } let playerResponse = GetPlayerResponse(videoInfo); if (typeof playerResponse === "undefined") { return; } if (typeof playerResponse === "string") { playerResponse = JSON.parse(playerResponse); delete videoInfo.player_response; delete videoInfo.embedded_player_response; } PatchPlayerResponse(playerResponse); })(); return Reflect.apply(target, thisArg, argumentsList); }, }); const TrapVideoConstructor = (value) => new Proxy(value, { defineProperty: (target, property, descriptor) => { (() => { switch (property) { case "prototype": descriptor.value = TrapConstructorPrototype(descriptor.value); case "create": descriptor.value = TrapConstructorCreate(descriptor.value); default: return; } descriptor.value = TrapConstructorPrototype(descriptor.value); })(); return Reflect.defineProperty(target, property, descriptor); }, }); const TrapUpdateVideoInfo = (value) => new Proxy(value, { apply: (target, thisArg, argumentsList) => { (() => { if (argumentsList.length !== 3) { return; } let videoInfo = argumentsList[1]; if (typeof videoInfo === "undefined") { return; } let playerResponse = GetPlayerResponse(videoInfo); if (typeof playerResponse === "undefined") { return; } if (typeof playerResponse === "string") { playerResponse = JSON.parse(playerResponse); delete videoInfo.player_response; delete videoInfo.embedded_player_response; } PatchPlayerResponse(playerResponse); })(); debug("TrapUpdateVideoInfo", thisArg, argumentsList); return Reflect.apply(target, thisArg, argumentsList); }, }); const TrapYTPlayer = (value) => { const VideoConstructorFuncRegex = /this.webPlayerContextConfig=/; const UpdateVideoInfoRegex = /a.errorCode=null/; let FoundVideoConstructor = false; let FoundUpdateVideoInfo = false; return new Proxy(value, { defineProperty: (target, property, descriptor) => { (() => { if (typeof descriptor.value !== "function") { return; } if (!FoundUpdateVideoInfo) { if (UpdateVideoInfoRegex.test(descriptor.value.toString())) { // UpdateVideoInfo is used for embeded videos, we need to trap // it to enable DVR on embeds. debug("Found UpdateVideoInfo func", property, descriptor.value); descriptor.value = TrapUpdateVideoInfo(descriptor.value); FoundUpdateVideoInfo = true; return; } } if (!FoundVideoConstructor) { if (VideoConstructorFuncRegex.test(descriptor.value.toString())) { // VideoConstructor func is the constructor for videos, // we use it to patch some data when new videos are loaded. debug("Found VideoConstructor func", property, descriptor.value); descriptor.value = TrapVideoConstructor(descriptor.value); FoundVideoConstructor = true; return; } } })(); return Reflect.defineProperty(target, property, descriptor); }, }); } debug("Script start"); Object.defineProperty(window, "_yt_player", { value: TrapYTPlayer({}), }); })();