Greasy Fork is available in English.

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