Youtube Play Next Queue

Don't like the youtube autoplay suggestion? This script can create a queue with videos you want to play after your current video has finished!

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Youtube Play Next Queue
// @version      2.5.6
// @description  Don't like the youtube autoplay suggestion? This script can create a queue with videos you want to play after your current video has finished!
// @author       Cpt_mathix & CY Fung
// @match        https://www.youtube.com/*
// @include      https://www.youtube.com/*
// @license      GPL-2.0-or-later; http://www.gnu.org/licenses/gpl-2.0.txt
// @require      https://cdn.jsdelivr.net/gh/culefa/JavaScript-autoComplete@19203f30f148e2d9d810ece292b987abb157bbe0/auto-complete.min.js
// @namespace    https://greatest.deepsurf.us/users/16080
// @run-at       document-start
// @grant        none
// @noframes
// ==/UserScript==

/**
 *
 * The latest fixes to the script were contributed by [CY Fung](https://greatest.deepsurf.us/en/users/371179)
 * Modern yt-lockup-view-model hover overlay support added.
 *
**/

/* jshint esversion: 11 */

(function() {
    'use strict';

    // Handle Trusted Type Policy Violations
    var TTP = window.TTP = {createHTML: (string, sink) => string, createScript: (string, sink) => string, createScriptURL: (string, sink) => string};
    if(typeof window.isSecureContext !== 'undefined' && window.isSecureContext){
        if (window.trustedTypes && window.trustedTypes.createPolicy){
            if(window.trustedTypes.defaultPolicy) {
                TTP = window.TTP = window.trustedTypes.defaultPolicy;
            } else {
                TTP = window.TTP = window.trustedTypes.createPolicy("default", TTP);
            }
        }
    }

    // ================================================================================= //
    // ============================ YOUTUBE PLAY NEXT QUEUE ============================ //
    // ================================================================================= //

    function youtube_play_next_queue_modern() {

        let script = {
            version: "2.0.0",
            initialized: false,

            queue: null,
            ytplayer: null,

            queue_visible: false,
            queue_rendered_observer: null,
            video_renderer_observer: null,
            playnext_data_observer: null,
            modern_thumb_overlay_observer: null,

            debug: false
        };

        const MODERN_QUEUE_BUTTON_CLASS = "youtube-play-next-modern-queue-button";
        const insp = o => o ? (o.polymerController || o.inst || o || null) : (o || null);

        document.addEventListener("load", loadScript);
        document.addEventListener("DOMContentLoaded", initScript);

        window.addEventListener("storage", function(event) {
            if (script.initialized && /YTQUEUE-MODERN#.*#QUEUE/.test(event.key)) {
                initQueue();
                displayQueue();
            }
        });

        // reload script on page change using youtube polymer fire events
        window.addEventListener("yt-page-data-updated", function(event) {
            if (script.debug) { console.log("# page updated #"); }
            startScript(2);
            initModernThumbOverlays();
        });

        window.addEventListener("yt-navigate-finish", function() {
            initModernThumbOverlays();
        });

        function initScript() {
            if (script.debug) { console.log("### Youtube Play Next Queue Initializing ###"); }

            if (window.Polymer === undefined) {
                return;
            }

            initQueue();
            injectCSS();

            // Old Polymer/ytd renderer overlays.
            setInterval(addThumbOverlayClickListeners, 250);
            setInterval(initThumbOverlays, 1000);

            // New yt-lockup-view-model hover overlays.
            initModernThumbOverlayObserver();
            initModernThumbOverlays();

            setInterval(initVideoPreviewThumbOverlay, 1000);

            if (script.debug) { console.log("### Youtube Play Next Queue Initialized ###"); }
            script.initialized = true;
        }

        function loadScript() {
            startScript(5);
        }

        function startScript(retry) {
            script.queue_visible = false;

            if (script.initialized && isPlayerAvailable()) {
                if (script.debug) { console.log("videoplayer is available"); }
                if (script.debug) { console.log("ytplayer: ", script.ytplayer); }

                if (script.ytplayer) {
                    if (script.debug) { console.log("initializing queue"); }
                    displayQueue();

                    if (script.debug) { console.log("initializing video statelistener"); }
                    initVideoStateListener();

                    if (script.debug) { console.log("initializing playnext data observer"); }
                    initPlayNextDataObserver();
                } else {
                    hideQueue();
                }
            } else if (retry > 0) { // fix conflict with Youtube+ script
                setTimeout( function() {
                    startScript(--retry);
                }, 1000);
            } else {
                if (script.debug) { console.log("videoplayer is unavailable"); }
            }
        }

        // *** LISTENERS & OBSERVERS *** //

        function initVideoStateListener() {
            if (!script.ytplayer.classList.contains('initialized-listeners')) {
                script.ytplayer.classList.add('initialized-listeners');
                script.ytplayer.addEventListener("onStateChange", handleVideoStateChanged);
            } else {
                if (script.debug) { console.log("statelistener already initialized"); }
            }

            // run handler once to make sure queue is in sync
            handleVideoStateChanged(script.ytplayer.getPlayerState());
        }

        function handleVideoStateChanged(videoState) {
            if (script.debug) { console.log("player state changed: " + videoState + "; queue empty: " + script.queue.isEmpty()); }

            const PLAYING_STATE = 1;
            const PAUSED_STATE = 2;
            const BUFFERING_STATE = 3;

            if (!script.queue.isEmpty()) {
                // dequeue video from the queue if it is currently playing
                if (script.ytplayer.getVideoData().video_id === script.queue.peek().id) {
                    script.queue.dequeue();
                }
            }

            let currentVideoIdFromUrl = getVideoIdFromUrl(window.location.href);
            if (videoState !== BUFFERING_STATE && isWatchPage() && !!currentVideoIdFromUrl && script.ytplayer.getVideoData().video_id !== currentVideoIdFromUrl && script.ytplayer.getVideoData().isListed) {
                if (script.debug) { console.log("Videoplayer not correctly loaded, LoadVideoById manually"); }
                script.ytplayer.loadVideoById(currentVideoIdFromUrl);
                script.ytplayer.playVideo();
            }

            if ((videoState === PLAYING_STATE || videoState === PAUSED_STATE) && !script.queue.isEmpty() && !isPlaylist()) {
                if (script.debug) { console.log("SetAsNextVideo: HandleVideoStateChanged"); }
                script.queue.peek().setAsNextVideo();
            }

            if (videoState === PAUSED_STATE) {
                // Check for annoying "are you still watching" popup
                setTimeout(() => {
                    let button = document.querySelector('yt-confirm-dialog-renderer #confirm-button');
                    if (button && !!(button.offsetWidth || button.offsetHeight || button.getClientRects().length)) {
                        if (script.debug) { console.log("### Clicking confirm button popup ###"); }
                        button.click();
                    }
                }, 1000);
            }
        }

        function initQueueRenderedObserver() {
            // Queue controls are now created directly in loadQueueItem().
            // No post-render observer is needed anymore.
        }

        function scheduleQueueButtonInitialization() {
            // Queue controls are now created directly in loadQueueItem().
        }

        function initPlayNextDataObserver() {
            if (script.playnext_data_observer) {
                script.playnext_data_observer.disconnect();
            }

            script.playnext_data_observer = new MutationObserver(function(mutations) {
                if (!script.queue.isEmpty() && script.queue_visible) {
                    if (isPlaylist()) {
                        if (script.debug) { console.log("Play next observer triggered but found playlist, hiding current queue"); }
                        hideQueue();
                    } else {
                        forEach(mutations, function(mutation) {
                            if (mutation.attributeName === "href") {
                                let nextVideoId = getVideoIdFromUrl(document.querySelector('.ytp-next-button').href);
                                let nextQueueItem = script.queue.peek();
                                if (nextQueueItem.id !== nextVideoId) {
                                    if (script.debug) { console.log("SetAsNextVideo: PlayNextDataObserver"); }
                                    nextQueueItem.setAsNextVideo();
                                }
                            }
                        });
                    }
                }
            });

            let observable = document.querySelector('.ytp-next-button');
            if (observable) {
                script.playnext_data_observer.observe(observable, { attributes: true });
            }
        }

        // *** VIDEOPLAYER *** //

        function getVideoPlayer() {
            return insp(document.getElementById('movie_player'));
        }

        function isPlayerAvailable() {
            script.ytplayer = getVideoPlayer();
            return script.ytplayer !== null && !!script.ytplayer.getVideoData?.().video_id;
        }

        function isPlaylist() {
            return !!script.ytplayer.getVideoStats().list || !document.querySelector('ytd-playlist-panel-renderer.ytd-watch-flexy[hidden]');
        }

        function isPlayerMinimized() {
            return !!document.querySelector('ytd-miniplayer[active][enabled]');
        }

        function isWatchPage() {
            return !!insp(document.querySelector('ytd-app'))?.__data?.isWatchPage;
        }

        function getVideoData(element) {
            let data = insp(element)?.__data?.data || insp(element)?.data;

            if (data?.content) {
                return data.content.videoRenderer;
            } else {
                return data;
            }
        }

        function getAutoplaySuggestion() {
            return document.querySelector('ytd-compact-autoplay-renderer ytd-compact-video-renderer') || document.querySelector('#related > ytd-watch-next-secondary-results-renderer ytd-compact-video-renderer');
        }

        function getVideoIdFromUrl(url) {
            let o = null;
            try {
                o = new URL(url, location.origin);
            } catch (e) { }
            let v = (o?.searchParams ? o.searchParams.get('v') : '') || '';
            return v;
        }

        // *** OBJECTS *** //

        // QueueItem object
        class QueueItem {
            constructor(id, data, type) {
                this.id = id;
                this.data = data;
                this.type = type;
            }

            getVideoLength() {
                if (this.data.lengthText) {
                    return this.data.lengthText.simpleText;
                } else if (this.data.thumbnailOverlays && this.data.thumbnailOverlays[0].thumbnailOverlayTimeStatusRenderer) {
                    return this.data.thumbnailOverlays[0].thumbnailOverlayTimeStatusRenderer.text.simpleText;
                } else {
                    return "";
                }
            }

            getSmallestThumb() {
                let thumbnails = this.data?.thumbnail?.thumbnails || [];
                if (!thumbnails.length) {
                    return { url: "", width: 168, height: 94 };
                }
                return thumbnails.reduce(function (thumb, currentSmallestThumb) {
                    return (currentSmallestThumb.height * currentSmallestThumb.width < thumb.height * thumb.width) ? currentSmallestThumb : thumb;
                });
            }

            getBiggestThumb() {
                let thumbnails = this.data?.thumbnail?.thumbnails || [];
                if (!thumbnails.length) {
                    return { url: "", width: 336, height: 188 };
                }
                return thumbnails.reduce(function (thumb, currentBiggestThumb) {
                    return (currentBiggestThumb.height * currentBiggestThumb.width > thumb.height * thumb.width) ? currentBiggestThumb : thumb;
                });
            }

            setAsNextVideo() {
                const PLAYING_STATE = 1;
                const PAUSED_STATE = 2;

                if (isPlaylist()) { return; }

                let currentVideoState = script.ytplayer.getPlayerState();
                if (currentVideoState !== PLAYING_STATE && currentVideoState !== PAUSED_STATE) {
                    return;
                }

                if (this.id === script.ytplayer.getVideoData().video_id) {
                    return;
                }

                if (script.debug) { console.log("changing next video"); }

                let ytdPlayer = document.querySelector('ytd-player');
                let watchNextData = insp(ytdPlayer)?.__data?.watchNextData;

                if (watchNextData && watchNextData.contents && watchNextData.contents.twoColumnWatchNextResults && watchNextData.playerOverlays && watchNextData.playerOverlays.playerOverlayRenderer) {
                    if (watchNextData.contents.twoColumnWatchNextResults.playlist) {
                        return;
                    }

                    let watchNextEndScreenRenderer = watchNextData.playerOverlays.playerOverlayRenderer.endScreen.watchNextEndScreenRenderer;
                    watchNextEndScreenRenderer.results[0].endScreenVideoRenderer = this.data;
                    watchNextEndScreenRenderer.results[0].endScreenVideoRenderer.lengthInSeconds = hmsToSeconds(this.getVideoLength());

                    let playerOverlayAutoplayRenderer = watchNextData.playerOverlays.playerOverlayRenderer.autoplay.playerOverlayAutoplayRenderer;
                    playerOverlayAutoplayRenderer.background.thumbnails = this.data.thumbnail.thumbnails;
                    playerOverlayAutoplayRenderer.byline = this.data.longBylineText || this.data.shortBylineText;
                    playerOverlayAutoplayRenderer.nextButton.buttonRenderer.navigationEndpoint = this.data.navigationEndpoint;
                    playerOverlayAutoplayRenderer.videoId = this.data.videoId;
                    playerOverlayAutoplayRenderer.videoTitle = this.data.title.simpleText || this.data.title.runs?.[0]?.text || "";

                    let autoplay = watchNextData.contents.twoColumnWatchNextResults.autoplay.autoplay;
                    autoplay.sets[0].autoplayVideo.watchEndpoint.videoId = this.data.videoId;

                    let watchNextResponse = { "raw_watch_next_response" : watchNextData};
                    script.ytplayer.updateVideoData(watchNextResponse);

                    if (!script.queue_visible) {
                        displayQueue();
                    }
                }
            }

            clearBadges() {
                this.data.badges = [];
            }

            addBadge(label, classes = []) {
                let badge = {
                    "metadataBadgeRenderer": {
                        "style": classes.join(" "),
                        "label": label
                    }
                };

                this.data.badges.push(badge);
            }

            toNode(classes = []) {
                let node = document.createElement("ytd-compact-video-renderer");
                node.classList.add("style-scope", "ytd-watch-next-secondary-results-renderer");
                classes.forEach(className => node.classList.add(className));
                node.data = this.data;
                return node;
            }

            static fromDOM(element) {
                let originalData = getVideoData(element);
                if (!originalData) {
                    throw new Error("No video data found on element");
                }

                let data = Object.assign({}, originalData);
                data.navigationEndpoint = data.navigationEndpoint || {};
                data.navigationEndpoint.watchEndpoint = { "videoId": data.videoId };
                data.navigationEndpoint.commandMetadata = { "webCommandMetadata": { "url": "/watch?v=" + data.videoId, webPageType: "WEB_PAGE_TYPE_WATCH" } };

                if (!data.shortBylineText) {
                    let titleLabel = data.title?.accessibility?.accessibilityData?.label || data.title?.simpleText || data.title?.runs?.[0]?.text || "";
                    data.shortBylineText = { "runs": [ { "text": titleLabel } ] };
                }

                let id = data.videoId;
                let type = element.tagName.toLowerCase();

                if (!id) {
                    throw new Error("No videoId found on element data");
                }

                return new QueueItem(id, data, type);
            }

            static fromModernDOM(element) {
                let link =
                    element.querySelector('a[href*="/watch?v="]') ||
                    element.querySelector('a[href*="youtu.be/"]');

                if (!link) {
                    return null;
                }

                let url = new URL(link.href, location.origin);
                let videoId = url.searchParams.get("v");

                if (!videoId && url.hostname.includes("youtu.be")) {
                    videoId = url.pathname.slice(1).split("/")[0];
                }

                if (!videoId) {
                    return null;
                }

                let title = readModernVideoTitle(element, link);

                let channel = readModernVideoChannel(element);

                let thumbnailUrl = readModernThumbnailUrl(element, videoId);

                let thumbnails = thumbnailUrl ? [
                    { url: thumbnailUrl, width: 168, height: 94 },
                    { url: thumbnailUrl, width: 336, height: 188 }
                ] : [
                    { url: "https://i.ytimg.com/vi/" + videoId + "/mqdefault.jpg", width: 320, height: 180 }
                ];

                let data = {
                    videoId: videoId,
                    title: {
                        runs: [
                            {
                                text: title,
                                navigationEndpoint: {
                                    watchEndpoint: {
                                        videoId: videoId
                                    },
                                    commandMetadata: {
                                        webCommandMetadata: {
                                            url: "/watch?v=" + videoId,
                                            webPageType: "WEB_PAGE_TYPE_WATCH"
                                        }
                                    }
                                }
                            }
                        ],
                        accessibility: {
                            accessibilityData: {
                                label: title
                            }
                        }
                    },
                    shortBylineText: {
                        runs: [
                            {
                                text: channel || "YouTube"
                            }
                        ]
                    },
                    longBylineText: {
                        runs: [
                            {
                                text: channel || "YouTube"
                            }
                        ]
                    },
                    thumbnail: {
                        thumbnails: thumbnails
                    },
                    lengthText: {
                        simpleText: readModernVideoLength(element)
                    },
                    navigationEndpoint: {
                        watchEndpoint: {
                            videoId: videoId
                        },
                        commandMetadata: {
                            webCommandMetadata: {
                                url: "/watch?v=" + videoId,
                                webPageType: "WEB_PAGE_TYPE_WATCH"
                            }
                        }
                    },
                    badges: [],
                    ownerBadges: [],
                    trackingParams: ""
                };

                return new QueueItem(videoId, data, element.tagName.toLowerCase());
            }

            static fromJSON(json) {
                let data = json.data;
                let id = json.id;
                let type = json.type;
                return new QueueItem(id, data, type);
            }
        }

        // Queue object
        class Queue {
            constructor() {
                this.queue = [];
            }

            get() {
                return this.queue;
            }

            set(queue) {
                this.queue = queue;
                setCache("QUEUE", queue);
            }

            size() {
                return this.queue.length;
            }

            isEmpty() {
                return this.size() === 0;
            }

            contains(videoId) {
                for (let i = 0; i < this.queue.length; i++) {
                    if (this.queue[i].id === videoId) {
                        return true;
                    }
                }
                return false;
            }

            peek() {
                return this.queue[0];
            }

            enqueue(item) {
                this.queue.push(item);
                this.update();
                this.show(250);
            }

            dequeue() {
                let item = this.queue.shift();
                this.update();
                this.show(0);
                return item;
            }

            remove(index) {
                this.queue.splice(index, 1);
                this.update();
                this.show(250);
            }

            playNext(index) {
                if (index <= 0 || index >= this.queue.length) {
                    return;
                }

                let video = this.queue.splice(index, 1)[0];
                if (!video) {
                    return;
                }

                this.queue.unshift(video);
                this.update();
                this.show(0);
            }

            playNow() {
                script.ytplayer.nextVideo(true);
            }

            update() {
                setCache("QUEUE", this.get());
                if (script.debug) { console.log("updated queue: ", this.get().slice()); }
            }

            show(delay) {
                setTimeout(function() {
                    if (isPlayerAvailable()) {
                        displayQueue();
                    }
                }, delay);
            }

            reset() {
                this.queue = [];
                this.update();
                this.show(0);
            }
        }

        // *** QUEUE *** //

        function initQueue() {
            script.queue = new Queue();
            let cachedQueue = getCache("QUEUE");

            if (cachedQueue) {
                try {
                    cachedQueue = cachedQueue.map(queueItem => QueueItem.fromJSON(queueItem));
                    script.queue.set(cachedQueue);
                } catch(e) {
                    setCache("QUEUE", script.queue.get());
                }
            } else {
                setCache("QUEUE", script.queue.get());
            }
        }

        function displayQueue() {
            if (script.debug) { console.log("showing queue: ", script.queue.get()); }

            script.queue_visible = true;

            let queue = document.querySelector('#youtube-play-next-queue-renderer #contents');
            if (!queue && isWatchPage()) {
                let anchor = document.querySelector('#related');
                if (anchor) {
                    let node = document.createElement("ytd-item-section-renderer");
                    node.classList.add("style-scope", "ytd-watch-next-secondary-results-renderer", "youtube-play-next-queue");
                    node.id = "youtube-play-next-queue-renderer";
                    window.Polymer.dom(anchor).insertBefore(node, anchor.firstChild);
                    queue = document.querySelector('#youtube-play-next-queue-renderer #contents');
                }
            } else if (!queue) {
                return;
            }

            queue.innerHTML = "";

            initQueueRenderedObserver();

            if (isPlaylist()) {
                if (script.debug) { console.log("Playlist found, hiding queue"); }
                queue.parentNode.setAttribute("hidden", "");
                script.queue_visible = false;
                return;
            }

            if (!script.queue.isEmpty()) {
                queue.parentNode.removeAttribute("hidden", "");

                let autoplay = document.querySelector('ytd-compact-autoplay-renderer #contents');
                if (autoplay) { autoplay.setAttribute("hidden", ""); }

                forEach(script.queue.get(), function(item, index) {
                    try {
                        loadQueueItem(item, index, queue);
                    } catch (ex) {
                        console.log("Failed to display queue item", ex);
                    }
                });

                scheduleQueueButtonInitialization();
            } else {
                queue.parentNode.setAttribute("hidden", "");

                let autoplay = document.querySelector('ytd-compact-autoplay-renderer #contents');
                if (autoplay) { autoplay.removeAttribute("hidden", ""); }

                if (script.debug) { console.log("SetAsNextVideo: Restore suggestion"); }
                let autoplaySuggestion = getAutoplaySuggestion();
                if (autoplaySuggestion) {
                    QueueItem.fromDOM(getAutoplaySuggestion()).setAsNextVideo();
                }

                script.queue_visible = false;
            }
        }

        function loadQueueItem(item, index, queueContents) {
            item.clearBadges();

            if (index === 0) {
                if (script.debug) { console.log("SetAsNextVideo: Load first queue item"); }
                item.setAsNextVideo();
            }

            let wrapper = document.createElement("div");
            wrapper.className = "youtube-play-next-queue-item-wrapper";
            wrapper.dataset.queueIndex = String(index);

            let node = item.toNode(["queue-item"]);
            node.classList.add("youtube-play-next-queue-item-host");
            node.dataset.queueIndex = String(index);

            wrapper.appendChild(node);
            wrapper.appendChild(createQueueItemControls(index));

            queueContents.appendChild(wrapper);
        }

        function createQueueItemControls(index) {
            let controls = document.createElement("div");
            controls.className = "youtube-play-next-inline-queue-controls";
            controls.dataset.queueIndex = String(index);

            if (index === 0) {
                controls.appendChild(createInlineQueueButton("Play Now", "queue-play-now", index));
                controls.appendChild(createInlineQueueButton("Remove", "queue-remove", index));
            } else {
                controls.appendChild(createInlineQueueButton("Play Next", "queue-play-next", index));
                controls.appendChild(createInlineQueueButton("Remove", "queue-remove", index));
            }

            return controls;
        }

        function createInlineQueueButton(label, className, index) {
            let button = document.createElement("button");
            button.type = "button";
            button.className = "youtube-play-next-inline-queue-button " + className;
            button.dataset.queueIndex = String(index);
            button.textContent = label;
            button.setAttribute("aria-label", label);

            [
                "pointerdown",
                "pointerup",
                "mousedown",
                "mouseup",
                "touchstart",
                "touchend",
                "auxclick",
                "dblclick"
            ].forEach(function(eventName) {
                button.addEventListener(eventName, stopQueueButtonEvent, true);
                button.addEventListener(eventName, stopQueueButtonEvent, false);
            });

            button.addEventListener("click", function(event) {
                event.preventDefault();
                event.stopImmediatePropagation();
                event.stopPropagation();

                let pos = Number(button.dataset.queueIndex || 0);

                if (button.classList.contains("queue-play-now")) {
                    script.queue.playNow();
                } else if (button.classList.contains("queue-play-next")) {
                    script.queue.playNext(pos);
                } else if (button.classList.contains("queue-remove")) {
                    script.queue.remove(pos);
                }
            }, true);

            button.addEventListener("click", function(event) {
                event.preventDefault();
                event.stopImmediatePropagation();
                event.stopPropagation();
            }, false);

            return button;
        }

        function hideQueue() {
            script.queue_visible = false;

            if (script.debug) { console.log("hiding queue"); }

            let queue = document.querySelector('#youtube-play-next-queue-renderer #contents');
            if (!queue) { return; }

            openToast("Youtube Play Next Queue hidden while playlist, mix or native youtube queue is active.");

            queue.innerHTML = "";
            queue.parentNode.setAttribute("hidden", "");
        }

        function initRemoveQueueButton(anchor) {
            let html = "<div class=\"queue-button remove-queue\">Remove Queue</div>";
            anchor.innerHTML = html;

            if (!anchor.querySelector(".flex-whitebox")) {
                anchor.classList.add("flex-none");
                anchor.insertAdjacentHTML("afterend", "<div class=\"flex-whitebox\"></div>");
            }

            anchor.querySelector('.remove-queue').addEventListener("click", function handler(e) {
                e.preventDefault();
                script.queue.reset();
                this.parentNode.innerHTML = "Up next";
            });
        }

        // *** THUMB OVERLAYS *** //

        function initModernThumbOverlayObserver() {
            if (script.modern_thumb_overlay_observer) {
                script.modern_thumb_overlay_observer.disconnect();
            }

            script.modern_thumb_overlay_observer = new MutationObserver(function(mutations) {
                forEach(mutations, function(mutation) {
                    forEach(mutation.addedNodes, function(node) {
                        if (!(node instanceof Element)) {
                            return;
                        }

                        if (node.matches && node.matches("yt-thumbnail-hover-overlay-toggle-actions-view-model")) {
                            injectModernThumbOverlayButton(node);
                        }

                        if (node.querySelectorAll) {
                            node.querySelectorAll("yt-thumbnail-hover-overlay-toggle-actions-view-model")
                                .forEach(injectModernThumbOverlayButton);

                            if (
                                node.matches?.("yt-lockup-view-model, .yt-lockup-view-model") ||
                                node.querySelector?.("yt-lockup-view-model, .yt-lockup-view-model")
                            ) {
                                injectModernButtonsIntoLockupsWithoutOverlay();
                            }
                        }
                    });
                });
            });

            script.modern_thumb_overlay_observer.observe(document.documentElement, {
                childList: true,
                subtree: true
            });
        }

        function initModernThumbOverlays() {
            document
                .querySelectorAll("yt-thumbnail-hover-overlay-toggle-actions-view-model")
                .forEach(injectModernThumbOverlayButton);

            injectModernButtonsIntoLockupsWithoutOverlay();
        }

        function injectModernButtonsIntoLockupsWithoutOverlay() {
            document
                .querySelectorAll([
                    "yt-lockup-view-model",
                    ".yt-lockup-view-model",
                    "ytd-item-section-renderer yt-lockup-view-model",
                    "ytd-watch-next-secondary-results-renderer yt-lockup-view-model"
                ].join(","))
                .forEach(function(lockup) {
                    if (!(lockup instanceof Element)) {
                        return;
                    }

                    if (lockup.querySelector("." + MODERN_QUEUE_BUTTON_CLASS)) {
                        return;
                    }

                    let link =
                        lockup.querySelector('a[href*="/watch?v="]') ||
                        lockup.querySelector('a[href*="youtu.be/"]');

                    if (!link) {
                        return;
                    }

                    let thumbnailHost = findModernThumbnailHost(lockup, link);

                    if (!thumbnailHost) {
                        return;
                    }

                    thumbnailHost.classList.add("youtube-play-next-modern-thumbnail-host");
                    lockup.classList.add("youtube-play-next-modern-lockup-host");
                    bindModernLockupHoverState(lockup);

                    let fallbackOverlay = document.createElement("div");
                    fallbackOverlay.className = "youtube-play-next-modern-fallback-overlay";

                    injectModernThumbOverlayButton(fallbackOverlay);

                    thumbnailHost.appendChild(fallbackOverlay);
                });
        }

        function bindModernLockupHoverState(lockup) {
            if (!lockup || lockup.classList.contains("youtube-play-next-hover-bound")) {
                return;
            }

            lockup.classList.add("youtube-play-next-hover-bound");

            lockup.addEventListener("pointerenter", function() {
                lockup.classList.add("youtube-play-next-modern-lockup-hover");
            }, true);

            lockup.addEventListener("mouseenter", function() {
                lockup.classList.add("youtube-play-next-modern-lockup-hover");
            }, true);

            lockup.addEventListener("pointerleave", function() {
                lockup.classList.remove("youtube-play-next-modern-lockup-hover");
            }, true);

            lockup.addEventListener("mouseleave", function() {
                lockup.classList.remove("youtube-play-next-modern-lockup-hover");
            }, true);
        }

        function findModernThumbnailHost(lockup, fallbackLink) {
            let selectors = [
                "yt-thumbnail-view-model",
                ".yt-thumbnail-view-model",
                "yt-collection-thumbnail-view-model",
                ".yt-lockup-view-model-wiz__content-image",
                ".yt-lockup-view-model__content-image",
                "a[href*='/watch?v=']"
            ];

            for (let i = 0; i < selectors.length; i++) {
                let candidate = lockup.querySelector(selectors[i]);
                if (candidate && candidate.querySelector?.("img")) {
                    return candidate;
                }
            }

            return fallbackLink;
        }

        function injectModernThumbOverlayButton(overlay) {
            if (!overlay || overlay.querySelector("." + MODERN_QUEUE_BUTTON_CLASS)) {
                return;
            }

            let button = document.createElement("button");
            button.type = "button";
            button.className = MODERN_QUEUE_BUTTON_CLASS;
            button.title = "Queue";
            button.setAttribute("aria-label", "Queue");

            button.innerHTML = `
                <svg xmlns="http://www.w3.org/2000/svg" focusable="false" aria-hidden="true" viewBox="0 0 24 24">
                    <path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"></path>
                </svg>
            `;

            [
                "pointerdown",
                "pointerup",
                "mousedown",
                "mouseup",
                "touchstart",
                "touchend",
                "auxclick",
                "dblclick"
            ].forEach(function(eventName) {
                button.addEventListener(eventName, stopModernThumbOverlayEvent, true);
                button.addEventListener(eventName, stopModernThumbOverlayEvent, false);
            });

            button.addEventListener("click", handleModernThumbOverlayClick, true);
            button.addEventListener("click", handleModernThumbOverlayClick, false);

            overlay.appendChild(button);
        }

        function stopModernThumbOverlayEvent(event) {
            event.stopImmediatePropagation();
            event.stopPropagation();
            event.preventDefault();
        }

        function handleModernThumbOverlayClick(event) {
            event.stopImmediatePropagation();
            event.stopPropagation();
            event.preventDefault();

            let videoRenderer = findModernVideoRenderer(event.target, event);

            if (!videoRenderer) {
                openToast("Could not find video item", event.target);
                return;
            }

            let newQueueItem;

            try {
                newQueueItem = QueueItem.fromDOM(videoRenderer);
            } catch (error) {
                if (script.debug) {
                    console.log("QueueItem.fromDOM failed for modern lockup, using fallback", error);
                }

                newQueueItem = QueueItem.fromModernDOM(videoRenderer);
            }

            if (!newQueueItem || !newQueueItem.id) {
                openToast("Could not read video data", event.target);
                return;
            }

            enqueueQueueItemFromThumbOverlay(newQueueItem, event.target);
        }

        function findModernVideoRenderer(startNode, event) {
            let path =
                event?.composedPath?.() ||
                event?.path ||
                eventPathFromNode(startNode);

            for (let i = 0; i < path.length; i++) {
                let node = path[i];

                if (!(node instanceof Element)) {
                    continue;
                }

                if (
                    node.matches &&
                    node.matches([
                        "yt-lockup-view-model",
                        ".yt-lockup-view-model",
                        "ytd-rich-item-renderer",
                        "ytd-rich-grid-video-renderer",
                        "ytd-video-renderer",
                        "ytd-compact-video-renderer",
                        "ytd-grid-video-renderer",
                        "ytd-playlist-video-renderer"
                    ].join(","))
                ) {
                    return node;
                }
            }

            return startNode.closest([
                "yt-lockup-view-model",
                ".yt-lockup-view-model",
                "ytd-rich-item-renderer",
                "ytd-rich-grid-video-renderer",
                "ytd-video-renderer",
                "ytd-compact-video-renderer",
                "ytd-grid-video-renderer",
                "ytd-playlist-video-renderer"
            ].join(","));
        }

        function eventPathFromNode(node) {
            let path = [];

            while (node) {
                path.push(node);
                node = node.parentNode || node.host;
            }

            path.push(window);
            return path;
        }

        function enqueueQueueItemFromThumbOverlay(newQueueItem, target) {
            if (!script.queue.contains(newQueueItem.id)) {
                script.queue.enqueue(newQueueItem);

                if (script.queue_visible && (isWatchPage() || isPlayerMinimized())) {
                    openToast("Video Added to Queue!", target);
                } else if (isPlaylist()) {
                    openToast("Video Added to Queue! Queue is hidden while playlist, mix or native youtube queue is active", target);
                } else {
                    openToast("Video Added to Queue! Play any video to view it.", target);
                }
            } else {
                openToast("Video Already Queued", target);
            }
        }

        function readModernVideoTitle(element, link) {
            let selectors = [
                "a[aria-label][href*='/watch?v=']",
                "a[title][href*='/watch?v=']",
                ".yt-lockup-metadata-view-model-wiz__title",
                ".yt-lockup-metadata-view-model__title",
                ".yt-lockup-view-model-wiz__title",
                "h3 a",
                "#video-title",
                "a[href*='/watch?v=']"
            ];

            for (let i = 0; i < selectors.length; i++) {
                let candidate = element.querySelector(selectors[i]);
                let text =
                    candidate?.getAttribute("title") ||
                    candidate?.getAttribute("aria-label") ||
                    candidate?.textContent?.trim() ||
                    "";

                text = cleanupModernText(text);

                if (text && !/^watch later$/i.test(text) && !/^add to queue$/i.test(text)) {
                    return text;
                }
            }

            let text =
                link?.getAttribute("title") ||
                link?.getAttribute("aria-label") ||
                link?.textContent?.trim() ||
                "Unknown title";

            return cleanupModernText(text) || "Unknown title";
        }

        function readModernVideoChannel(element) {
            let selectors = [
                ".yt-lockup-metadata-view-model-wiz__metadata a",
                ".yt-lockup-metadata-view-model__metadata a",
                "a[href^='/@']",
                "a[href^='/channel/']",
                "a[href^='/c/']",
                "ytd-channel-name a"
            ];

            for (let i = 0; i < selectors.length; i++) {
                let text = element.querySelector(selectors[i])?.textContent?.trim() || "";
                text = cleanupModernText(text);
                if (text) {
                    return text;
                }
            }

            return "";
        }

        function readModernThumbnailUrl(element, videoId) {
            let imgs = element.querySelectorAll("img");

            for (let i = 0; i < imgs.length; i++) {
                let src = imgs[i].currentSrc || imgs[i].src || imgs[i].getAttribute("src") || "";
                if (src && !src.startsWith("data:")) {
                    return src;
                }
            }

            return "https://i.ytimg.com/vi/" + videoId + "/mqdefault.jpg";
        }

        function cleanupModernText(text) {
            return String(text || "")
                .replace(/\s+/g, " ")
                .replace(/ - YouTube$/, "")
                .trim();
        }

        function readModernVideoLength(element) {
            let candidates = element.querySelectorAll([
                "badge-shape",
                "thumbnail-badge-view-model",
                ".badge-shape-wiz__text",
                ".ytd-thumbnail-overlay-time-status-renderer",
                "ytd-thumbnail-overlay-time-status-renderer",
                "[aria-label]"
            ].join(","));

            for (let i = 0; i < candidates.length; i++) {
                let text =
                    candidates[i].textContent?.trim() ||
                    candidates[i].getAttribute("aria-label") ||
                    "";

                let match = text.match(/\b\d{1,2}:\d{2}(?::\d{2})?\b/);

                if (match) {
                    return match[0];
                }
            }

            return "";
        }

        function addThumbOverlay(thumbOverlays) {
            let overlay = {
                "thumbnailOverlayToggleButtonRenderer": {
                    "ytQueue": true,
                    "isToggled": false,
                    "toggledIcon": {iconType: "ADD"},
                    "toggledTooltip": "Queue",
                    "toggledAccessibility": {
                        "accessibilityData": {
                            "label": "Queue"
                        }
                    },
                    "untoggledIcon": {iconType: "ADD"},
                    "untoggledTooltip": "Queue",
                    "untoggledAccessibility": {
                        "accessibilityData": {
                            "label": "Queue"
                        }
                    }
                }
            };

            thumbOverlays.push(overlay);
        }

        function hasThumbOverlay(videoOverlays) {
            for(let i = 0; i < videoOverlays.length; i++) {
                if (videoOverlays[i].thumbnailOverlayToggleButtonRenderer && videoOverlays[i].thumbnailOverlayToggleButtonRenderer.ytQueue) {
                    return true;
                }
            }
            return false;
        }

        function initThumbOverlay(videoRenderer) {
            let videoData = getVideoData(videoRenderer);
            if (videoData && videoData.thumbnailOverlays && !hasThumbOverlay(videoData.thumbnailOverlays) && !videoData.upcomingEventData) {
                addThumbOverlay(videoData.thumbnailOverlays);
            } else if (videoData && videoData?.contentImage?.thumbnailViewModel?.overlays && !hasThumbOverlay(videoData.contentImage.thumbnailViewModel.overlays)) {
                addThumbOverlay(videoData.contentImage.thumbnailViewModel.overlays);
            }
        }

        function initThumbOverlays() {
            let videoRenderers = document.querySelectorAll('ytd-compact-video-renderer, ytd-grid-video-renderer, ytd-video-renderer, ytd-playlist-video-renderer, ytd-rich-grid-video-renderer, ytd-rich-item-renderer');
            forEach(videoRenderers, function(videoRenderer) {
                initThumbOverlay(videoRenderer);
                detectPeekAPic(videoRenderer);
            });
        }

        function detectPeekAPic(videoRenderer) {
            var peekAPicStoryboard = videoRenderer.querySelector(".yt-peek-a-pic-storyboard");
            if (!peekAPicStoryboard) return;
            videoRenderer.classList.add("peek-a-pic-enabled");
        }

        function addThumbOverlayClickListeners() {
            let overlays = document.querySelectorAll('ytd-thumbnail-overlay-toggle-button-renderer > yt-icon');

            forEach(overlays, function(overlay) {
                overlay.removeEventListener("click", handleThumbOverlayClick);

                if (overlay.parentNode.getAttribute("aria-label") !== "Queue") {
                    return;
                }

                overlay.addEventListener("click", handleThumbOverlayClick);
            });
        }

        function handleThumbOverlayClick(event) {
            event.stopPropagation(); event.preventDefault();

            let path = event.path || (event.composedPath && event.composedPath()) || event._composedPath;
            for(let i = 0; i < path.length; i++) {
                let tagNames = ["YTD-COMPACT-VIDEO-RENDERER", "YTD-GRID-VIDEO-RENDERER", "YTD-VIDEO-RENDERER", "YTD-PLAYLIST-VIDEO-RENDERER", "YTD-RICH-GRID-VIDEO-RENDERER", "YTD-RICH-ITEM-RENDERER"];
                if (tagNames.includes(path[i].tagName)) {
                    let newQueueItem = QueueItem.fromDOM(path[i]);
                    enqueueQueueItemFromThumbOverlay(newQueueItem, event.target);
                    break;
                }
            }
        }

        // *** VIDEO PREVIEW THUMB OVERLAYS *** //

        function initVideoPreviewThumbOverlay() {
            let videoPreview = document.querySelector('ytd-video-preview');
            if (videoPreview) {
                let previewControls = videoPreview.querySelector(".ytp-inline-preview-controls");
                if (previewControls) {
                    let queueControl = previewControls.querySelector("#queue-control-add");
                    if (!queueControl) {
                        previewControls.insertAdjacentHTML("beforeend", '<button id="queue-control-add" class="ytp-subtitles-button ytp-button" aria-keyshortcuts="m" data-title-no-tooltip="Queue" aria-label="Queue" title="Queue"><svg xmlns="http://www.w3.org/2000/svg" focusable="false" fill-opacity="1" width="100%" height="100%" viewBox="-5 -7 36 36"><path d="M20 12h-8v8h-1v-8H3v-1h8V3h1v8h8v1z" fill="#fff"></path></svg></button>');
                        previewControls.querySelector("#queue-control-add").addEventListener("click", handlePreviewThumbOverlayClick);
                    }
                }
            }
        }

        function handlePreviewThumbOverlayClick(event) {
            event.stopPropagation(); event.preventDefault();

            let videoPreview = document.querySelector('ytd-video-preview');
            if (videoPreview) {
                let mediaRenderer = insp(videoPreview).__data.opts.mediaRenderer;
                let newQueueItem = QueueItem.fromDOM(mediaRenderer);
                enqueueQueueItemFromThumbOverlay(newQueueItem, event.target);
            }
        }

        // *** BUTTONS *** //

        function initQueueButtons() {
            // Queue controls are now created directly in loadQueueItem().
        }

        function initQueueButtonAction(className, btnAction) {
            // Legacy badge-button initializer. Kept as a no-op for safety.
        }

        function stopQueueButtonEvent(event) {
            event.preventDefault();
            event.stopImmediatePropagation();
            event.stopPropagation();
        }

        // *** POPUPS *** //

        function openToast(text, target) {
            let openPopupAction = {
                "openPopupAction": {
                    "popup": {
                        "notificationActionRenderer": {
                            "responseText": {simpleText: text},
                            "trackingParams": ""
                        }
                    },
                    "popupType": "TOAST"
                }
            };

            let popupContainer = document.querySelector('ytd-popup-container');
            if (!popupContainer) {
                console.log(text);
                return;
            }
            if (popupContainer.handleOpenPopupAction_) {
                popupContainer.handleOpenPopupAction_(openPopupAction, target || document.documentElement);
            } else {
                popupContainer.handleOpenPopupAction(openPopupAction, target || document.documentElement);
            }
        }

        // *** LOCALSTORAGE *** //

        function getCache(key) {
            return JSON.parse(localStorage.getItem("YTQUEUE-MODERN#" + script.version + "#" + key));
        }

        function deleteCache(key) {
            localStorage.removeItem("YTQUEUE-MODERN#" + script.version + "#" + key);
        }

        function setCache(key, value) {
            localStorage.setItem("YTQUEUE-MODERN#" + script.version + "#" + key, JSON.stringify(value));
        }

        // *** CSS *** //

        function injectCSS() {
            let css = `
#youtube-play-next-queue-renderer {
    height: 310px;
    position: sticky;
    border: 1px solid var(--yt-spec-10-percent-layer);
    padding: 5px 0 0 5px;
    margin-bottom: 16px;
    overflow-y: visible;
    overflow-x: hidden;
    resize: vertical;
}

ytd-compact-autoplay-renderer > #contents { padding-bottom: 8px }

.queue-item { margin-top: 0px !important; margin-bottom: 6px !important; }
.queue-item #metadata-line { display: none; }

.queue-button {
    min-height: 24px;
    line-height: 1.7rem !important;
    padding: 5px 8px !important;
    margin: 0 !important;
    cursor: pointer !important;
    z-index: 10000 !important;
    position: relative !important;
    pointer-events: auto !important;
    border: 0 !important;
    border-radius: 2px !important;
    background-color: var(--yt-spec-10-percent-layer);
    color: var(--yt-spec-text-secondary);
    font: inherit !important;
    appearance: none !important;
    -webkit-appearance: none !important;
}

#youtube-play-next-queue-renderer .youtube-play-next-queue-item-wrapper {
    position: relative !important;
    min-height: 104px !important;
    margin-bottom: 8px !important;
    overflow: visible !important;
}

#youtube-play-next-queue-renderer .youtube-play-next-queue-item-wrapper > .queue-item,
#youtube-play-next-queue-renderer .youtube-play-next-queue-item-wrapper > .queue-item.youtube-play-next-queue-item-host {
    position: relative !important;
    margin-bottom: 0 !important;
}

#youtube-play-next-queue-renderer .queue-item .youtube-play-next-queue-actions,
#youtube-play-next-queue-renderer .queue-item .youtube-play-next-queue-positioned-actions {
    display: none !important;
}

#youtube-play-next-queue-renderer .youtube-play-next-queue-item-wrapper > .youtube-play-next-inline-queue-controls {
    position: absolute !important;
    left: 176px !important;
    right: 4px !important;
    top: 66px !important;
    bottom: auto !important;
    display: flex !important;
    flex-direction: row !important;
    flex-wrap: wrap !important;
    align-items: center !important;
    justify-content: flex-start !important;
    gap: 6px !important;
    z-index: 2147483647 !important;
    pointer-events: auto !important;
    opacity: 1 !important;
    visibility: visible !important;
    overflow: visible !important;
}

#youtube-play-next-queue-renderer .youtube-play-next-queue-item-wrapper .youtube-play-next-inline-queue-button {
    display: inline-flex !important;
    align-items: center !important;
    justify-content: center !important;
    min-width: auto !important;
    width: auto !important;
    height: 24px !important;
    min-height: 24px !important;
    max-height: 24px !important;
    line-height: 16px !important;
    padding: 4px 8px !important;
    margin: 0 !important;
    cursor: pointer !important;
    z-index: 2147483647 !important;
    position: relative !important;
    pointer-events: auto !important;
    border: 0 !important;
    border-radius: 3px !important;
    background: rgba(0, 0, 0, 0.78) !important;
    color: #fff !important;
    font-family: Roboto, Arial, sans-serif !important;
    font-size: 12px !important;
    font-weight: 500 !important;
    text-align: center !important;
    white-space: nowrap !important;
    opacity: 1 !important;
    visibility: visible !important;
    appearance: none !important;
    -webkit-appearance: none !important;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.35) !important;
}

#youtube-play-next-queue-renderer .youtube-play-next-queue-item-wrapper .youtube-play-next-inline-queue-button:hover {
    background: rgba(0, 0, 0, 0.92) !important;
    color: #fff !important;
}

#youtube-play-next-queue-renderer .youtube-play-next-queue-item-wrapper .youtube-play-next-inline-queue-button * {
    pointer-events: none !important;
}
.queue-button.queue-play-now, .queue-button.queue-play-next { margin: 5px 3px 5px 0 !important; }
.queue-button:hover { box-shadow: 0px 0px 3px black; }
[dark] .queue-button:hover { box-shadow: 0px 0px 3px white; }

ytd-thumbnail-overlay-toggle-button-renderer[aria-label=Queue] { bottom: 0; top: auto !important; right: auto; left: 0; }
.peek-a-pic-enabled ytd-thumbnail-overlay-toggle-button-renderer[aria-label=Queue] { bottom: 25%; top: auto !important; right: auto; left: 0; }
ytd-thumbnail-overlay-toggle-button-renderer[aria-label=Queue] #label-container { left: 28px !important; right: auto !important; }
ytd-thumbnail-overlay-toggle-button-renderer[aria-label=Queue] #label-container > #label { padding: 0 8px 0 2px !important; }
ytd-thumbnail-overlay-toggle-button-renderer[aria-label=Queue] paper-tooltip { right: -70px !important; left: auto !important }
.queue-item ytd-thumbnail-overlay-toggle-button-renderer[aria-label=Queue] { display: none; }

ytd-thumbnail-overlay-toggle-button-renderer[aria-label=Queued] { display: none; }

yt-thumbnail-hover-overlay-toggle-actions-view-model {
    position: absolute !important;
    inset: 0 !important;
    pointer-events: none !important;
}

yt-thumbnail-hover-overlay-toggle-actions-view-model > * {
    pointer-events: auto !important;
}

.youtube-play-next-modern-queue-button {
    position: absolute !important;
    left: 6px !important;
    bottom: 6px !important;
    top: auto !important;
    right: auto !important;
    width: 34px !important;
    height: 34px !important;
    min-width: 34px !important;
    min-height: 34px !important;
    max-width: 34px !important;
    max-height: 34px !important;
    box-sizing: border-box !important;
    padding: 6px !important;
    margin: 0 !important;
    border: 0 !important;
    border-radius: 50% !important;
    background-color: rgba(0, 0, 0, 0.78) !important;
    color: #fff !important;
    cursor: pointer !important;
    display: flex !important;
    align-items: center !important;
    justify-content: center !important;
    z-index: 2147483647 !important;
    pointer-events: auto !important;
    opacity: 1 !important;
    appearance: none !important;
    -webkit-appearance: none !important;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.55) !important;
    overflow: visible !important;
}

.youtube-play-next-modern-queue-button:hover {
    background-color: rgba(0, 0, 0, 0.92) !important;
}

.youtube-play-next-modern-thumbnail-host {
    position: relative !important;
}

.youtube-play-next-modern-lockup-host,
.youtube-play-next-modern-thumbnail-host {
    position: relative !important;
}

.youtube-play-next-modern-fallback-overlay {
    position: absolute !important;
    inset: 0 !important;
    z-index: 2147483647 !important;
    pointer-events: none !important;
    opacity: 1 !important;
    visibility: visible !important;
    display: block !important;
    overflow: visible !important;
}

.youtube-play-next-modern-fallback-overlay .youtube-play-next-modern-queue-button {
    opacity: 0 !important;
    visibility: visible !important;
    transition: opacity 120ms ease-in-out !important;
}

.youtube-play-next-modern-thumbnail-host:hover .youtube-play-next-modern-fallback-overlay .youtube-play-next-modern-queue-button,
.youtube-play-next-modern-lockup-host:hover .youtube-play-next-modern-fallback-overlay .youtube-play-next-modern-queue-button,
.youtube-play-next-modern-lockup-host.youtube-play-next-modern-lockup-hover .youtube-play-next-modern-fallback-overlay .youtube-play-next-modern-queue-button,
.youtube-play-next-modern-fallback-overlay:hover .youtube-play-next-modern-queue-button,
.youtube-play-next-modern-fallback-overlay:focus-within .youtube-play-next-modern-queue-button {
    opacity: 1 !important;
    visibility: visible !important;
    display: flex !important;
}

.youtube-play-next-modern-fallback-overlay .youtube-play-next-modern-queue-button {
    position: absolute !important;
    left: 6px !important;
    bottom: 6px !important;
    top: auto !important;
    right: auto !important;
    pointer-events: auto !important;
}

.youtube-play-next-modern-queue-button svg {
    width: 22px !important;
    height: 22px !important;
    min-width: 22px !important;
    min-height: 22px !important;
    display: block !important;
    fill: #fff !important;
    color: #fff !important;
    pointer-events: none !important;
    flex: 0 0 auto !important;
    overflow: visible !important;
}
`;

            let style = document.createElement("style");
            style.type = "text/css";
            if (style.styleSheet){
                style.styleSheet.cssText = css;
            } else {
                style.appendChild(document.createTextNode(css));
            }

            (document.body || document.head || document.documentElement).appendChild(style);
        }

        // *** FUNCTIONALITY *** //

        function forEach(array, callback, scope) {
            for (let i = 0; i < array.length; i++) {
                callback.call(scope, array[i], i);
            }
        }

        function forEachReverse(array, callback, scope) {
            for (let i = array.length - 1; i >= 0; i--) {
                callback.call(scope, array[i], i);
            }
        }

        function hmsToSeconds(str) {
            if (!str) {
                return 0;
            }
            let p = str.split(":"),
                s = 0, m = 1;

            while (p.length > 0) {
                s += m * parseInt(p.pop(), 10);
                m *= 60;
            }

            return s;
        }
    }

    function youtube_search_while_watching_video() {

        let script = {
            initialized: false,

            ytplayer: null,

            search_bar: null,
            search_autocomplete: null,
            search_suggestions: [],
            searched: false,

            debug: false
        };

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

        document.addEventListener("DOMContentLoaded", initScript);

        window.addEventListener("yt-page-data-updated", function(event) {
            if (script.debug) { console.log("# page updated #"); }
            cleanupSearch();
            startScript(2);
        });

        function initScript() {
            if (script.debug) { console.log("### Youtube Search While Watching Video Initializing ###"); }

            initSearch();
            injectCSS();

            if (script.debug) { console.log("### Youtube Search While Watching Video Initialized ###"); }
            script.initialized = true;

            startScript(5);
        }

        function startScript(retry) {
            if (script.initialized && isPlayerAvailable()) {
                if (script.debug) { console.log("videoplayer is available"); }
                if (script.debug) { console.log("ytplayer: ", script.ytplayer); }

                if (script.ytplayer) {
                    try {
                        if (script.debug) { console.log("initializing search"); }
                        loadSearch();
                    } catch (error) {
                        console.log("Failed to initialize search: ", (script.debug) ? error : error.message);
                    }
                }
            } else if (retry > 0) {
                setTimeout( function() {
                    startScript(--retry);
                }, 1000);
            } else {
                if (script.debug) { console.log("videoplayer is unavailable"); }
            }
        }

        // *** VIDEOPLAYER *** //

        function getVideoPlayer() {
            return insp(document.getElementById('movie_player'));
        }

        function isPlayerAvailable() {
            script.ytplayer = getVideoPlayer();
            return script.ytplayer !== null && script.ytplayer.getVideoData?.().video_id;
        }

        // *** SEARCH *** //

        function initSearch() {
            window.suggestions_callback = suggestionsCallback;
        }

        function loadSearch() {
            let playlistOrLiveSearchBar = document.querySelector('#suggestions-search.playlist-or-live');
            if (playlistOrLiveSearchBar) { playlistOrLiveSearchBar.remove(); }

            let searchbar = document.getElementById('suggestions-search');
            if (!searchbar) {
                createSearchBar();
            } else {
                searchbar.value = "";
            }

            script.searched = false;
        }

        function cleanupSearch() {
            if (script.search_autocomplete) {
                script.search_autocomplete.destroy();
            }

            cleanupSuggestionRequests();
        }

        function createSearchBar() {
            let anchor, html;

            anchor = document.querySelector('ytd-compact-autoplay-renderer > #contents');
            if (anchor) {
                html = "<input id=\"suggestions-search\" type=\"search\" placeholder=\"Search\">";
                anchor.insertAdjacentHTML("afterend", html);
            } else {
                anchor = document.querySelector('#related > ytd-watch-next-secondary-results-renderer');
                if (anchor) {
                    html = "<input id=\"suggestions-search\" class=\"playlist-or-live\" type=\"search\" placeholder=\"Search\">";
                    anchor.insertAdjacentHTML("beforebegin", html);
                }
            }

            let searchBar = document.getElementById('suggestions-search');
            if (searchBar) {
                script.search_bar = searchBar;

                script.search_autocomplete = new window.autoComplete({
                    selector: '#suggestions-search',
                    minChars: 1,
                    delay: 100,
                    source: function(term, suggest) {
                        script.search_suggestions = {
                            query: term,
                            suggest: suggest
                        };
                        searchSuggestions(term);
                    },
                    onSelect: function(event, term, item) {
                        prepareNewSearchRequest(term);
                    }
                });

                script.search_bar.addEventListener("keyup", function(event) {
                    if (this.value === "") {
                        resetSuggestions();
                    }
                });

                script.search_bar.addEventListener("keydown", function(event) {
                    const ENTER = 13;
                    if (this.value.trim() !== "" && (event.key == "Enter" || event.keyCode === ENTER)) {
                        prepareNewSearchRequest(this.value.trim());
                    }
                });

                script.search_bar.addEventListener("search", function(event) {
                    if(this.value === "") {
                        script.search_bar.blur();
                        script.search_suggestions = [];

                        resetSuggestions();
                    }
                });

                script.search_bar.addEventListener("focus", function(event) {
                    this.select();
                });
            }
        }

        function suggestionsCallback(data) {
            if (script.debug) { console.log(data); }

            let query = data[0];
            if (query !== script.search_suggestions.query) {
                return;
            }

            let raw = data[1];
            let suggestions = raw.map(function(array) {
                return array[0];
            });

            script.search_suggestions.suggest(suggestions);
        }

        function searchSuggestions(query) {
            const GeoLocation = window.yt.config_.INNERTUBE_CONTEXT_GL;
            const HostLanguage = window.yt.config_.INNERTUBE_CONTEXT_HL;

            if (script.debug) { console.log("suggestion request send", query); }
            let scriptElement = document.createElement("script");
            scriptElement.type = "text/javascript";
            scriptElement.className = "suggestion-request";
            scriptElement.src = "https://clients1.google.com/complete/search?client=youtube&hl=" + HostLanguage + "&gl=" + GeoLocation + "&gs_ri=youtube&ds=yt&q=" + encodeURIComponent(query) + "&callback=suggestions_callback";
            (document.body || document.head || document.documentElement).appendChild(scriptElement);
        }

        function cleanupSuggestionRequests() {
            let requests = document.getElementsByClassName('suggestion-request');
            forEachReverse(requests, function(request) {
                request.remove();
            });
        }

        function prepareNewSearchRequest(value) {
            if (script.debug) { console.log("searching for " + value); }

            script.search_bar.blur();
            script.search_suggestions = [];
            cleanupSuggestionRequests();

            sendSearchRequest("https://www.youtube.com/results?pbj=1&search_query=" + encodeURIComponent(value));
        }

        function sendSearchRequest(url) {
            let xmlHttp = new XMLHttpRequest();
            xmlHttp.onreadystatechange = function() {
                if (xmlHttp.readyState == 4 && xmlHttp.status == 200) {
                    processSearch(xmlHttp.responseText);
                }
            };

            xmlHttp.open("GET", url, true);
            xmlHttp.setRequestHeader("x-youtube-client-name", window.yt.config_.INNERTUBE_CONTEXT_CLIENT_NAME);
            xmlHttp.setRequestHeader("x-youtube-client-version", window.yt.config_.INNERTUBE_CONTEXT_CLIENT_VERSION);
            xmlHttp.setRequestHeader("x-youtube-client-utc-offset", new Date().getTimezoneOffset() * -1);

            if (window.yt.config_.ID_TOKEN) {
                xmlHttp.setRequestHeader("x-youtube-identity-token", window.yt.config_.ID_TOKEN);
            }

            xmlHttp.send(null);
        }

        function processSearch(responseText) {
            try {
                let data = JSON.parse(responseText);

                let found = searchJson(data, (key, value) => {
                    if (key === "itemSectionRenderer") {
                        if (script.debug) { console.log(value.contents); }
                        let succeeded = createSuggestions(value.contents);
                        return succeeded;
                    }
                    return false;
                });

                if (!found) {
                    alert("The search request was succesful but the script was unable to parse the results");
                }
            } catch (error) {
                alert("Failed to retrieve search data, sorry!\nError message: " + error.message + "\nSearch response: " + responseText);
            }
        }

        function searchJson(json, func) {
            let found = false;

            for (let item in json) {
                found = func(item, json[item]);
                if (found) { break; }

                if (json[item] !== null && typeof(json[item]) == "object") {
                    found = searchJson(json[item], func);
                    if (found) { break; }
                }
            }

            return found;
        }

        // *** HTML & CSS *** //

        function createSuggestions(data) {
            if (data.length < 10) {
                return false;
            }

            let hidden_continuation_item_renderer;
            let watchRelated = document.querySelector('#related ytd-watch-next-secondary-results-renderer #items ytd-item-section-renderer #contents') || document.querySelector('#related ytd-watch-next-secondary-results-renderer #items');
            forEachReverse(watchRelated.children, function(item) {
                if (item.tagName === "YTD-CONTINUATION-ITEM-RENDERER") {
                    item.setAttribute("hidden", "");
                    hidden_continuation_item_renderer = item;
                } else if (item.tagName !== "YTD-COMPACT-AUTOPLAY-RENDERER") {
                    item.remove();
                }
            });

            forEach(data, function(videoData) {
                if (videoData.videoRenderer || videoData.compactVideoRenderer) {
                    window.Polymer.dom(watchRelated).appendChild(videoQueuePolymer(videoData.videoRenderer || videoData.compactVideoRenderer, "ytd-compact-video-renderer"));
                } else if (videoData.radioRenderer || videoData.compactRadioRenderer) {
                    window.Polymer.dom(watchRelated).appendChild(videoQueuePolymer(videoData.radioRenderer || videoData.compactRadioRenderer, "ytd-compact-radio-renderer"));
                } else if (videoData.playlistRenderer || videoData.compactPlaylistRenderer) {
                    window.Polymer.dom(watchRelated).appendChild(videoQueuePolymer(videoData.playlistRenderer || videoData.compactPlaylistRenderer, "ytd-compact-playlist-renderer"));
                }
            });

            if (hidden_continuation_item_renderer) {
                watchRelated.appendChild(hidden_continuation_item_renderer);
            }

            script.searched = true;

            return true;
        }

        function resetSuggestions() {
            if (script.searched) {
                let itemSectionRenderer = document.querySelector('#related ytd-watch-next-secondary-results-renderer #items ytd-item-section-renderer') || document.querySelector("#related ytd-watch-next-secondary-results-renderer");
                let data = insp(itemSectionRenderer).__data.data;
                createSuggestions(data.contents || data.results);

                let continuation = itemSectionRenderer.querySelector('ytd-continuation-item-renderer[hidden]');
                if (continuation) {
                    continuation.removeAttribute("hidden");
                }
            }

            script.searched = false;
        }

        function videoQueuePolymer(videoData, type) {
            let node = document.createElement(type);
            node.classList.add("style-scope", "ytd-watch-next-secondary-results-renderer", "yt-search-generated");
            node.data = videoData;
            return node;
        }

        function injectCSS() {
            let css = `
.autocomplete-suggestions {
text-align: left; cursor: default; border: 1px solid var(--ytd-searchbox-legacy-border-color); border-top: 0; background: var(--ytd-searchbox-background);
position: absolute; max-height: 254px; overflow: hidden; overflow-y: auto; box-sizing: border-box; box-shadow: -1px 1px 3px rgba(0,0,0,.1);
    left: auto; top: auto; width: 100%; margin: 0; contain: content;
}
.autocomplete-suggestion { position: relative; padding: 0 .6em; line-height: 23px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 1.22em; color: var(--ytd-searchbox-text-color); }
.autocomplete-suggestion b { font-weight: normal; color: #b31217; }
.autocomplete-suggestion.selected { background: #ddd; }
[dark] .autocomplete-suggestion.selected { background: #333; }

autocomplete-holder {
    overflow: visible; position: absolute; left: auto; top: auto; width: 100%; height: 0; z-index: 9999; box-sizing: border-box; margin:0; padding:0; border:0; contain: size layout;
}

ytd-compact-autoplay-renderer { padding-bottom: 0px; }

#suggestions-search {
outline: none; width: 100%; padding: 6px 5px; margin-bottom: 16px;
border: 1px solid var(--ytd-searchbox-legacy-border-color); border-radius: 2px 0 0 2px;
box-shadow: inset 0 1px 2px var(--ytd-searchbox-legacy-border-shadow-color);
color: var(--ytd-searchbox-text-color); background-color: var(--ytd-searchbox-background);
}
`;

            let style = document.createElement("style");
            style.type = "text/css";
            if (style.styleSheet){
                style.styleSheet.cssText = css;
            } else {
                style.appendChild(document.createTextNode(css));
            }

            (document.body || document.head || document.documentElement).appendChild(style);
        }

        // *** FUNCTIONALITY *** //

        function forEach(array, callback, scope) {
            for (let i = 0; i < array.length; i++) {
                callback.call(scope, array[i], i);
            }
        }

        function forEachReverse(array, callback, scope) {
            for (let i = array.length - 1; i >= 0; i--) {
                callback.call(scope, array[i], i);
            }
        }
    }

    // ================================================================================= //
    // =============================== INJECTING SCRIPTS =============================== //
    // ================================================================================= //

    document.documentElement.setAttribute("youtube-play-next-queue", "");

    if (!document.getElementById("autocomplete_script")) {
        let autoCompleteScript = document.createElement('script');
        autoCompleteScript.id = "autocomplete_script";
        autoCompleteScript.type = 'text/javascript';
        autoCompleteScript.textContent = 'window.autoComplete = ' + autoComplete + ';';
        (document.body || document.head || document.documentElement).appendChild(autoCompleteScript);
    }

    if (!document.getElementById("play_next_queue_script")) {
        let playNextQueueScript = document.createElement('script');
        playNextQueueScript.id = "play_next_queue_script";
        playNextQueueScript.type = 'text/javascript';
        playNextQueueScript.textContent = '('+ youtube_play_next_queue_modern +')();';
        (document.body || document.head || document.documentElement).appendChild(playNextQueueScript);
    }

    if (!document.getElementById("search_while_watching_video")) {
        let searchWhileWatchingVideoScript = document.createElement('script');
        searchWhileWatchingVideoScript.id = "search_while_watching_video";
        searchWhileWatchingVideoScript.type = 'text/javascript';
        searchWhileWatchingVideoScript.textContent = '('+ youtube_search_while_watching_video +')();';
        (document.body || document.head || document.documentElement).appendChild(searchWhileWatchingVideoScript);
    }
})();