// ==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({}),
});
})();