Greasy Fork is available in English.

YouTube: Quality Auto Max

To make Quality Auto Max

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name        YouTube: Quality Auto Max
// @namespace   UserScripts
// @match       https://www.youtube.com/*
// @version     0.3.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 = [];
        let brr = new Map();

        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 < 200) continue; // p.setPlaybackQualityRange must be available

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

                if (q > 0) brr.set(k, q);

            }

        }

        if (arr.length === 0) {

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

            arr = arr.map(key => [key, (brr.get(key) || 0)]);

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

            console.log(`[${w}]`, arr);
            return arr[0][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 < 280) continue; // p.getPreferredQuality and p.getVideoData must be available

                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 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' && typeof kagi === 'undefined' ? (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 isUrlInEmbed = location.href.includes('.youtube.com/embed/');
    const isAbortSignalSupported = typeof AbortSignal !== "undefined";

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

        const { setTimeout } = __CONTEXT__;

        const promiseForTamerTimeout = new Promise(resolve => {
            !isUrlInEmbed && isAbortSignalSupported && document.addEventListener('yt-action', function () {
                setTimeout(resolve, 480);
            }, { capture: true, passive: true, once: true });
            !isUrlInEmbed && isAbortSignalSupported && typeof customElements === "object" && customElements.whenDefined('ytd-app').then(() => {
                setTimeout(resolve, 1200);
            });
            setTimeout(resolve, 3000);
        });


        let resultantQualities = null;
        let byPass = false;

        // @@ durationchange @@
        let pm2 = new PromiseExternal();
        let lastURL = null;

        let activatedOnce = false;

        const fn = async (evt) => {
            try {
                const target = (evt || 0).target
                if (!(target instanceof HTMLMediaElement)) return;
                pm2.resolve();
                const pm1 = pm2 = new PromiseExternal();
                const mainMedia = await observablePromise(() => {
                    return isUrlInEmbed ? document.querySelector('#movie_player .html5-main-video') : document.querySelector('ytd-player#ytd-player #movie_player .html5-main-video')
                }, pm2.then()).obtain();
                if (!mainMedia) return;
                if (pm1 !== pm2) return;
                const ytdPlayerElm = isUrlInEmbed ? mainMedia.closest('#movie_player') : mainMedia.closest('ytd-player#ytd-player');
                if (!ytdPlayerElm) return;

                let player_
                for (let i = 10; --i;) {
                    player_ = isUrlInEmbed ? ytdPlayerElm : await ((insp(ytdPlayerElm) || 0).player_ || 0);
                    if (player_) break;
                    if (pm1 !== pm2) return;
                    await new Promise(r => setTimeout(r, 18));
                }

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

                if (pm1 !== pm2) return;
                if (typeof player_.setPlaybackQualityRange !== 'function') return;

                let _url = lastURL;
                let url = mainMedia.src;
                if (url === _url) return;
                lastURL = url;

                if (resultantQualities) {
                    !activatedOnce && console.log('YouTube Quality Auto Max is activated.');
                    activatedOnce = true;
                    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;
                        const setItemN = function (a, b) {
                            // console.log('setItem', ...arguments)
                            // yt-player-quality
                            // yt-player-performance-cap
                            // yt-player-performance-cap-active-set
                        };
                        const pd = Object.getOwnPropertyDescriptor(localStorage.constructor.prototype, 'setItem');
                        if (pd && pd.configurable) {
                            delete localStorage.constructor.prototype.setItem;
                            Object.defineProperty(localStorage.constructor.prototype, 'setItem', {
                                get() {
                                    return setItemN
                                },
                                set(nv) {
                                    return true;
                                },
                                enumerable: false,
                                configurable: true,
                            });
                        }
                        player_.setPlaybackQualityRange(resultantQuality, resultantQuality);
                        if (pd && pd.configurable && setItemN === localStorage.setItem) {
                            delete localStorage.constructor.prototype.setItem;
                            Object.defineProperty(localStorage.constructor.prototype, 'setItem', pd);
                        }
                        byPass = false;
                        console.log('YouTube Quality Auto Max sets Quality to ', resultantQuality);
                    }
                }
            } catch (e) {
                console.warn(e)
            }
        };
        // document.addEventListener('loadstart', fn, true)
        document.addEventListener('durationchange', fn, true);

        // @@ durationchange @@

        (async () => {

            try {


                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;

                    if(typeof gkp.setPlaybackQualityRange132 !== "function" && typeof gkp.setPlaybackQualityRange === "function"){

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

                        console.log('YouTube Quality Auto Max - function modified [setPlaybackQualityRange]')

                    }

                }

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

                    let keyZf = null;

                    if (typeof gkp.getVideoData31 !== "function" && typeof gkp.getVideoData === "function" && typeof gkp.setupOnNewVideoData61 !== "function") {

                        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;


                            console.log('YouTube Quality Auto Max - resultantQualities is detected.');

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

                        console.log('YouTube Quality Auto Max - function modified [getVideoData]')
                    }

                }




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



        })();

    });

})();