YouTube: Quality Auto Max

To make Quality Auto Max

目前為 2023-12-29 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name        YouTube: Quality Auto Max
// @namespace   UserScripts
// @match       https://www.youtube.com/*
// @version     0.1.0
// @author      CY Fung
// @license     MIT
// @description To make Quality Auto Max
// @grant       none
// @run-at      document-start
// @unwrap
// @inject-into page
// ==/UserScript==

(() => {




  const getL0 = (_yt_player) => {
    const w = 'L0';

    let arr = [];

    for (const [k, v] of Object.entries(_yt_player)) {

      const p = typeof v === 'function' ? v.prototype : 0;
      if (p) {
        let q = 0;
        if (typeof p.getPreferredQuality === 'function' && p.getPreferredQuality.length === 0) q += 200;
        if (typeof p.getVideoData === 'function' && p.getVideoData.length === 0) q += 80;
        if (typeof p.isPlaying === 'function' && p.isPlaying.length === 0) q += 2;

        if (typeof p.getPlayerState === 'function' && p.getPlayerState.length === 0) q += 2;

        if (typeof p.getPlayerType === 'function' && p.getPlayerType.length === 0) q += 2;


        if (q > 0) arr.push([k, q])

      }

    }

    if (arr.length === 0) {

      console.warn(`Key does not exist. [${w}]`);
    } else {

      if (arr.length > 1) arr.sort((a, b) => b[1] - a[1]);


      console.log(`[${w}]`, arr);
      return arr[0][0];
    }



  }


  const getZf = (vL0) => {
    const w = 'vL0';

    let arr = [];

    for (const [k, v] of Object.entries(vL0)) {

      // console.log(k,v)

      const p = v;
      if (p) {
        let q = 0;
        if (typeof p.videoData === 'object' && p.videoData) {

          if (Object.keys(p).length === 2) q += 200;

        }


        if (q > 0) arr.push([k, q])

      }

    }

    if (arr.length === 0) {

      // console.warn(`Key does not exist. [${w}]`);
    } else {

      if (arr.length > 1) arr.sort((a, b) => b[1] - a[1]);


      console.log(`[${w}]`, arr);
      return arr[0][0];
    }



  }




  const onRegistryReady = (callback) => {
    if (typeof customElements === 'undefined') {
      if (!('__CE_registry' in document)) {
        // https://github.com/webcomponents/polyfills/
        Object.defineProperty(document, '__CE_registry', {
          get() {
            // return undefined
          },
          set(nv) {
            if (typeof nv == 'object') {
              delete this.__CE_registry;
              this.__CE_registry = nv;
              this.dispatchEvent(new CustomEvent(EVENT_KEY_ON_REGISTRY_READY));
            }
            return true;
          },
          enumerable: false,
          configurable: true
        })
      }
      let eventHandler = (evt) => {
        document.removeEventListener(EVENT_KEY_ON_REGISTRY_READY, eventHandler, false);
        const f = callback;
        callback = null;
        eventHandler = null;
        f();
      };
      document.addEventListener(EVENT_KEY_ON_REGISTRY_READY, eventHandler, false);
    } else {
      callback();
    }
  };

  const cleanContext = async (win) => {
    const waitFn = requestAnimationFrame; // shall have been binded to window
    try {
      let mx = 16; // MAX TRIAL
      const frameId = 'vanillajs-iframe-v1';
      /** @type {HTMLIFrameElement | null} */
      let frame = document.getElementById(frameId);
      let removeIframeFn = null;
      if (!frame) {
        frame = document.createElement('iframe');
        frame.id = frameId;
        const blobURL = typeof webkitCancelAnimationFrame === 'function' ? (frame.src = URL.createObjectURL(new Blob([], { type: 'text/html' }))) : null; // avoid Brave Crash
        frame.sandbox = 'allow-same-origin'; // script cannot be run inside iframe but API can be obtained from iframe
        let n = document.createElement('noscript'); // wrap into NOSCRPIT to avoid reflow (layouting)
        n.appendChild(frame);
        while (!document.documentElement && mx-- > 0) await new Promise(waitFn); // requestAnimationFrame here could get modified by YouTube engine
        const root = document.documentElement;
        root.appendChild(n); // throw error if root is null due to exceeding MAX TRIAL
        if (blobURL) Promise.resolve().then(() => URL.revokeObjectURL(blobURL));

        removeIframeFn = (setTimeout) => {
          const removeIframeOnDocumentReady = (e) => {
            e && win.removeEventListener("DOMContentLoaded", removeIframeOnDocumentReady, false);
            win = null;
            const m = n;
            n = null;
            setTimeout(() => m.remove(), 200);
          }
          if (document.readyState !== 'loading') {
            removeIframeOnDocumentReady();
          } else {
            win.addEventListener("DOMContentLoaded", removeIframeOnDocumentReady, false);
          }
        }
      }
      while (!frame.contentWindow && mx-- > 0) await new Promise(waitFn);
      const fc = frame.contentWindow;
      if (!fc) throw "window is not found."; // throw error if root is null due to exceeding MAX TRIAL
      const { requestAnimationFrame, setTimeout, cancelAnimationFrame, setInterval, clearInterval, requestIdleCallback, getComputedStyle } = fc;
      const res = { requestAnimationFrame, setTimeout, cancelAnimationFrame, setInterval, clearInterval, requestIdleCallback, getComputedStyle };
      for (let k in res) res[k] = res[k].bind(win); // necessary
      if (removeIframeFn) Promise.resolve(res.setTimeout).then(removeIframeFn);
      res.animate = fc.HTMLElement.prototype.animate;
      return res;
    } catch (e) {
      console.warn(e);
      return null;
    }
  };


  const promiseForCustomYtElementsReady = new Promise(onRegistryReady);

  cleanContext(window).then(__CONTEXT__ => {
    if (!__CONTEXT__) return null;

    const { setTimeout } = __CONTEXT__;




    const promiseForTamerTimeout = new Promise(resolve => {
      promiseForCustomYtElementsReady.then(() => {
        customElements.whenDefined('ytd-app').then(() => {
          setTimeout(resolve, 1200);
        });
      });
      setTimeout(resolve, 3000);
    });



    (async () => {


      const observablePromise = (proc, timeoutPromise) => {
        let promise = null;
        return {
          obtain() {
            if (!promise) {
              promise = new Promise(resolve => {
                let mo = null;
                const f = () => {
                  let t = proc();
                  if (t) {
                    mo.disconnect();
                    mo.takeRecords();
                    mo = null;
                    resolve(t);
                  }
                }
                mo = new MutationObserver(f);
                mo.observe(document, { subtree: true, childList: true })
                f();
                timeoutPromise && timeoutPromise.then(() => {
                  resolve(null)
                });
              });
            }
            return promise
          }
        }
      }


      const _yt_player = await observablePromise(() => {
        return (((window || 0)._yt_player || 0) || 0);
      }, promiseForTamerTimeout).obtain();

      if (!_yt_player || typeof _yt_player !== 'object') return;

      const vmHash = new WeakSet();

      const g = _yt_player;
      const keyL0 = getL0(_yt_player);


      if (keyL0) {
        let k = keyL0;
        let gk = g[k];
        let gkp = g[k].prototype;

        let keyZf = null;

        gkp.getVideoData31 = gkp.getVideoData;
        gkp.setupOnNewVideoData61 = function () {

          keyZf = getZf(this);
          if (!keyZf) return;

          const tZf = this[keyZf];

          if (!tZf) return;

          let keyJ = Object.keys(tZf).filter(e => e !== 'videoData')[0]

          const tZfJ = tZf[keyJ];
          const videoData = tZf.videoData;
          if (!tZfJ || !videoData || !tZfJ.videoInfos) return;


          let videoTypes = tZfJ.videoInfos.map(info => info.video);


          if (!videoTypes[0] || !videoTypes[0].quality || !videoTypes[0].height) return;

          let highestQuality = videoTypes[0].quality

          console.log('highestQuality', highestQuality)

          let keyLists = new Set();
          let keyLists2 = new Set();
          const o = {
            [keyZf]: {
              videoData: new Proxy(videoData, {
                get(obj, key) {
                  keyLists.add(key);
                  const v = obj[key];
                  if (typeof v === 'object') return new Proxy(v, {
                    get(obj, key) {
                      keyLists2.add(key);
                      return obj[key]
                    }
                  })
                  return v
                }
              })
            }
          }

          this.getPreferredQuality.call(o)
          // console.log(keyLists.size, keyLists2.size)
          if (keyLists.size !== 2) return;
          if (keyLists2.size < 3) return;



          /*
           * 1080p Premium

              g.k.Nj = function(a) {
                  h_a(this);
                  this.options[a].element.setAttribute("aria-checked", "true");
                  this.Yd(this.Dk(a));
                  this.C = a
              }

          */

          /*
              TP = function(a) {
                  return SP[a.j || a.B] || "auto"
              }
          */

          const [keyAy, keyxU] = [...keyLists];
          const keyLs = [...keyLists2]
          const keyPs = [keyAy, keyxU]

          let cz = 0;
          function inc() {
            for (const pey of keyPs) {

              for (const ley of keyLs) {
                const val = videoData[pey][ley]
                if (typeof val === 'number' && val >= 0 && ~~val === val) {
                  if (!cz) cz = ley;
                  if (cz === ley) {
                    // videoData[pey][ley]  = 5120;
                    // videoData[pey][ley] = videoTypes[0].height;
                    continue
                  }
                  videoData[pey][ley] = videoTypes[0].height;
                  // videoData[pey][ley]='1080p Premium'
                  // videoData[pey][ley] = '1080p';
                  // videoData[pey]['reason']='m'
                } else if (typeof val === 'boolean' && val === false) {
                  videoData[pey][ley] = true;
                }
              }

            }
          }

          inc();
          // console.log(22, this)

          // const keyyU=getyU(_yt_player);
          // _yt_player[keyyU].prototype.


          //           (async()=>{
          //             await customElements.whenDefined('ytd-watch-flexy');
          //             await customElements.whenDefined('ytd-player');

          //             await new Promise(r=>setTimeout(r, 800));
          //             console.log(12321, document.querySelector('ytd-watch-flexy'))
          //             console.log(12322, document.querySelector('ytd-watch-flexy').polymerController.player )

          //             document.querySelector('ytd-watch-flexy').polymerController.player.setPlaybackQuality(highestQuality)
          //             console.log(this.getPreferredQuality())
          //           })();


          // inc();
          // console.log(this.getPreferredQuality())
          // inc();
          // console.log(this.getPreferredQuality())
          // console.log(videoData, keyxU)

          // console.log(this)
          // console.log(1237, keyZf, keyJ, this[keyZf], videoTypes, videoData[keyAy], videoData[keyxU], keyLists2)

        }
        gkp.getVideoData = function () {
          const vd = this.getVideoData31();;
          if (!vd || typeof vd !== 'object') return vd;
          if (!vmHash.has(vd)) {
            vmHash.add(vd);
            this.setupOnNewVideoData61();
            if (!keyZf) vmHash.delete(vd)
          }
          return vd;
        }


      }







    })();

  });

})();