YouTube HD Plus

Automatically select your preferred video quality and enable Premium playback when available. (Supports YouTube Desktop, Music & Mobile)

// ==UserScript==
// @name               YouTube HD Plus
// @name:en            YouTube HD Plus
// @name:id            YouTube HD Plus
// @name:zh-CN         YouTube HD Plus
// @name:zh-TW         YouTube HD Plus
// @name:ja            YouTube HD Plus
// @name:ko            YouTube HD Plus
// @name:fr            YouTube HD Plus
// @name:es            YouTube HD Plus
// @name:de            YouTube HD Plus
// @name:ru            YouTube HD Plus
// @description        Automatically select your preferred video quality and enable Premium playback when available. (Supports YouTube Desktop, Music & Mobile)
// @description:en     Automatically select your preferred video quality and enable Premium playback when available. (Supports YouTube Desktop, Music & Mobile)
// @description:id     Otomatis memilih kualitas video yang Anda sukai dan mengaktifkan pemutaran Premium jika tersedia. (Mendukung YouTube Desktop, Music & Mobile)
// @description:zh-CN  自动选择您偏好的视频画质,并在可用时启用 Premium 播放。 (支持 YouTube 桌面版、音乐和移动端)
// @description:zh-TW  自動選擇您偏好的影片畫質,並在可用時啟用 Premium 播放。 (支援 YouTube 桌面版、音樂和行動裝置)
// @description:ja     希望する画質を自動で選択し、利用可能な場合は Premium 再生を有効にします。(対応: YouTube デスクトップ、Music、モバイル)
// @description:ko     선호하는 동영상 화질을 자동으로 선택하고, 가능할 경우 Premium 재생을 활성화합니다. (지원: YouTube 데스크톱, Music, 모바일)
// @description:fr     Sélectionne automatiquement la qualité vidéo préférée et active la lecture Premium lorsque disponible. (Compatible avec YouTube Desktop, Music et Mobile)
// @description:es     Selecciona automáticamente la calidad de vídeo preferida y activa la reproducción Premium cuando esté disponible. (Compatible con YouTube Desktop, Music y Móvil)
// @description:de     Wählt automatisch die bevorzugte Videoqualität und aktiviert Premium-Wiedergabe, wenn verfügbar. (Unterstützt YouTube Desktop, Music & Mobile)
// @description:ru     Автоматически выбирает предпочтительное качество видео и включает воспроизведение Premium, если доступно. (Поддерживает YouTube Desktop, Music и Mobile)
// @version            2.6.3
// @run-at             document-end
// @inject-into        content
// @match              https://www.youtube.com/*
// @match              https://www.youtube-nocookie.com/*
// @match              https://m.youtube.com/*
// @match              https://music.youtube.com/*
// @exclude            https://*.youtube.com/live_chat*
// @exclude            https://*.youtube.com/tv*
// @icon               https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @grant              GM.getValue
// @grant              GM.setValue
// @author             Fznhq
// @namespace          https://github.com/fznhq
// @homepageURL        https://github.com/fznhq/userscript-collection
// @homepage           https://github.com/fznhq/userscript-collection
// @compatible         firefox
// @compatible         chrome
// @compatible         safari
// @compatible         opera
// @compatible         edge
// @license            GNU GPLv3
// ==/UserScript==

// Icons provided by https://uxwing.com/

(async function () {
    "use strict";

    const body = document.body;
    const head = document.head;

    const $host = location.hostname;
    const isMobile = $host.includes("m.youtube");
    const isMusic = $host.includes("music.youtube");
    const isEmbed = isVideoPage("embed");

    let manualOverride = false;
    let settingsClicked = false;

    const listQuality = [144, 240, 360, 480, 720, 1080, 1440, 2160, 4320];

    /**
     * You can change the config here in the script.
     * @namespace
     */
    const options = {
        preferred_quality: undefined, // Value from listQuality.
        preferred_premium: undefined, // true or false.
        show_ui: undefined, // true or false.
        updated_id: "",
    };

    const labels = {
        premium: "Preferred Premium;", // Remove ";" to set your own label.
        quality: "Preferred Quality;", // Remove ";" to set your own label.
    };

    const icons = {
        premium: `{"svg":{"viewBox":"-12 -12 147 119"},"path":{"d":"M1 28 20 1a3 3 0 0 1 3-1h77a3 3 0 0 1 3 1l19 27a3 3 0 0 1 1 2 3 3 0 0 1-1 2L64 94a3 3 0 0 1-4 0L1 32a3 3 0 0 1-1-1 3 3 0 0 1 1-3m44 5 17 51 17-51Zm39 0L68 82l46-49ZM56 82 39 33H9zM28 5l13 20L56 5Zm39 0 15 20L95 5Zm33 2L87 27h28zM77 27 61 7 47 27Zm-41 0L22 7 8 27Z"}}`,
        quality: `{"svg":{"viewBox":"-12 -12 147 131"},"path":{"fill-rule":"evenodd","d":"M89.8 56.3c-.925.267-1.862.484-2.8.7l-2.1-2.5c-1.447-1.791-4.18-1.847-6-.4l-4 3.6c-1.785 1.575-2.014 4.318-.5 6.1l1.8 2.1c-.595.898-1.098 1.848-1.6 2.8l-3.2-.2c-2.373-.142-4.362 1.565-4.5 4L67 80c.529 1.974 2.318 2.752 3.6 2.8l2.7.2c.263 1.056.643 2.074 1 3.1l-2.5 2c-1.624 1.298-1.722 3.357-1.3 4.6.854 2.08 2.104 4.051 3.5 5.9 1.533 1.77 4.28 1.831 6 .4l2-2a13 13 0 0 0 1.2.8l1.4.7.2.1-.2 3.2a4.2 4.2 0 0 0 4 4.5l5.4.4c2.4.178 4.427-1.605 4.5-3.9l.2-2.7c1.144-.244 2.23-.639 3.3-1.1l2 2.4c1.213 1.415 3.22 1.686 4.6 1.2.887-.3 5.542-3.716 6.6-5.7.865-1.642.35-3.582-.6-4.7l-1.8-2.1c.595-.898 1.098-1.848 1.6-2.8l3.2.2a4.115 4.115 0 0 0 4.5-4l.4-5.5c.124-2.228-1.387-4.394-3.9-4.5l-2.7-.2c-.222-1.07-.628-2.077-1-3.1l2.5-2.1c1.427-1.21 2.03-3.55.4-6-1.354-2.214-2.274-4.385-4.8-5.1m-20 7c-8 .683-13.871 7.818-13 15.8 1.374 12.585 17.215 17.308 25.258 7.534S105.614 62.924 93 64m1 7.8a6.4 6.4 0 0 0-5.8 7c.556 5.661 7.668 7.838 11.297 3.458S99.667 71.294 94 71.8M5.2 15.9v-5.1a5.6 5.6 0 0 1 5.6-5.6H24V16Zm23.8 0V5.2h21v10.7ZM93.2 5.2a5.6 5.6 0 0 1 5.7 5.6v5.1h-18V5.2ZM55.3 15.9V5.2h20.4v10.7ZM24 86.9H10.8a5.6 5.6 0 0 1-5.6-5.6v-5.7H24Zm5 0V75.6h21V87Zm17.5-56c-.348-.261-3.94-2.2-4.1 2.1v25.6c0 2.216 2.592 3.376 4.2 2l18.775-13.152c1.017-.88 1.13-1.996.025-3.048M10.8 0C4.835 0 0 4.835 0 10.8v70.5C.055 87.226 4.874 92 10.8 92h49c-.035-1.268.318-2.502.7-3.7L59 86.8h-3.7V75.5h.7c.073-1.752-.311-4.056.946-5.231L5.2 70.3V21h93.6v19.2c2.12.018 3.675.325 4.8.7l.4.1V10.8C104 4.835 99.165 0 93.2 0ZM113 57c-1.605-.52-3.349-.203-4.4.7l-2.1 1.8c-.868-.655-1.814-1.152-2.8-1.6l.2-3.3c.137-2.274-1.431-4.333-4-4.5l-5.4-.4c-2.465-.183-4.416 1.662-4.5 3.9l-.2 2.7"}}`,
        check_mark: `{"svg":{"viewBox":"-32 -32 186.9 153.8"},"path":{"d":"M1.2 55.5a3.7 3.7 0 0 1 5-5.5l34.1 30.9 76.1-79.7a3.8 3.8 0 0 1 5.4 5.1L43.2 88.7a3.7 3.7 0 0 1-5.2.2L1.2 55.5z"}}`,
        arrow: `{"svg":{"class":"transform-icon-svg","viewBox":"0 0 24 24"},"path":{"d":"M8.793 5.293a1 1 0 000 1.414L14.086 12l-5.293 5.293a1 1 0 101.414 1.414L16.914 12l-6.707-6.707a1 1 0 00-1.414 0Z"}}`,
    };

    /**
     * @param {string} name
     * @param {Record<string, string | number | boolean>} [attributes]
     * @param {Node[]} [append]
     * @returns {SVGElement}
     */
    function createNS(name, attributes = {}, append = []) {
        const el = document.createElementNS("http://www.w3.org/2000/svg", name);
        for (const k in attributes) el.setAttributeNS(null, k, attributes[k]);
        return el.append(...append), el;
    }

    for (const name in icons) {
        const icon = JSON.parse(icons[name]);
        icon.svg = { ...icon.svg, width: "100%", height: "100%" };
        icons[name] = createNS("svg", icon.svg, [createNS("path", icon.path)]);
    }

    /**
     * @param {string} key
     * @param {any} value
     */
    function saveOption(key, value) {
        GM.setValue(key, value);
        if (key in options) options[key] = value;
    }

    for (const key in labels) {
        const storageKeyLabel = `label_${key}`;
        let label = labels[key];
        if (label.endsWith(";")) {
            label = await GM.getValue(storageKeyLabel, label);
        } else saveOption(storageKeyLabel, label);
        labels[key] = label.replace(/;$/, "");
    }

    /** DO NOT CHANGE */
    const fallbackOptions = {
        preferred_quality: 1080,
        preferred_premium: true,
        show_ui: true,
    };

    /**
     * @param {boolean} [init]
     */
    async function loadOptions(init) {
        for (const key in options) {
            const value = options[key];
            const defaultValue = value ?? fallbackOptions[key];
            const saved = await GM.getValue(key, defaultValue);
            const lastDefaultKey = `last_default_${key}`;
            const lastDefault = await GM.getValue(lastDefaultKey);
            const isDefaultChange = init && lastDefault !== value;

            if (isDefaultChange) saveOption(lastDefaultKey, value);
            if (isDefaultChange && value !== undefined) saveOption(key, value);
            else options[key] = saved;
        }
    }

    await loadOptions(true);

    /**
     * @param {string} [prefix=id]
     * @returns {string}
     */
    function generateId(prefix = "id") {
        return prefix + (Date.now() + Math.random() * 10e20).toString(36);
    }

    const proxyName = generateId("ythdp-proxy-");
    const proxyFunction = function () {
        function handleAPI(ev) {
            const [id, elementId, fn, ...args] = ev.detail.split("|");
            const player = document.getElementById(elementId);
            const detail = { id, response: player[fn]?.(...args) };
            document.dispatchEvent(
                new CustomEvent("receiver-proxyName", { detail })
            );
        }

        function spoofData(ev) {
            const item = ev.target.closest?.("[proxyName]");
            if (item) item.data = {};
        }

        const create = (name) => document.createElement(name);
        const container = document.body.appendChild(create("ythdp-elements"));
        container.style.display = "none";
        container.append(create("ytd-toggle-menu-service-item-renderer"));

        document.addEventListener("proxyName", handleAPI);
        window.addEventListener("touchstart", spoofData, true);
        window.addEventListener("mousedown", spoofData, true);
    }.toString();

    const policyOptions = { createScript: (script) => script };
    const proxyPolicy = window.trustedTypes
        ? window.trustedTypes.createPolicy(proxyName, policyOptions)
        : policyOptions;
    const script = head.appendChild(document.createElement("script"));
    script.textContent = proxyPolicy.createScript(
        `(${proxyFunction.replace(/proxyName/g, proxyName)})();`
    );

    /** @type {Map<string, (response: any) => void>}  */
    const APIQueue = new Map();

    document.addEventListener("receiver-" + proxyName, (ev) => {
        const { id, response } = ev.detail;
        APIQueue.get(id)(response);
        APIQueue.delete(id);
    });

    /**
     * @param {string} elementId
     * @param {'getAvailableQualityData' | 'setPlaybackQualityRange' | 'playVideo' | 'loadVideoById'} name
     * @param {string[]} [args]
     * @returns {Promise<any>}
     */
    function API(elementId, name, ...args) {
        const id = generateId(name);
        const detail = [id, elementId, name, ...args].join("|");
        return new Promise((resolve) => {
            APIQueue.set(id, resolve);
            document.dispatchEvent(new CustomEvent(proxyName, { detail }));
        });
    }

    /**
     * @param {Document | HTMLElement} context
     * @param {string} query
     * @param {boolean} [all=false]
     * @returns {HTMLElement | NodeListOf<HTMLElement> | null}
     */
    function find(context, query, all = false) {
        return context[all ? "querySelectorAll" : "querySelector"](query);
    }

    /**
     * @param {string} query
     * @param {boolean} [cache=true]
     * @returns {() => (HTMLElement | null)}
     */
    function $(query, cache = true) {
        let element = null;
        return () => (cache && element) || (element = find(document, query));
    }

    const caches = {
        /** @type {Record<string, HTMLElement[]>} */
        player: {},
        /** @type {Set<Text>} */
        text_quality: new Set(),
        /** @type {Set<HTMLElement>} */
        toggle_premium: new Set(),
    };
    const element = {
        settings: $(".ytp-settings-menu"),
        panel_settings: $(".ytp-settings-menu .ytp-panel-menu"),
        movie_player: $("#movie_player", !isMobile),
        short_player: $("#shorts-player"),
        popup_menu: $("ytd-popup-container ytd-menu-service-item-renderer"),
        m_bottom_container: $("bottom-sheet-container:not(:empty)", false),
        music_menu_item: $("ytmusic-menu-service-item-renderer[class*=popup]"),
        link: $("link[rel=canonical]"),
        offline: $("[class*=offline][style*='v=']", false),
    };

    const style = head.appendChild(document.createElement("style"));
    style.textContent = /*css*/ `
        [dir=rtl] svg.transform-icon-svg { transform: scale(-1, 1); }
        #items.ytmusic-menu-popup-renderer { width: 250px !important; }
        .ythdp-icon { fill: currentColor; }
        .ythdp-toggle [role=button][hidden] { display: inherit !important; }
    `;

    /**
     * @param {MutationCallback} callback
     * @param {Node} [target]
     * @param {MutationObserverInit} [options]
     */
    function observer(callback, target = body, options) {
        const mutation = new MutationObserver(callback);
        mutation.observe(target, options || { subtree: true, childList: true });
    }

    /**
     * @param {string} label
     * @returns {number}
     */
    function parseQualityLabel(label) {
        return parseInt(label.replace(/^\D+/, "").slice(0, 4));
    }

    /**
     * @typedef {object} QualityData
     * @property {any} formatId
     * @property {string} qualityLabel
     * @property {string} quality
     * @property {boolean} isPlayable
     */

    /**
     * @param {QualityData[]} data
     * @returns {number}
     */
    function getPreferredQuality(data) {
        let preferred = 0,
            min = Infinity;

        for (const d of data) {
            const q = parseQualityLabel(d.qualityLabel);
            if (q < min) min = q;
            if (q <= options.preferred_quality && q > preferred) preferred = q;
        }

        return preferred || min;
    }

    /**
     * @param {QualityData[]} qualityData
     * @returns {QualityData | undefined}
     */
    function getQuality(qualityData) {
        const quality = { premium: undefined, normal: undefined };
        const preferred = getPreferredQuality(qualityData);

        if (!isFinite(preferred)) return;

        qualityData.forEach((data) => {
            const label = data.qualityLabel;
            if (data.isPlayable && parseQualityLabel(label) === preferred) {
                if (/premium/i.test(label)) quality.premium = data;
                else quality.normal = data;
            }
        });

        return (options.preferred_premium && quality.premium) || quality.normal;
    }

    /** @type {(() => Promise<void>)[]} */
    let stackSequence = [];
    let isSequenceRun = false;

    async function runSequence() {
        if (isSequenceRun) return;
        isSequenceRun = true;
        for (let fn; (fn = stackSequence.pop()); ) await fn();
        isSequenceRun = false;
    }

    function setVideoQuality() {
        if (manualOverride) return;

        stackSequence.push(async () => {
            const id = this.id;
            const qualityData = await API(id, "getAvailableQualityData");
            const selected = getQuality(qualityData || []);

            if (selected) {
                await API(
                    id,
                    "setPlaybackQualityRange",
                    selected.quality,
                    selected.quality,
                    selected.formatId
                );
            }
        });

        runSequence();
    }

    /**
     * @param {HTMLElement} [element]
     * @returns {HTMLElement | undefined}
     */
    function togglePremium(element) {
        if (element) caches.toggle_premium.add(element);
        caches.toggle_premium.forEach((toggle) => {
            toggle.removeAttribute("hidden");
            toggle.toggleAttribute("checked", options.preferred_premium);
            toggle.setAttribute("aria-checked", options.preferred_premium);
        });
        return element;
    }

    /**
     * @param {Text} [nodeText]
     * @returns {Text | undefined}
     */
    function setTextQuality(nodeText) {
        if (nodeText) caches.text_quality.add(nodeText);
        caches.text_quality.forEach((text) => {
            text.textContent = options.preferred_quality + "p";
        });
        return nodeText;
    }

    /**
     * @param {keyof options} optionKey
     * @param {any} newValue
     * @param {HTMLElement} player
     * @param {Boolean} [clearOverride]
     */
    function savePreferred(optionKey, newValue, player, clearOverride) {
        if (clearOverride) manualOverride = false;
        saveOption(optionKey, newValue);
        saveOption("updated_id", generateId());
        togglePremium(), setTextQuality();
        setVideoQuality.call(player);
    }

    /**
     * @param {string} className
     * @param {Node[]} [append]
     * @returns {HTMLDivElement}
     */
    function itemElement(className = "", append = []) {
        const el = document.createElement("div");
        el.className = "ytp-menuitem" + (className ? "-" + className : "");
        return el.append(...append), el;
    }

    /**
     * @param {HTMLElement[]} elements
     */
    function removeAttributes(elements) {
        for (const element of elements) {
            element.textContent = "";
            for (const attr of element.attributes) {
                if (attr.name !== "class") element.removeAttribute(attr.name);
            }
        }
    }

    /**
     * @param {NodeListOf<HTMLElement>} element
     * @returns {HTMLElement}
     */
    function firstOnly(element) {
        for (let i = element.length; --i; ) element[i].remove();
        return element[0];
    }

    /**
     * @param {HTMLElement} element
     * @returns {HTMLElement}
     */
    function removeDisabled(element) {
        const query = "[disabled], [aria-disabled=true], [class*=disabled]";
        const items = find(element, query, true);

        for (const item of items) {
            item.removeAttribute("disabled");
            item.setAttribute("aria-disabled", false);
            item.className = item.className.replaceAll("disabled", "");
        }

        return element;
    }

    let selectedLabel = document.createTextNode("");

    /**
     * @param {Object} param
     * @param {HTMLElement} param.menuItem
     * @param {SVGSVGElement | undefined} [param.icon]
     * @param {string} [param.label]
     * @param {Boolean} [param.selected=true]
     */
    function parseItem({
        menuItem,
        icon = icons.quality,
        label = labels.quality,
        selected = true,
    }) {
        const item = body.appendChild(removeDisabled(menuItem.cloneNode(true)));
        const iIcon = firstOnly(find(item, "c3-icon, yt-icon", true));
        const iTexts = find(item, "[role=text], yt-formatted-string", true);
        const iText = firstOnly(iTexts);
        const optionLabel = iText.cloneNode();
        const optionIcon = iIcon.cloneNode();
        const wrapperIcon = (icon) => {
            return itemElement(
                " ythdp-icon yt-icon-shape yt-spec-icon-shape ytSpecIconShapeHost",
                [icon]
            );
        };

        item.setAttribute(proxyName, "");
        item.setAttribute("use-icons", "");
        iText.after(optionLabel, optionIcon);
        removeAttributes([iIcon, iText, optionIcon, optionLabel]);
        iText.textContent = label;

        if (icon) iIcon.append(wrapperIcon(icon.cloneNode(true)));
        if (selected) {
            optionIcon.append(wrapperIcon(icons.arrow));
            optionIcon.style.width = "18px";
            optionLabel.className = iTexts[iTexts.length - 1].className;
            optionLabel.style.marginInline = "auto 0";
            optionLabel.append(setTextQuality(selectedLabel));
            if (iTexts.length === 1) {
                optionLabel.style.fontSize = "1.4rem";
                optionLabel.style.opacity = "0.7";
            }
        } else optionIcon.remove();

        return item;
    }

    /**
     * @param {HTMLElement} menuItem
     * @returns {{items: HTMLElement[], preferredIndex: number}}
     */
    function listQualityToItem(menuItem) {
        const name = "preferred_quality";
        const tempIndex = listQuality.indexOf(options[name]);
        const preferredIndex = listQuality.length - 1 - tempIndex;
        const items = listQuality.map((quality, i) => {
            const icon = tempIndex === i && icons.check_mark;
            const label = quality + "p";
            const item = parseItem({ menuItem, icon, label, selected: false });
            item.addEventListener("click", () => {
                body.click();
                body.dispatchEvent(new Event("tap"));
                savePreferred(name, quality, element.movie_player(), true);
            });
            return item;
        });
        return { items: items.reverse(), preferredIndex };
    }

    /**
     * @param {HTMLElement} player
     */
    function addVideoListener(player) {
        const cache = caches.player[player.id];
        const video = find(player, "video");
        if (!cache || cache[1] !== video) {
            caches.player[player.id] = [player, video];
            const fn = setVideoQuality.bind(player);
            const types = ["playing", "resize"];
            types.forEach((type) => video.addEventListener(type, fn));
        }
    }

    /**
     * @param {'watch' | 'shorts' | 'embed'} [type]
     * @returns {boolean}
     */
    function isVideoPage(type) {
        const types = type || "watch shorts clip embed";
        return types.includes(location.pathname.split("/")[1] || "!");
    }

    function resetState() {
        manualOverride = false;
    }

    /**
     * @param {MouseEvent} ev
     * @param {string} query
     */
    function setManualOverride(ev, query) {
        const item = ev.target.closest(query);
        if (item) {
            const selected = parseQualityLabel(item.textContent);
            manualOverride = listQuality.includes(selected);
        }
    }

    async function syncOptions() {
        if ((await GM.getValue("updated_id")) !== options.updated_id) {
            await loadOptions(), togglePremium(), setTextQuality();
            for (const id in caches.player) {
                const [player, video] = caches.player[id];
                if (!video.paused) setVideoQuality.call(player);
            }
        }
    }

    if (options.show_ui) {
        (function checkOptions() {
            setTimeout(() => syncOptions().then(checkOptions), 1000);
        })();
    }

    (function music() {
        if (!isMusic) return;

        /**
         * @param {HTMLElement} menuItem
         */
        function musicPopupObserver(menuItem) {
            const dropdown = menuItem.closest("tp-yt-iron-dropdown");
            const menu = find(dropdown, "#items");
            const item = parseItem({ menuItem });
            const addItem = () => settingsClicked && menu.append(item);

            item.addEventListener("click", () => {
                menu.textContent = "";
                menu.append(...listQualityToItem(item).items);
                document.dispatchEvent(new Event("resize", { bubbles: true }));
            });

            addItem();
            observer(addItem, dropdown, { attributeFilter: ["aria-hidden"] });
            find(item, "yt-formatted-string + yt-icon").style.marginInline = 0;
        }

        function musicSetSettingsClicked(/** @type {MouseEvent} */ ev) {
            settingsClicked = !!ev.target.closest(
                "#main-panel [class*=menu], .middle-controls-buttons [class*=menu]"
            );
        }

        if (options.show_ui) {
            window.addEventListener("tap", musicSetSettingsClicked, true);
            window.addEventListener("click", musicSetSettingsClicked, true);
        }

        observer((_, observe) => {
            const player = element.movie_player();
            const menuItem = settingsClicked && element.music_menu_item();

            if (player && !caches.player[player.id]) addVideoListener(player);
            if (menuItem || (!options.show_ui && player)) observe.disconnect();
            if (menuItem) musicPopupObserver(menuItem);
        });
    })();

    (function mobile() {
        if (!isMobile && !isEmbed) return;

        /** @type {HTMLElement} */
        let customListMenu = null;

        /**
         * @param {HTMLElement} menu
         * @returns {HTMLElement}
         */
        function findItem(menu) {
            return (
                find(menu, "[role=menuitem]") ||
                find(menu, "[role=listitem]") ||
                find(menu, "ytm-menu-service-item-renderer")
            );
        }

        /**
         * @param {HTMLElement} container
         */
        function customMenu(container) {
            location.replace("#custom-q-bottom-menu");
            customListMenu = container.cloneNode(true);
            customListMenu.addEventListener("click", () => {
                if (isEmbed) location.hash = "";
                else history.back();
            });

            const item = findItem(customListMenu);
            const menu = item.parentElement;
            const content = find(customListMenu, "[id*=content]");
            const header = content.previousElementSibling;
            const contentHeight = parseInt(content.style.maxHeight || 150);
            const maxHeight = Math.min(contentHeight + 20, 250);
            const { items, preferredIndex } = listQualityToItem(item);

            menu.textContent = "";
            menu.append(...items);
            header?.remove();
            content.style.maxHeight = maxHeight + "px";
            body.style.overflow = "hidden";
            container.parentElement.parentElement.append(customListMenu);

            const preferred = items[preferredIndex];
            const preferredHeight = preferred.offsetHeight;
            const scrollTarget =
                preferredHeight * preferredIndex -
                maxHeight / 2 +
                preferredHeight / 2;
            content.scrollTo(0, scrollTarget);
        }

        function mobileQualityMenu() {
            const container = element.m_bottom_container();

            if (container) {
                settingsClicked = false;

                const menuItem = findItem(container);
                const item = parseItem({ menuItem });
                item.addEventListener("click", () => customMenu(container));
                menuItem.parentElement.append(item);
            }
        }

        function mobileSetSettingsClicked(/** @type {MouseEvent} */ ev) {
            if (isVideoPage() && !element.m_bottom_container()) {
                settingsClicked = !!ev.target.closest(
                    "player-top-controls .player-settings-icon, shorts-video ytm-bottom-sheet-renderer"
                );
            }
        }

        let menuStep = 0;

        function mobileSetOverride(ev) {
            if (manualOverride || customListMenu) return;
            if (!element.m_bottom_container()) menuStep = 0;
            if (menuStep++ >= 2) setManualOverride(ev, "[role=menuitem]");
        }

        function mobilePlayerUpdated(/** @type {CustomEvent} */ ev) {
            if (isVideoPage() && ev.detail.type === "newdata") resetState();
        }

        function mobileHandlePressBack() {
            if (customListMenu) {
                customListMenu = customListMenu.remove();
                body.style.overflow = "";
            }
        }

        const videoIdRegex = /(?:shorts\/|watch\?v=|clip\/)([^#&?]*)/;

        /**
         * @returns {boolean | string}
         */
        function getVideoId() {
            const id1 = element.link().href.match(videoIdRegex);
            const id2 = location.href.match(videoIdRegex);
            return id1 && id2 && id1[1] === id2[1] && id1[1];
        }

        function registerPlayer() {
            const player = element.movie_player();

            if (player) {
                addVideoListener(player);

                if (
                    player.closest("[playable=true]") &&
                    player.className.includes("unstarted-mode")
                ) {
                    const id = getVideoId();
                    const elemId = player.id;

                    if (id) {
                        if (element.offline()) API(elemId, "loadVideoById", id);
                        API(elemId, "playVideo");
                    }
                }
            }
        }

        if (options.show_ui) {
            window.addEventListener("click", mobileSetSettingsClicked, true);
        }

        window.addEventListener("click", mobileSetOverride, true);
        window.addEventListener("popstate", mobileHandlePressBack);
        document.addEventListener("video-data-change", mobilePlayerUpdated);

        observer(() => {
            if (!isEmbed && isVideoPage()) registerPlayer();
            if (settingsClicked) mobileQualityMenu();
        });
    })();

    (function desktop() {
        if (isMusic || isMobile) return;

        /**
         * @param {SVGElement} svg
         * @param {string} textLabel
         * @param {Boolean} [checkbox]
         * @returns {{item: HTMLDivElement, content: HTMLDivElement}}
         */
        function createMenuItem(svg, textLabel, checkbox) {
            const inner = checkbox ? [itemElement("toggle-checkbox")] : [];
            const content = itemElement("content", inner);
            const label = itemElement("label", [textLabel]);
            const icon = itemElement("icon ythdp-icon", [svg.cloneNode(true)]);
            return { item: itemElement("", [icon, label, content]), content };
        }

        /**
         * @param {HTMLElement} item
         * @param {HTMLElement} player
         * @returns {HTMLElement}
         */
        function premiumOption(item, player) {
            const name = "preferred_premium";
            const toggle = find(item, "[role=button]") || item;
            item.addEventListener("click", () => {
                savePreferred(name, !options[name], player);
            });
            return togglePremium(toggle);
        }

        function premiumMenu() {
            return premiumOption(
                createMenuItem(icons.premium, labels.premium, true).item,
                element.movie_player()
            );
        }

        function shortPremiumMenu() {
            const item = parseItem({
                menuItem: find(body, "ytd-toggle-menu-service-item-renderer"),
                label: labels.premium,
                icon: icons.premium,
                selected: false,
            });
            item.classList.add("ythdp-toggle");
            find(item, ".toggle-label").textContent = "";
            premiumOption(item, element.short_player());
            return item;
        }

        /**
         * @param {HTMLElement} content
         * @param {HTMLElement} player
         */
        function qualityOption(content, player) {
            const name = "preferred_quality";
            const text = document.createTextNode("");

            content.style.cursor = "pointer";
            content.style.wordSpacing = "2rem";
            content.append("< ", text, " >");
            content.addEventListener("click", (ev) => {
                const threshold = content.clientWidth / 2;
                const contentLeft = content.getBoundingClientRect().left;
                const clickPos = ev.clientX - contentLeft;
                const length = listQuality.length - 1;
                let pos = listQuality.indexOf(options[name]);

                if (
                    (clickPos < threshold && pos > 0 && pos--) ||
                    (clickPos > threshold && pos < length && ++pos)
                ) {
                    savePreferred(name, listQuality[pos], player, true);
                }
            });

            setTextQuality(text);
        }

        function qualityMenu() {
            const menu = createMenuItem(icons.quality, labels.quality);

            menu.item.style.cursor = "default";
            menu.content.style.fontSize = "130%";

            qualityOption(menu.content, element.movie_player());
            return menu.item;
        }

        /**
         * @param {HTMLElement} menuItem
         * @returns {HTMLElement}
         */
        function shortQualityMenu(menuItem) {
            const item = parseItem({ menuItem, selected: false });
            const container = find(item, "yt-formatted-string:last-of-type");
            const option = document.createElement("div");

            item.style.userSelect = "none";
            item.style.cursor = "default";
            container.append(option);
            container.style.minWidth = "130px";
            option.style.margin = container.style.margin = "0 auto";
            option.style.width = "fit-content";

            qualityOption(option, element.short_player());
            return item;
        }

        function setOverride(ev) {
            if (!manualOverride) setManualOverride(ev, "[role=menuitemradio]");
        }

        function playerUpdated(/** @type {CustomEvent} */ ev) {
            if (isVideoPage()) {
                const player = [
                    element.movie_player(),
                    element.short_player(),
                ].find((player) => ev.target.contains(player));

                if (player) {
                    resetState();
                    addVideoListener(player);
                }
            }
        }

        function attachShortMenuItem() {
            const menu = isVideoPage("shorts") && element.popup_menu();
            if (menu && !find(menu.parentElement, `[${proxyName}]`)) {
                menu.parentElement.append(
                    shortPremiumMenu(),
                    shortQualityMenu(menu)
                );
            }
        }

        if (options.show_ui) {
            window.addEventListener("click", attachShortMenuItem);
        }

        observer((_, observe) => {
            const moviePlayer = element.movie_player();
            const shortPlayer = element.short_player();

            if (shortPlayer) addVideoListener(shortPlayer);
            if (!moviePlayer) return;

            observe.disconnect();
            addVideoListener(moviePlayer);
            document.addEventListener("yt-player-updated", playerUpdated);
            const panelSettings = options.show_ui && element.panel_settings();

            if (panelSettings) {
                panelSettings.append(premiumMenu(), qualityMenu());
                element.settings().addEventListener("click", setOverride, true);
            }
        });
    })();
})();