Cpt_mathix / Youtube Play Next Queue

// ==UserScript==
// @name         Youtube Play Next Queue
// @version      2.0.2
// @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 = {
            initialized: false,
            version: "2.0.0",

            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) #"); }
            if (script.initialized) {
                startScript(2);
            } else {
                initScript();
                startScript(5);
            }
        });

        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 (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() {
        var script = {
            initialized: false,
            version: "2.0.0",

            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 (/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) #"); }
            if (script.initialized) {
                startScript();
            } else {
                initScript();
                startScript();
            }
        });

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

        function startScript() {
            if (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 (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(var i = 0; i < urlVariables.length; i++) {
                var 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 = {
                    iurlmq: this.data.thumbnail.thumbnails[0].url,
                    length_seconds: hmsToSeconds(this.data.lengthText ?
                                                 this.data.lengthText.simpleText : this.data.thumbnailOverlays[0].thumbnailOverlayTimeStatusRenderer ?
                                                 this.data.thumbnailOverlays[0].thumbnailOverlayTimeStatusRenderer.text.simpleText : ""),
                    id: this.data.videoId,
                    iurlhq: this.data.thumbnail.thumbnails[0].url,
                    title: this.data.title.simpleText,
                    session_data: "itct=" + this.data.navigationEndpoint.clickTrackingParams,
                    aria_label: this.data.title.accessibility.accessibilityData.label,
                    author: this.data.shortBylineText.runs[0].text,
                    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;
            }

            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.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" } };
                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 && !suggestion.classList.contains("queue-item")) {
                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) {
                    item.clearBadges();
                    if (index === 0) {
                        item.setAsNextVideo();
                        item.addBadge("Play Now", ["QUEUE_BUTTON", "QUEUE_PLAY_NOW"]);
                        item.addBadge("Remove", ["QUEUE_BUTTON", "QUEUE_REMOVE"]);
                    } else {
                        item.addBadge("Play Next", ["QUEUE_BUTTON", "QUEUE_PLAY_NEXT"]);
                        item.addBadge("Remove", ["QUEUE_BUTTON", "QUEUE_REMOVE"]);
                    }
                    window.Polymer.dom(queueContents).appendChild(item.toNode(["queue-item"]));
                });

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

        // The "remove queue and all its videos" button
        function initRemoveQueueButton(anchor) {
            var 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(videoOverlays) {
            // 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"
                        }
                    }
                }
            };

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

            var 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; right: auto; left: 0; }
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; }
`;
            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 = {
            loaded: false,
            ytplayer: null,
            modern: false,
            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 (http://youtube.github.io/js/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("### Normal youtube loaded ###"); }
                script.modern = false;
            } else {
                if (script.debug) { console.log("### Material youtube loaded ###"); }
                script.modern = true;
            }

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

            script.loaded = true;

            startScript(5);
        }

        function startScript(retry) {
            if (script.loaded && 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(--this.retry);
                    }.bind({retry: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 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-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);
})();