YouTube: Quality Auto Max

To make Quality Auto Max

当前为 2023-12-30 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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

(() => {


  const Promise = (async () => { })().constructor;

  const PromiseExternal = ((resolve_, reject_) => {
    const h = (resolve, reject) => { resolve_ = resolve; reject_ = reject };
    return class PromiseExternal extends Promise {
      constructor(cb = h) {
        super(cb);
        if (cb === h) {
          /** @type {(value: any) => void} */
          this.resolve = resolve_;
          /** @type {(reason?: any) => void} */
          this.reject = reject_;
        }
      }
    };
  })();

  const insp = o => o ? (o.polymerController || o.inst || o || 0) : (o || 0);

  const getResValue = (m) => {

    return m.width < m.height ? m.width : m.height
  }

  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 addProtoToArr = (parent, key, arr) => {


    let isChildProto = false;
    for (const sr of arr) {
      if (parent[key].prototype instanceof parent[sr]) {
        isChildProto = true;
        break;
      }
    }

    if (isChildProto) return;

    arr = arr.filter(sr => {
      if (parent[sr].prototype instanceof parent[key]) {
        return false;
      }
      return true;
    });

    arr.push(key);

    return arr;


  }

  const getuU = (_yt_player) => {
    const w = 'uU';

    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.setPlaybackQualityRange === 'function' && p.setPlaybackQualityRange.length === 3) q += 200;
        if (typeof p.updateVideoData === 'function' && p.updateVideoData.length === 2) q += 80;
        if (p.getVideoAspectRatio) q += 20;
        if (p.getStreamTimeOffset) q += 20;
        // if (typeof p.updatePlaylist ==='function' && p.updatePlaylist.length===1)q += 80;

        if (q > 0) arr = addProtoToArr(_yt_player, k, arr) || arr;

      }

    }

    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];
    }



  }

  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);
    });


    let resultantQualities = null;
    let byPass = false;

    (async () => {


      let pm2 = new PromiseExternal();
      let lastURL = null;

      const fn = async (evt) => {
        try {
          const target = (evt || 0).target
          if (!(target instanceof HTMLVideoElement)) return;
          let _url = lastURL;
          let url = target.src;
          if (url === _url) return;
          lastURL = url;

          pm2.resolve();
          pm2 = new PromiseExternal();
          const ytdPlayerElm = await observablePromise(() => {
            return target.closest('ytd-player#ytd-player')
          }, pm2.then()).obtain();

          if (!ytdPlayerElm) return;
          let player_
          for (let i = 10; --i;) {
            player_ = await ((insp(ytdPlayerElm) || 0).player_ || 0);
            if (player_) break;
            await new Promise(r => setTimeout(r));
          }

          if (!player_) return;
          for (let i = 10; --i;) {
            if (player_.setPlaybackQualityRange) break;
            await new Promise(r => setTimeout(r));
          }

          if (!player_.setPlaybackQualityRange) return;
          if (resultantQualities) {
            let resultantQuality;
            let qualityThreshold = +localStorage.qualityThreshold || 0;
            if (!(qualityThreshold > 60)) qualityThreshold = 0;
            for (const entry of resultantQualities) {
              const entryRes = getResValue(entry);

              if (entryRes > 60 && entry.quality && typeof entry.quality === 'string') {


                if (qualityThreshold === 0 || (qualityThreshold > 60 && entryRes <= qualityThreshold)) {
                  resultantQuality = entry.quality;
                  break;
                }

              }
            }
            if (resultantQuality) {
              byPass = true;

              player_.setPlaybackQualityRange(resultantQuality, resultantQuality)
              byPass = false;
            }
          }
        } catch (e) {
          console.warn(e)
        }
      };
      // document.addEventListener('loadstart', fn, true)
      document.addEventListener('durationchange', fn, true);
    })();


    (async () => {

      try {

        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 keyuU = getuU(_yt_player);
        const keyL0 = getL0(_yt_player);

        if (keyuU) {

          let k = keyuU;
          let gk = g[k];
          let gkp = g[k].prototype;

          gkp.setPlaybackQualityRange132 = gkp.setPlaybackQualityRange;
          gkp.setPlaybackQualityRange = function (...args) {
            if (!byPass && resultantQualities && document.visibilityState === 'visible') {
              if (args[0] === args[1] && typeof args[0] === 'string' && args[0]) {
                const selectionEntry = resultantQualities.filter(e => e.quality === args[0])[0] || 0
                const selectionHeight = selectionEntry ? getResValue(selectionEntry) : 0;
                if (selectionHeight > 60) {
                  localStorage.qualityThreshold = selectionHeight;
                }
              } else if (!args[0] && !args[1]) {
                delete localStorage.qualityThreshold;
              }
            }
            return this.setPlaybackQualityRange132(...args)
          }
        }

        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);


            // console.log(videoTypes)
            if (!videoTypes[0] || !videoTypes[0].quality || !getResValue(videoTypes[0])) 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] = getResValue(videoTypes[0]);
                    // videoData[pey][ley]='1080p Premium'
                    // videoData[pey][ley] = '1080p';
                    videoData[pey]['reason'] = 'm'
                  } else if (typeof val === 'boolean' && val === false) {
                    videoData[pey][ley] = true;
                  }
                }

              }
            }

            // console.log(22, this)

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

            resultantQualities = videoTypes;

            // 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;
          }


        }




      } catch (e) {
        console.warn(e)
      }



    })();

  });

})();