YTBetter

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