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!

От 22.10.2019. Виж последната версия.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==UserScript==
// @name         Youtube Play Next Queue
// @version      2.0.8
// @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
// @match        https://www.youtube.com/*
// @license      GPL-2.0-or-later; http://www.gnu.org/licenses/gpl-2.0.txt
// @require      https://cdnjs.cloudflare.com/ajax/libs/JavaScript-autoComplete/1.0.4/auto-complete.min.js
// @namespace    https://greasyfork.org/users/16080
// @run-at       document-start
// @grant        none
// @noframes
// ==/UserScript==

/* jshint esversion: 6 */

(function() {
    'use strict';

    // ================================================================================= //
    // ======================= YOUTUBE PLAY NEXT QUEUE (CLASSIC) ======================= //
    // ================================================================================= //

    function youtube_play_next_queue_classic() {
        var script = {
            version: "2.0.0",
            initialized: false,

            queue: null,
            ytplayer: null,

            autoplay_suggestion: null,

            addButtonGenerator: null,
            removeButtonGenerator: null,
            playNextButtonGenerator: null,
            playNowButtonGenerator: null,

            suggestion_observer: null,
            playnext_data_observer: null,

            debug: false
        };

        document.addEventListener("DOMContentLoaded", initScript);

        // reload script on page change using youtube spf events (http://youtube.github.io/js/documentation/events/)
        window.addEventListener("spfdone", function(e) {
            if (script.debug) { console.log("# page updated (classic) #"); }
            startScript(2);
        });

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

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

            initQueue();
            initButtonGenerators();
            injectCSS();

            if (script.debug) { console.log("### Classic youtube loaded ###"); }
            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 && !isPlaylist()) {
                    if (getVideoInfoFromUrl(document.location.href, "t") == "0s") {
                        script.ytplayer.seekTo(0);
                    }

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

                    if (script.debug) { console.log("initializing queue add buttons"); }
                    initAddQueueButtons();

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

                    if (script.debug) { console.log("initializing suggestion observer"); }
                    initSuggestionObserver();

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

        // *** LISTENERS *** //

        function initVideoStateListener() {
            if (!script.ytplayer.classList.contains('initialized-listeners')) {
                script.ytplayer.classList.add('initialized-listeners');
                script.ytplayer.addEventListener("onStateChange", handleVideoStateChanged);

                // run handler once to make sure queue is in sync
                handleVideoStateChanged(script.ytplayer.getPlayerState());
            } else {
                if (script.debug) { console.log("statelistener already initialized"); }
            }
        }

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

            const FINISHED_STATE = 0;
            const PLAYING_STATE = 1;
            const PAUSED_STATE = 2;
            const BUFFERING_STATE = 3;
            const CUED_STATE = 5;

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

            if ((videoState === PLAYING_STATE || videoState === PAUSED_STATE) && !script.queue.isEmpty()) {
                changeNextVideo(script.queue.peek());
            }
        }

        function initSuggestionObserver() {
            if (script.suggestion_observer) {
                script.suggestion_observer.disconnect();
            }

            script.suggestion_observer = new MutationObserver(function(mutations) {
                mutations.forEach(function(mutation) {
                    console.log(mutation);
                    forEach(mutation.addedNodes, function(addedNode) {
                        if (!addedNode.classList.contains('processed-buttons')) {
                            initAddQueueButton(addedNode);
                        }
                    });
                });
            });

            var observables = document.querySelectorAll('#watch-related, #watch-more-related');
            forEach(observables, function(observable) {
                script.suggestion_observer.observe(observable, { childList: true });
            });
        }

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

            // If youtube updates the videoplayer with the autoplay suggestion,
            // replace it with the next video in our queue.
            script.playnext_data_observer = new MutationObserver(function(mutations) {
                if (!script.queue.isEmpty() && !isPlaylist() && !isLivePlayer()) {
                    forEach(mutations, function(mutation) {
                        if (mutation.attributeName === "href") {
                            let nextVideoId = getVideoInfoFromUrl(document.querySelector('.ytp-next-button').href, "v");
                            let nextQueueItem = script.queue.peek();
                            if (nextQueueItem.id !== nextVideoId) {
                                changeNextVideo(nextQueueItem);
                            }
                        }
                    });
                }
            });

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

        // *** VIDEOPLAYER *** //

        function getVideoPlayer() {
            return 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;
        }

        function isLivePlayer() {
            return script.ytplayer.getVideoData().isLive;
        }

        function isPlayerFullscreen() {
            return script.ytplayer.classList.contains('ytp-fullscreen');
        }

        function getVideoInfoFromUrl(url, info) {
            if (url.indexOf("?") === -1) {
                return null;
            }

            let urlVariables = url.split("?")[1].split("&");

            for(var i = 0; i < urlVariables.length; i++) {
                var varName = urlVariables[i].split("=");

                if (varName[0] === info) {
                    return varName[1] === undefined ? null : varName[1];
                }
            }
        }

        // play next video behavior depending on if you're watching fullscreen
        function playNextVideo(nextVideoId) {
            if (script.debug) { console.log("playing next song:", nextVideoId); }
            if (isPlayerFullscreen()) {
                script.ytplayer.loadVideoById(nextVideoId, 0);
            } else {
                window.spf.navigate("https://www.youtube.com/watch?v=" + nextVideoId + "&t=0s");
            }
        }

        // reconfigure the next video button (video player)
        function changeNextVideo(video) {
            if (video.id === script.ytplayer.getVideoData().video_id) {
                return;
            }

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

            // next video autoplay settings
            var related_vid_config = window.yt.config_.RELATED_PLAYER_ARGS;
            if (!related_vid_config) { return; }

            var related_vids_params = related_vid_config.rvs.split(",");
            var first_vid_params = related_vids_params[0];
            var other_vid_params = related_vids_params.slice(1).join(",");

            // changing next video with first from queue
            var params = ["author", "id", "title", "iurlhq", "iurlmq", "length_seconds", "short_view_count_text", "session_data", "endscreen_autoplay_session_data"];
            forEach(params, function(param) {
                var re = new RegExp("(" + param + ")=(.[^&]+)", "g");
                first_vid_params = first_vid_params.replace(re, function($0, param, value) {
                    return param + "=" + encodeURIComponent(video[param] || "");
                });
            });

            script.ytplayer.updateVideoData(JSON.parse('{"rvs":"' + first_vid_params + ',' + other_vid_params + '"}'));
        }

        // extracting video information and creating a video object (that can be added to the queue)
        function findVideoInformation(video) {
            var anchor = video.querySelector('.yt-uix-sessionlink:not(.related-playlist)');
            if (anchor) {
                var id = getVideoInfoFromUrl(video.querySelector('a.yt-uix-sessionlink').href, "v");
                var title = video.querySelector('span.title').textContent.trim();
                var author = video.querySelector('span.author') ? video.querySelector('span.author').textContent.trim() : video.querySelector('span.attribution').textContent.trim();
                var time = video.querySelector('span.video-time') ? video.querySelector('span.video-time').textContent.trim() : "0";
                var thumb = video.querySelector('span.yt-uix-simple-thumb-related > img').dataset.thumb || video.querySelector('span.yt-uix-simple-thumb-related > img').src;
                var sessionData = video.querySelector('a.yt-uix-sessionlink').getAttribute("data-sessionlink");
                return new QueueItem(title, id, video.outerHTML, anchor, author, time, null, thumb, sessionData);
            }
            return null;
        }

        // *** OBJECTS *** //

        // QueueItem object
        function QueueItem(title, id, html, anchor, author, time, stats, thumb, sessionData) {
            this.title = title;
            this.id = id;
            this.html = html;
            this.buttonAnchor = anchor;
            this.author = author;
            this.time = time;
            this.length_seconds = hmsToSecondsOnly(time);
            this.stats = stats;
            this.iurlhq = thumb;
            this.iurlmq = thumb;
            this.session_data = sessionData;
            this.endscreen_autoplay_session_data = "autonav=1&playnext=1&" + sessionData;
        }

        // Queue object
        function Queue() {
            var queue = [];

            this.get = function() {
                return queue;
            };

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

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

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

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

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

            this.enqueue = function(item) {
                queue.push(item);
                this.update();
                this.show(500);
            };

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

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

            this.playNext = function(index) {
                var video = queue.splice(index, 1);
                queue.unshift(video[0]);
                this.update();
                this.show(0);
            };

            this.playNow = function() {
                var video = this.dequeue();
                playNextVideo(video.id);
            };

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

            this.html = function() {
                var html = "";
                queue.forEach(function(item) {
                    html += item.html;
                });
                return html;
            };

            this.show = function(delay) {
                setTimeout(function() {
                    displayQueue();
                }, delay);
            };

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

        // *** QUEUE *** //

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

            if (cachedQueue) {
                script.queue.set(cachedQueue);
            } else {
                setCache("QUEUE", script.queue.get());
            }
        }

        function loadQueue() {
            // prepare html for queue
            var queue = document.querySelector('.autoplay-bar');
            if (queue) {
                queue.id = 'play-next-queue';

                // add class to autoplay suggestion video so it doesn't get queue related buttons
                var suggestion = queue.querySelector('.related-list-item');
                if (suggestion && !suggestion.classList.contains("queue-item")) {
                    script.autoplay_suggestion = findVideoInformation(suggestion);
                }

                // show the queue if not empty
                if (!script.queue.isEmpty()) {
                    displayQueue();
                }
            }
        }

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

            var queue = document.getElementById('play-next-queue');
            if (!queue) {
                return;
            }

            var queueContents = queue.querySelector('.video-list');
            if (!queueContents) {
                return;
            }

            // cleanup current queue
            queueContents.innerHTML = "";

            // display new queue
            if (!script.queue.isEmpty()) {
                // insert new queue
                queueContents.innerHTML = script.queue.html();

                // add buttons
                var items = queueContents.querySelectorAll('.video-list-item');
                forEach(items, function(item, index) {
                    var video = findVideoInformation(item);

                    // remove addbutton if there is one
                    var addedButton = item.querySelector('.queue-add');
                    if (addedButton) { addedButton.remove(); }

                    if (video) {
                        if (index === 0) {
                            changeNextVideo(video);
                            script.playNowButtonGenerator.build(video);
                        } else {
                            script.playNextButtonGenerator.build(video, index);
                        }
                        script.removeButtonGenerator.build(video, index);
                    }

                    item.classList.add('processed-buttons', 'queue-item');
                });

                // show autoplay suggestion under queue if it is not queued
                if (!script.queue.contains(script.autoplay_suggestion.id)) {
                    queueContents.insertAdjacentHTML("beforeend", script.autoplay_suggestion.html);

                    var suggestion = queueContents.querySelector('.video-list-item:last-child');
                    initAddQueueButton(suggestion);
                }

                // replace autoplay options with remove queue button
                var upNext = queue.querySelector('h4.watch-sidebar-head');
                if (upNext) {
                    initRemoveQueueButton(upNext);
                }

                // remove not interested menu
                var menus = queue.getElementsByClassName('yt-uix-menu-trigger');
                forEachReverse(menus, function(menu) {
                    menu.remove();
                });
            } else {
                // restore autoplay suggestion (queue is empty)
                queueContents.innerHTML = script.autoplay_suggestion.html;

                // change next video button of the youtube player
                changeNextVideo(script.autoplay_suggestion);
            }

            // triggering lazyload
            window.scrollTo(window.scrollX, window.scrollY + 1);
            window.scrollTo(window.scrollX, window.scrollY - 1);
        }

        // *** BUTTONS *** //

        // initialize Button Generators (Template design)
        function initButtonGenerators() {
            AddButtonGenerator.prototype = new ButtonGenerator();
            script.addButtonGenerator = new AddButtonGenerator();

            RemoveButtonGenerator.prototype = new ButtonGenerator();
            script.removeButtonGenerator = new RemoveButtonGenerator();

            PlayNextButtonGenerator.prototype = new ButtonGenerator();
            script.playNextButtonGenerator = new PlayNextButtonGenerator();

            PlayNowButtonGenerator.prototype = new ButtonGenerator();
            script.playNowButtonGenerator = new PlayNowButtonGenerator();
        }

        function initAddQueueButton(video) {
            try {
                var videoData = findVideoInformation(video);
                video.classList.add('processed-buttons');
                if (videoData) {
                    script.addButtonGenerator.build(videoData);
                }
            } catch(error) {
                console.error("Couldn't initialize \"Add to queue\" buttons for a video \n" + error.message);
            }
        }

        function initAddQueueButtons() {
            var videos = document.querySelectorAll('#watch-related .video-list-item:not(.processed-buttons)');
            forEach(videos, function(video) {
                initAddQueueButton(video);
            });
        }

        // Button template
        function ButtonGenerator() {
            this.build = function(video, index) {
                var anchor = video.buttonAnchor;
                var html = "<div class=\"queue-button " + this.type + " yt-uix-button yt-uix-button-default yt-uix-button-size-default\"><button class=\"yt-uix-button-content\">" + this.text + "</button></div>";
                anchor.insertAdjacentHTML("beforeend", html);
                var button = this;

                anchor.getElementsByClassName(this.type)[0].addEventListener("click", function handler(e) {
                    e.preventDefault();
                    button.clickBehavior(this, video, index);
                    e.currentTarget.removeEventListener(e.type, handler);
                    this.addEventListener("click", function(e) {
                        e.preventDefault();
                    });
                });
            };
        }

        function AddButtonGenerator() {
            this.type = "queue-add";
            this.text = "Add to queue";
            this.clickBehavior = function(element, video, index) {
                if (!script.queue.contains(video.id)) {
                    script.queue.enqueue(video);
                    element.textContent = "Queued!";
                } else {
                    element.textContent = "Already Queued";
                }
            };
        }

        function RemoveButtonGenerator() {
            this.type = "queue-remove";
            this.text = "Remove";
            this.clickBehavior = function(element, video, index) {
                element.textContent = "Removed!";
                script.queue.remove(index);
                restoreAddButton(video.id);
            };
        }

        function PlayNextButtonGenerator() {
            this.type = "queue-next";
            this.text = "Play Next";
            this.clickBehavior = function(element, video, index) {
                this.textContent = "To the top!";
                script.queue.playNext(index);
            };
        }

        function PlayNowButtonGenerator() {
            this.type = "queue-now";
            this.text = "Play Now";
            this.clickBehavior = function(element, video, index) {
                this.textContent = "Playing!";
                script.queue.playNow();
            };
        }

        // The "remove queue and all its videos" button
        function initRemoveQueueButton(anchor) {
            var html = "<div class=\"queue-button remove-queue yt-uix-button yt-uix-button-default yt-uix-button-size-default\"><button class=\"yt-uix-button-content\">Remove Queue</button></div>";
            anchor.innerHTML = html;

            anchor.getElementsByClassName('remove-queue')[0].addEventListener("click", function handler(e) {
                e.preventDefault();
                script.queue.reset();
                restoreAddButton("*"); // restore all add buttons
                this.parentNode.innerHTML = "Up Next";
            });
        }

        function restoreAddButton(id) {
            var videos = document.querySelectorAll('#watch-related .related-list-item');
            forEach(videos, function(video) {
                if (id === "*" || id === getVideoInfoFromUrl(video.querySelector('a.yt-uix-sessionlink').href, "v")) {
                    // remove current addbutton if there is one
                    var addedButton = video.querySelector('.queue-add');
                    if (addedButton) { addedButton.remove(); }

                    // make new addbutton
                    var videoData = findVideoInformation(video);
                    if (videoData) {
                        script.addButtonGenerator.build(videoData);
                    }
                }
            });
        }

        // *** LOCALSTORAGE *** //

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

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

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

        // injecting css
        function injectCSS() {
            var css = `
'#play-next-queue' { list-style: none; }
'#play-next-queue' .standalone-collection-badge-renderer-icon { display: none; }
'#play-next-queue' .standalone-collection-badge-renderer-text { display: none; }
'#play-next-queue' .related-list-item span.title { max-height: 2.3em; }

.processed-buttons .queue-add { display: none; }
.processed-buttons:hover .queue-add { display: inline-block; }
#watch-related .processed-buttons:hover .standalone-collection-badge-renderer-icon { display: none; }
#watch-related .processed-buttons:hover .standalone-collection-badge-renderer-text { display: none; }

.queue-item span.title { max-height: 2.3em; }
.related-list-item:hover span.title { max-height: 2.3em; }
.queue-button { height: 15px; padding: 0.2em 0.4em 0.2em 0.4em; margin: 2px 0; }
.queue-remove { margin-left: 4px; }
`;

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

            document.documentElement.appendChild(style);
        }

        // *** FUNCTIONALITY *** //

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

        // When you want to remove elements
        function forEachReverse(array, callback, scope) {
            for (var i = array.length - 1; i >= 0; i--) {
                callback.call(scope, array[i], i);
            }
        }

        // hh:mm:ss => only seconds
        function hmsToSecondsOnly(str) {
            var p = str.split(":"),
                s = 0, m = 1;

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

            return s;
        }
    }

    // ================================================================================ //
    // ======================= YOUTUBE PLAY NEXT QUEUE (MODERN) ======================= //
    // ================================================================================ //

    function youtube_play_next_queue_modern() {
        let script = {
            version: "2.0.0",
            initialized: false,

            queue: null,
            ytplayer: null,

            autoplay_suggestion: null,
            queue_rendered_observer: null,
            video_renderer_observer: null,
            playnext_data_observer: null,

            debug: false
        };

        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 (material) #"); }
            startScript(2);
        });

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

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

            initQueue();
            injectCSS();

            // TODO, better / more efficient alternative?
            setInterval(addThumbOverlayClickListeners, 250);
            setInterval(initThumbOverlays, 1000);

            if (script.debug) { console.log("### Modern youtube loaded ###"); }
            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 && !isPlaylist()) {
                    if (script.debug) { console.log("initializing queue"); }
                    loadQueue();

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

                    if (script.debug) { console.log("initializing playnext data observer"); }
                    initPlayNextDataObserver();
                }
            } 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);

                // run handler once to make sure queue is in sync
                handleVideoStateChanged(script.ytplayer.getPlayerState());
            } else {
                if (script.debug) { console.log("statelistener already initialized"); }
            }
        }

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

            const FINISHED_STATE = 0;
            const PLAYING_STATE = 1;
            const PAUSED_STATE = 2;
            const BUFFERING_STATE = 3;
            const CUED_STATE = 5;

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

            if ((videoState === PLAYING_STATE || videoState === PAUSED_STATE) && !script.queue.isEmpty()) {
                script.queue.peek().setAsNextVideo();
            }

            if (videoState === PAUSED_STATE) {
                // TODO: check if this works
                // Check for annoying "are you still watching" popup
                setTimeout(() => {
                    let button = document.getElementById('confirm-button');
                    if (button && button.offsetParent === null) {
                        if (script.debug) { console.log("### Clicking confirm button popup ###"); }
                        button.click();
                    }
                }, 1000);
            }
        }

        function initQueueRenderedObserver() {
            if (script.queue_rendered_observer) {
                script.queue_rendered_observer.disconnect();
            }

            // if the queue is completely rendered, mutationCount is equal to the queue size
            // => initialize queue button listeners for Play Now, Play Next and Remove
            let mutationCount = 0;
            script.queue_rendered_observer = new MutationObserver(function(mutations) {
                mutations.forEach(function(mutation) {
                    mutationCount += mutation.addedNodes.length;

                    if (mutationCount === script.queue.size()) {
                        initQueueButtons();
                        script.queue_rendered_observer.disconnect();
                    }
                });
            });

            let observable = document.querySelector('ytd-compact-autoplay-renderer > #contents');
            script.queue_rendered_observer.observe(observable, { childList: true });
        }

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

            // If youtube updates the videoplayer with the autoplay suggestion,
            // replace it with the next video in our queue.
            script.playnext_data_observer = new MutationObserver(function(mutations) {
                if (!script.queue.isEmpty() && !isPlaylist() && !isLivePlayer()) {
                    forEach(mutations, function(mutation) {
                        if (mutation.attributeName === "href") {
                            let nextVideoId = getVideoInfoFromUrl(document.querySelector('.ytp-next-button').href, "v");
                            let nextQueueItem = script.queue.peek();
                            if (nextQueueItem.id !== nextVideoId) {
                                nextQueueItem.setAsNextVideo();
                            }
                        }
                    });
                }
            });

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

        /* function initVideoRendererObserver() {
            if (script.video_renderer_observer) {
                script.video_renderer_observer.disconnect();
            }

            script.video_renderer_observer = new MutationObserver(function(mutations) {
                mutations.forEach(function(mutation) {
                    forEach(mutation.addedNodes, function(node) {
                        let tagNames = ["YTD-COMPACT-VIDEO-RENDERER", "YTD-GRID-VIDEO-RENDERER", "YTD-VIDEO-RENDERER"];
                        if (tagNames.includes(node.tagName)) {
                            initThumbOverlay(node);

                            // If youtube updates node data, reinit thumb overlay
                            new MutationObserver(function(mutations) {
                                mutations.forEach(function(mutation) {
                                    initThumbOverlay(mutation.target);
                                });
                            }).observe(node, { attributes: true });
                        }
                    });
                });
            });

            let observable = document.querySelector('ytd-watch-next-secondary-results-renderer > #items');
            script.video_renderer_observer.observe(observable, { childList: true });
        } */

        // *** VIDEOPLAYER *** //

        function getVideoPlayer() {
            return 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;
        }

        function isLivePlayer() {
            return script.ytplayer.getVideoData().isLive;
        }

        function isPlayerFullscreen() {
            return script.ytplayer.classList.contains('ytp-fullscreen');
        }

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

        function getVideoInfoFromUrl(url, info) {
            if (url.indexOf("?") === -1) {
                return null;
            }

            let urlVariables = url.split("?")[1].split("&");

            for(let i = 0; i < urlVariables.length; i++) {
                let varName = urlVariables[i].split("=");

                if (varName[0] === info) {
                    return varName[1] === undefined ? null : varName[1];
                }
            }
        }

        // *** OBJECTS *** //

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

            getRelatedVideoArgs() {
                let args = {
                    id: this.data.videoId,
                    title: this.data.title.simpleText,
                    author: this.data.author || this.data.shortBylineText.runs[0].text,
                    length_seconds: hmsToSeconds(this.getVideoLength()),
                    aria_label: this.data.title.accessibility.accessibilityData.label,
                    iurlmq: this.getSmallestThumb().url,
                    iurlhq: this.getBiggestThumb().url,
                    session_data: "itct=" + this.data.navigationEndpoint.clickTrackingParams,
                    short_view_count_text: this.data.shortViewCountText ? this.data.shortViewCountText.simpleText : "",
                    endscreen_autoplay_session_data: "autonav=1&playnext=1&itct=" + this.data.navigationEndpoint.clickTrackingParams,
                };

                return args;
            }

            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() {
                return this.data.thumbnail.thumbnails.reduce(function (thumb, currentSmallestThumb) {
                    return (currentSmallestThumb.height * currentSmallestThumb.width < thumb.height * thumb.width) ? currentSmallestThumb : thumb;
                });
            }

            getBiggestThumb() {
                return this.data.thumbnail.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;

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

                // next video autoplay settings
                let relatedVideoConfig = document.querySelector('ytd-player').__data.watchNextData.webWatchNextResponseExtensionData;
                let relatedVideosArgsList = relatedVideoConfig.relatedVideoArgs.split(",");
                let firstVideoArgs = relatedVideosArgsList[0];
                let otherVideoArgs = relatedVideosArgsList.slice(1).join(",");

                let videoParams = this.getRelatedVideoArgs();

                // changing next video with first from queue
                forEach(Object.keys(videoParams), function(param) {
                    let re = new RegExp("(" + param + ")=(.[^&]+)", "g");
                    firstVideoArgs = firstVideoArgs.replace(re, function($0, param, value) {
                        return param + "=" + encodeURIComponent(videoParams[param] || "");
                    });
                });

                script.ytplayer.updateVideoData(JSON.parse('{"rvs":"' + firstVideoArgs + ',' + otherVideoArgs + '"}'));
            }

            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.setAttribute("draggable", true);
                node.data = this.data;
                return node;
            }

            static fromDOM(element) {
                let data = Object.assign({}, element.__data.data);
                data.navigationEndpoint.watchEndpoint = { "videoId": data.videoId };
                data.navigationEndpoint.commandMetadata = { "webCommandMetadata": { "url": "/watch?v=" + data.videoId, webPageType: "WEB_PAGE_TYPE_WATCH" } };
                data.shortBylineText = data.shortBylineText || { "runs": [ { "text": data.title.accessibility.accessibilityData.label.match(/by\s(.+?)\s\d+/)[1] } ] };

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

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

            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) {
                let video = this.queue.splice(index, 1);
                this.queue.unshift(video[0]);
                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) {
                cachedQueue = cachedQueue.map(queueItem => QueueItem.fromJSON(queueItem));
                script.queue.set(cachedQueue);
            } else {
                setCache("QUEUE", script.queue.get());
            }
        }

        function loadQueue() {
            // prepare html for queue
            let queue = document.querySelector('ytd-compact-autoplay-renderer');

            if (!queue) {
                return;
            }

            let suggestion = queue.querySelector('ytd-compact-video-renderer');
            if (suggestion) {
                script.autoplay_suggestion = QueueItem.fromDOM(suggestion);
            }

            // show the queue if not empty
            if (!script.queue.isEmpty()) {
                displayQueue();
            }
        }

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

            let queue = document.querySelector('ytd-compact-autoplay-renderer');
            if (!queue) { return; }

            let queueContents = queue.querySelector('#contents');
            if (!queueContents) { return; }

            initQueueRenderedObserver();

            // clear current content
            queueContents.innerHTML = "";

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

                // show autoplay suggestion under queue if it is not queued
                if (!script.queue.contains(script.autoplay_suggestion.id)) {
                    window.Polymer.dom(queueContents).appendChild(script.autoplay_suggestion.toNode());
                }

                // initialize remove queue button.
                let upNext = queue.querySelector("#upnext");
                if (upNext) {
                    initRemoveQueueButton(upNext);
                }
            } else {
                // restore autoplay suggestion (queue is empty)
                script.autoplay_suggestion.setAsNextVideo();
                window.Polymer.dom(queueContents).appendChild(script.autoplay_suggestion.toNode());

                // restore up next header
                let upNext = queue.querySelector("#upnext");
                if (upNext) {
                    upNext.innerHTML = "Up next";
                }
            }
        }

        function loadQueueItem(item, index, queueContents) {
            item.clearBadges();
            if (index === 0) {
                item.setAsNextVideo();
                item.addBadge("Play Now", ["QUEUE_BUTTON", "QUEUE_PLAY_NOW"]);
                // item.addBadge("↓", ["QUEUE_BUTTON", "QUEUE_MOVE_DOWN"]);
                item.addBadge("Remove", ["QUEUE_BUTTON", "QUEUE_REMOVE"]);
            } else {
                item.addBadge("Play Next", ["QUEUE_BUTTON", "QUEUE_PLAY_NEXT"]);
                // item.addBadge("↑", ["QUEUE_BUTTON", "QUEUE_MOVE_UP"]);
                // item.addBadge("↓", ["QUEUE_BUTTON", "QUEUE_MOVE_DOWN"]);
                item.addBadge("Remove", ["QUEUE_BUTTON", "QUEUE_REMOVE"]);
            }
            window.Polymer.dom(queueContents).appendChild(item.toNode(["queue-item"]));
        }

        // The "remove queue and all its videos" button
        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 addThumbOverlay(thumbOverlays) {
            // we don't use the toggled icon, that's why both have the same values.
            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 = videoRenderer.__data.data;

            if (videoData && videoData.thumbnailOverlays && !hasThumbOverlay(videoData.thumbnailOverlays)) {
                addThumbOverlay(videoData.thumbnailOverlays);
            }
        }

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

        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"];
                if (tagNames.includes(path[i].tagName)) {
                    let newQueueItem = QueueItem.fromDOM(path[i]);
                    if (!script.queue.contains(newQueueItem.id)) {
                        script.queue.enqueue(newQueueItem);
                        openToast("Video Added to Queue", event.target);
                    } else {
                        openToast("Video Already Queued", event.target);
                    }
                    break;
                }
            }
        }

        // *** BUTTONS *** //

        function initQueueButtons() {
            // initQueueButtonAction("queue-play-now", () => script.queue.playNow());
            initQueueButtonAction("queue-play-next", (pos) => script.queue.playNext(pos+1));
            initQueueButtonAction("queue-remove", (pos) => script.queue.remove(pos));
        }

        function initQueueButtonAction(className, btnAction) {
            let buttons = document.getElementsByClassName(className);

            forEach(buttons, function(button, index) {
                let pos = index;
                if (!button.classList.contains("button-listener")) {
                    button.addEventListener("click", function(event) {
                        event.preventDefault();
                        event.stopPropagation();
                        btnAction(pos);
                    });
                    button.classList.add("button-listener");
                }
            });
        }

        // *** POPUPS *** //

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

            let popupContainer = document.querySelector('ytd-popup-container');
            popupContainer.handleOpenPopupAction_(openPopupAction, target);
        }

        // *** 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 *** //

        // injecting css
        function injectCSS() {
            let css = `
.queue-button { height: 15px; line-height: 1.7rem !important; padding: 5px !important; margin: 5px 3px !important; cursor: default; z-index: 99; background-color: var(--yt-spec-10-percent-layer); color: var(--yt-spec-text-secondary); }
.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; }
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; }

.queue-item #metadata-line { display: none; }

#upnext.flex-none { flex: 0 !important; white-space: nowrap; }
#upnext > .queue-button { font-size: 1.4rem; font-weight: 500; margin: 0 !important; }
.flex-whitebox { flex: 1; }

[draggable] {
  -moz-user-select: none;
  -khtml-user-select: none;
  -webkit-user-select: none;
  user-select: none;
  /* Required to make elements draggable in old WebKit */
  -khtml-user-drag: element;
  -webkit-user-drag: element;
}
`;
            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);
            }
        }

        // When you want to remove elements
        function forEachReverse(array, callback, scope) {
            for (let i = array.length - 1; i >= 0; i--) {
                callback.call(scope, array[i], i);
            }
        }

        // hh:mm:ss => only seconds
        function hmsToSeconds(str) {
            let p = str.split(":"),
                s = 0, m = 1;

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

            return s;
        }
    }

    // ================================================================================= //
    // ====================== YOUTUBE SEARCH WHILE WATCHING VIDEO ====================== //
    // ================================================================================= //

    function youtube_search_while_watching_video() {
        var script = {
            initialized: false,
            modern: false,

            ytplayer: null,

            search_bar: null,
            search_timeout: null,
            search_suggestions: [],
            suggestion_observer: null,

            debug: false
        };

        document.addEventListener("DOMContentLoaded", initScript);

        // reload script on page change using youtube spf events (https://youtube.github.io/spfjs/documentation/events/)
        window.addEventListener("spfdone", function(e) {
            if (script.debug) { console.log("# page updated (normal) #"); }
            startScript(2);
        });

        // 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 (material) #"); }
            startScript(2);
        });

        function initScript() {
            if (script.debug) { console.log("Youtube search while watching video initializing"); }

            if (window.Polymer === undefined) {
                if (script.debug) { console.log("### Classic youtube loaded ###"); }
                script.modern = false;
            } else {
                if (script.debug) { console.log("### Modern youtube loaded ###"); }
                script.modern = true;
            }

            initSearch();
            initSuggestionObserver();
            injectCSS();

            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) { // fix conflict with Youtube+ script
                setTimeout( function() {
                    startScript(--retry);
                }, 1000);
            } else {
                if (script.debug) { console.log("videoplayer is unavailable"); }
            }
        }

        // *** OBSERVERS *** //

        function initSuggestionObserver() {
            script.suggestion_observer = new MutationObserver(function(mutations) {
                mutations.forEach(function(mutation) {
                    forEach(mutation.addedNodes, function(addedNode) {
                        if (!addedNode.classList.contains('yt-search-generated') && addedNode.tagName !== "YTD-COMPACT-AUTOPLAY-RENDERER") {
                            addedNode.classList.add('suggestion-tag');
                        }
                    });
                });
            });
        }

        // *** VIDEOPLAYER *** //

        // video object (normal youtube only)
        function YtVideo(id, title, author, time, stats, thumb, sessionData) {
            this.id = id;
            this.title = title;
            this.author = author;
            this.time = time;
            this.stats = stats;
            this.iurlhq = thumb;
            this.iurlmq = thumb;
            this.session_data = sessionData;
        }

        function getVideoPlayer() {
            return 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;
        }

        function isLivePlayer() {
            return script.ytplayer.getVideoData().isLive;
        }

        // *** SEARCH *** //

        function initSearch() {
            // callback function for search suggestion results
            window.suggestions_callback = suggestionsCallback;
        }

        function loadSearch() {
            if (script.modern) {
                showSuggestions(true);

                // prevent double searchbar
                var playlistOrLiveSearchBar = document.querySelector('#suggestions-search.playlist-or-live');
                if (playlistOrLiveSearchBar) { playlistOrLiveSearchBar.remove(); }
            }

            if (!document.getElementById('suggestions-search')) {
                createSearchBar();
                tagCurrentSuggestions();
            }

            cleanupSuggestionRequests();
        }

        function createSearchBar() {
            var anchor, html;

            if (script.modern) {
                anchor = document.querySelector('ytd-compact-autoplay-renderer > #contents');
                if (anchor) {
                    html = "<input id=\"suggestions-search\" type=\"search\" placeholder=\"Search\">";
                    anchor.insertAdjacentHTML("afterend", html);
                } else { // playlist or live video?
                    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);
                    }
                }
            } else {
                anchor = document.querySelector('#watch7-sidebar-modules > div:nth-child(2)');
                if (anchor) {
                    html = "<input id=\"suggestions-search\" class=\"search-term yt-uix-form-input-bidi\" type=\"search\" placeholder=\"Search\">";
                    anchor.insertAdjacentHTML("afterbegin", html);
                } else { // playlist or live video?
                    anchor = document.querySelector('#watch7-sidebar-modules');
                    if (anchor) {
                        html = "<input id=\"suggestions-search\" class=\"search-term yt-uix-form-input-bidi playlist-or-live\" type=\"search\" placeholder=\"Search\">";
                        anchor.insertAdjacentHTML("afterbegin", html);
                    }
                }
            }

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

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

                script.search_bar.addEventListener("keyup", function(event) {
                    if (this.value === "") {
                        showSuggestions(true);
                    } else {
                        searchSuggestions(this.value);
                    }
                });

                // seperate keydown listener because the search listener blocks keyup..?
                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(); // close search suggestions dropdown
                        script.search_suggestions = []; // clearing the search suggestions
                        showSuggestions(true);
                    }
                });

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

        // add class to current suggestions, so we can toggle hide/show
        function tagCurrentSuggestions() {
            if (script.suggestion_observer) {
                script.suggestion_observer.disconnect();

                var observables = document.querySelectorAll('ytd-watch-next-secondary-results-renderer > #items, #watch-related, #watch-more-related');
                forEach(observables, function(observable) {
                    script.suggestion_observer.observe(observable, { childList: true });
                });
            }

            var suggestions = document.querySelectorAll('#watch-related > li.video-list-item, ytd-compact-video-renderer.ytd-watch-next-secondary-results-renderer, ytd-compact-radio-renderer.ytd-watch-next-secondary-results-renderer');
            forEach(suggestions, function(suggestion) {
                suggestion.classList.add('suggestion-tag');
            });
        }

        // toggle hide/show suggestions depending on $show and remove previously searched videos if any
        function showSuggestions(show) {
            var videoListItems = document.querySelectorAll('#watch-related > li.video-list-item, #watch-more-related > li.video-list-item, #items > ytd-compact-video-renderer, #items > ytd-compact-radio-renderer, #items > ytd-compact-playlist-renderer');

            forEachReverse(videoListItems, function(video) {
                if (video.classList.contains('suggestion-tag')) {
                    video.style.display = (show) ? "" : "none";
                } else {
                    video.remove();
                }
            });

            if (!script.modern) {
                var watchRelated = document.getElementById('watch-related');

                var currNavigation = watchRelated.parentNode.querySelector('.search-pager');
                if (currNavigation) { currNavigation.remove(); } // remove navigation

                var seperationLine = watchRelated.parentNode.querySelector('.watch-sidebar-separation-line');
                if (seperationLine) { seperationLine.remove(); } // remove seperation line
            }

            var showMore = document.getElementById('watch-more-related-button') || document.querySelector('#continuations.ytd-watch-next-secondary-results-renderer');
            if (showMore) { showMore.style.display = (show) ? "" : "none"; } // toggle hide/show the "More Suggestions" link
        }

        // callback from search suggestions attached to window
        function suggestionsCallback(data) {
            var raw = data[1]; // extract relevant data from json
            var suggestions = raw.map(function(array) {
                return array[0]; // change 2D array to 1D array with only suggestions
            });
            if (script.debug) { console.log(suggestions); }
            script.search_suggestions = suggestions;
        }

        function searchSuggestions(value) {
            if (script.search_timeout !== null) { clearTimeout(script.search_timeout); }

            // youtube search parameters
            const GeoLocation = window.yt.config_.INNERTUBE_CONTEXT_GL;
            const HostLanguage = window.yt.config_.INNERTUBE_CONTEXT_HL;

            // only allow 1 suggestion request every 100 milliseconds
            script.search_timeout = setTimeout(function() {
                if (script.debug) { console.log("suggestion request send", this.searchValue); }
                var 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(this.searchValue) + "&callback=suggestions_callback";
                (document.body || document.head || document.documentElement).appendChild(scriptElement);
            }.bind({searchValue:value}), 100);
        }

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

        // send new search request (with the search bar)
        function prepareNewSearchRequest(value) {
            if (script.debug) { console.log("searching for " + value); }

            script.search_bar.blur(); // close search suggestions dropdown
            script.search_suggestions = []; // clearing the search suggestions

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

        // given the url, retrieve the search results
        function sendSearchRequest(url) {
            var xmlHttp = new XMLHttpRequest();
            xmlHttp.onreadystatechange = function() {
                if (xmlHttp.readyState == 4 && xmlHttp.status == 200) {
                    if (script.modern) {
                        processSearchModern(xmlHttp.responseText);
                    } else {
                        var container = document.implementation.createHTMLDocument().documentElement;
                        container.innerHTML = xmlHttp.responseText;
                        processSearch(container);
                    }
                }
            };

            xmlHttp.open("GET", url, true);

            if (script.modern) {
                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) { // null if not logged in
                    xmlHttp.setRequestHeader("x-youtube-identity-token", window.yt.config_.ID_TOKEN);
                }
            }

            xmlHttp.send(null);
        }

        // process search request (normal youtube)
        function processSearch(container) {
            var watchRelated = document.getElementById('watch-related');

            // hide current suggestions and remove searched videos if any
            showSuggestions(false);

            // insert searched videos
            var videoItems = container.querySelectorAll('.item-section .yt-lockup-video');
            forEach(videoItems, function(videoItem) {
                if (videoItem.querySelector('.yt-badge-live') === null) {
                    try {
                        var videoId = videoItem.dataset.contextItemId;
                        var videoTitle = videoItem.querySelector('.yt-lockup-title > a').title;
                        var videoStats = videoItem.querySelector('.yt-lockup-meta').innerHTML;
                        var videoTime = videoItem.querySelector('.video-time') ? videoItem.querySelector('.video-time').textContent : "0";
                        var author = videoItem.querySelector('.yt-lockup-byline') ? videoItem.querySelector('.yt-lockup-byline').textContent : "";
                        var videoThumb = videoItem.querySelector('div.yt-lockup-thumbnail img').dataset.thumb || videoItem.querySelector('div.yt-lockup-thumbnail img').src;
                        var sessionData = videoItem.querySelector('a.yt-uix-sessionlink').getAttribute("data-sessionlink");

                        var videoObject = new YtVideo(videoId, videoTitle, author, videoTime, videoStats, videoThumb, sessionData);
                        if (script.debug) { console.log(videoObject); }

                        watchRelated.insertAdjacentHTML("beforeend", videoQueueHTML(videoObject).html);
                    } catch (error) {
                        console.error("failed to process video " + error.message, videoItem);
                    }
                }
            });

            // insert navigation buttons
            var navigation = container.querySelector('.search-pager');
            var navigationButtons = navigation.getElementsByTagName('a');
            forEach(navigationButtons, function(button) {
                button.addEventListener("click", function handler(e) {
                    e.preventDefault();
                    script.search_bar.scrollIntoView();
                    window.scrollBy(0, -1 * document.getElementById('yt-masthead-container').clientHeight);
                    sendSearchRequest(this.href);
                });
            });

            watchRelated.parentNode.appendChild(navigation); // append new navigation
            watchRelated.insertAdjacentHTML("afterend", "<hr class=\"watch-sidebar-separation-line\">"); // insert separation line between videos and navigation
        }

        // process search request (material youtube)
        function processSearchModern(responseText) {
            var data = JSON.parse(responseText);

            if (data && data[1] && data[1].response) {
                try {
                    // dat chain o.O
                    var videosData = data[1].response.contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents[0].itemSectionRenderer.contents;
                    if (script.debug) { console.log(videosData); }

                    // hide current suggestions and remove previously searched videos if any
                    showSuggestions(false);

                    var watchRelated = document.querySelector('ytd-watch-next-secondary-results-renderer #items');
                    forEach(videosData, function(videoData) {
                        if (videoData.videoRenderer) {
                            window.Polymer.dom(watchRelated).appendChild(videoQueuePolymer(videoData.videoRenderer, "ytd-compact-video-renderer"));
                        } else if (videoData.radioRenderer) {
                            window.Polymer.dom(watchRelated).appendChild(videoQueuePolymer(videoData.radioRenderer, "ytd-compact-radio-renderer"));
                        } else if (videoData.playlistRenderer) {
                            window.Polymer.dom(watchRelated).appendChild(videoQueuePolymer(videoData.playlistRenderer, "ytd-compact-playlist-renderer"));
                        }
                    });
                } catch (error) {
                    alert("failed to retrieve search data, sorry! " + error.message);
                }
            }
        }

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

        function videoQueueHTML(video) {
            var strVar = "";

            strVar += "<li class=\"video-list-item related-list-item show-video-time related-list-item-compact-video yt-search-generated\">";
            strVar += "    <div class=\"related-item-dismissable\">";
            strVar += "        <div class=\"content-wrapper\">";
            strVar += "            <a href=\"\/watch?v=" + video.id + "\" class=\"yt-uix-sessionlink content-link spf-link spf-link\" data-sessionlink=\"" + video.session_data + "\" rel=\"spf-prefetch\" title=\"" + video.title + "\">";
            strVar += "                <span dir=\"ltr\" class=\"title\">" + video.title + "<\/span>";
            strVar += "				   <span class=\"stat author\">" + video.author + "<\/span>";
            strVar += "				   <div class=\"yt-lockup-meta stat\">" + video.stats + "<\/div>";
            strVar += "            <\/a>";
            strVar += "        <\/div>";
            strVar += "        <div class=\"thumb-wrapper\">";
            strVar += "	           <a href=\"\/watch?v=" + video.id + "\" class=\"yt-uix-sessionlink thumb-link spf-link spf-link\" data-sessionlink=\"" + video.session_data + "\" rel=\"spf-prefetch\" tabindex=\"-1\" aria-hidden=\"true\">";
            strVar += "                <span class=\"yt-uix-simple-thumb-wrap yt-uix-simple-thumb-related\" tabindex=\"0\" data-vid=\"" + video.id + "\">";
            strVar += "                    <img aria-hidden=\"true\" alt=\"\" src=\"" + video.iurlhq + "\">";
            strVar += "                <\/span>";
            strVar += "            <\/a>";
            strVar += "	           <span class=\"video-time\">"+ video.time +"<\/span>";
            strVar += "            <button class=\"yt-uix-button yt-uix-button-size-small yt-uix-button-default yt-uix-button-empty yt-uix-button-has-icon no-icon-markup addto-button video-actions spf-nolink hide-until-delayloaded addto-watch-later-button yt-uix-tooltip\" type=\"button\" onclick=\";return false;\" title=\"Watch Later\" role=\"button\" data-video-ids=\"" + video.id + "\" data-tooltip-text=\"Watch Later\"><\/button>";
            strVar += "        <\/div>";
            strVar += "    <\/div>";
            strVar += "<\/li>";

            video.html = strVar;
            return video;
        }

        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() {
            var css;

            if (script.modern) {
                css = `
.autocomplete-suggestions {
text-align: left; cursor: default; border: 1px solid var(--ytd-searchbox-legacy-border-color); border-top: 0; background: var(--yt-searchbox-background);
position: absolute; display: none; z-index: 9999; max-height: 254px; overflow: hidden; overflow-y: auto; box-sizing: border-box; box-shadow: -1px 1px 3px rgba(0,0,0,.1);
}
.autocomplete-suggestion { position: relative; padding: 0 .6em; line-height: 23px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 1.22em; color: var(--yt-placeholder-text); }
.autocomplete-suggestion b { font-weight: normal; color: #b31217; }
.autocomplete-suggestion.selected { background: #ddd; }
[dark] .autocomplete-suggestion.selected { background: #333; }

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

#suggestions-search {
outline: none; width: 100%; padding: 6px 5px; margin: 8px 0 0 0;
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(--yt-searchbox-text-color); background-color: var(--yt-searchbox-background);
}
#suggestions-search.playlist-or-live { margin-bottom: 16px; }
`;
            } else {
                css = `
.autocomplete-suggestions {
text-align: left; cursor: default; border: 1px solid #ccc; border-top: 0; background: #fff; box-shadow: -1px 1px 3px rgba(0,0,0,.1);
position: absolute; display: none; z-index: 9999; max-height: 254px; overflow: hidden; overflow-y: auto; box-sizing: border-box;
}
.autocomplete-suggestion { position: relative; padding: 0 .6em; line-height: 23px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 1.02em; color: #333; }
.autocomplete-suggestion b { font-weight: normal; color: #b31217; }
.autocomplete-suggestion.selected { background: #f0f0f0; }

.yt-uix-simple-thumb-wrap > img { top: 0px; width: 168px; height: 94px; }
.watch-sidebar-body > div.search-pager { width: 97.5%; padding: 5px 5px; display: flex; justify-content: center; }
.watch-sidebar-body > div.search-pager > .yt-uix-button { margin: 0 1px; }

#suggestions-search { outline: none; width: 98%; padding: 5px 5px; margin: 0 4px; }
#suggestions-search.playlist-or-live { width: 97%; margin: 0 10px 10px 10px; }
`;
            }

            var 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 (var i = 0; i < array.length; i++) {
                callback.call(scope, array[i], i);
            }
        }

        // When you want to remove elements
        function forEachReverse(array, callback, scope) {
            for (var i = array.length - 1; i >= 0; i--) {
                callback.call(scope, array[i], i);
            }
        }
    }

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

    var autoCompleteScript = document.createElement('script');
    autoCompleteScript.appendChild(document.createTextNode('window.autoComplete = ' + autoComplete + ';'));
    (document.body || document.head || document.documentElement).appendChild(autoCompleteScript);

    var queueScriptClassic = document.createElement('script');
    queueScriptClassic.appendChild(document.createTextNode('('+ youtube_play_next_queue_classic +')();'));
    (document.body || document.head || document.documentElement).appendChild(queueScriptClassic);

    var queueScriptModern = document.createElement('script');
    queueScriptModern.appendChild(document.createTextNode('('+ youtube_play_next_queue_modern +')();'));
    (document.body || document.head || document.documentElement).appendChild(queueScriptModern);

    var searchScript = document.createElement('script');
    searchScript.appendChild(document.createTextNode('('+ youtube_search_while_watching_video +')();'));
    (document.body || document.head || document.documentElement).appendChild(searchScript);
})();