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