// ==UserScript== // @name YTBetter // @namespace YTBetter // @match https://*.youtube.com/* // @run-at document-start // @grant none // @version 1.1 // @author トワ… // @description Patches YouTube to bypass some limitations // ==/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 TrapVideoConstructor = (value) => new Proxy(value, { construct: (target, argumentsList, newTarget) => { debug("Trapped SE constructor with arguments", target, argumentsList, newTarget); (() => { if(argumentsList.length !== 2) { return; } let videoInfo = argumentsList[1]; 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); if(playerResponse.videoFlags && !playerResponse.videoFlags.playableInEmbed) { try { // We need to patch these to force embeds to work //argumentsList[0].deviceParams.c = "WEB"; for(const key in argumentsList[0]) { if(argumentsList[0][key] === "embedded") { debug("Patching embedded key", key); argumentsList[0][key] = "detailpage"; } } } catch (err) { debug("Failed to patch embeds", err); } } })(); return Reflect.construct(target, argumentsList, newTarget); }, }); const TrapCopyConf = (value) => new Proxy(value, { apply: (target, thisArg, argumentsList) => { if(argumentsList.length != 2) { return Reflect.apply(target, thisArg, argumentsList); } try { let client = argumentsList[1].c; let clientVer = argumentsList[1].cver; if(client === "WEB_EMBEDDED_PLAYER") { debug("Patching WEB_EMBEDDED_PLAYER"); argumentsList[1].c = "WEB"; argumentsList[0].el = "detailpage"; if(!clientVer.includes(".")) { debug("Patching client version"); argumentsList[1].cver = "2." + clientVer; } } } catch (err) { debug("CopyConf error", err); } finally { return Reflect.apply(target, thisArg, argumentsList); } }, }); const TrapConfigFetch = (value) => new Proxy(value, { apply: (target, thisArg, argumentsList) => { if(argumentsList.length == 0) { return Reflect.apply(target, thisArg, argumentsList); } let key = argumentsList[0]; let result = Reflect.apply(target, thisArg, argumentsList); //debug(key, result); switch (key) { case "INNERTUBE_CONTEXT": try { let client = result.client.clientName; let clientVer = result.client.clientVersion; if(client === "WEB_EMBEDDED_PLAYER") { result.client.clientName = "WEB"; if(!clientVer.includes(".")) { debug("Patching client version"); result.client.clientVersion = "2." + clientVer; } } } catch (err) { debug("ConfigFetch error", err); } finally { return result; } //case "INNERTUBE_CONTEXT_CLIENT_NAME": // if (result === 56) { // // YouTube uses this header to determine if you're watching from an embedded video player, // // we have to strip it to force embeds to work even when they're disabled by the author. // debug("Stripping INNERTUBE_CONTEXT_CLIENT_NAME"); // return; // } // break; } return result; }, }); const TrapUpdateVideoInfo = (value) => new Proxy(value, { apply: (target, thisArg, argumentsList) => { (() => { if(argumentsList.length !== 3) { return; } let videoInfo = argumentsList[1]; 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.adaptiveFormats="";/; const ConfigFetchFuncRegex = /^function\(a,b\)\{return a in /; const CopyConfFuncRegex = /^function\(a,b\)\{for\(var c in b\)a\[c\]=b\[c\]\}$/; const UpdateVideoInfoRegex = /a.errorCode=null/; let FoundVideoConstructor = false; let FoundConfigFetch = false; let FoundCopyConf = 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(!FoundCopyConf) { if(CopyConfFuncRegex.test(descriptor.value.toString())) { // CopyConfFunc is used to copy configuration data between objects, // we patch it and change some data as it's copied debug("Found CopyConf func", property, descriptor.value); descriptor.value = TrapCopyConf(descriptor.value); FoundCopyConf = 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; } } if(!FoundConfigFetch) { if(ConfigFetchFuncRegex.test(descriptor.value.toString())) { // ConfigFetch func is used to fetch configuration data used in HTTP headers, // we need to strip some of them to force embeds to work. debug("Found ConfigFetch func", property, descriptor); descriptor.value = TrapConfigFetch(descriptor.value); FoundConfigFetch = true; return; } } })(); return Reflect.defineProperty(target, property, descriptor); }, }); } debug("Script start"); Object.defineProperty(window, "_yt_player", { value: TrapYTPlayer({}), }); })();