Youtube - Search While Watching Video

Search YouTube without interrupting the video, by loading the search results in the related video bar

Устаревшая версия за 06.10.2019. Перейдите к последней версии.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

// ==UserScript==
// @name         Youtube - Search While Watching Video
// @version      2.1.2
// @description  Search YouTube without interrupting the video, by loading the search results in the related video bar
// @author       Cpt_mathix
// @match        https://www.youtube.com/*
// @license      GPL-2.0-or-later
// @require      https://cdnjs.cloudflare.com/ajax/libs/JavaScript-autoComplete/1.0.4/auto-complete.min.js
// @namespace    https://greatest.deepsurf.us/users/16080
// @run-at       document-start
// @grant        none
// @noframes
// ==/UserScript==

(function() {
    'use strict';

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

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

        var appliedYoutubePlayerReadyCalls = [];

        function shareYoutubePlayerReady(originalYoutubePlayerReady) {
            return function () {
                startScript();

                if (!originalYoutubePlayerReady in appliedYoutubePlayerReadyCalls) {
                    appliedYoutubePlayerReadyCalls.push(originalYoutubePlayerReady);

                    if (originalYoutubePlayerReady) {
                        return originalYoutubePlayerReady.apply(this, arguments);
                    }
                }
            };
        }

        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;

            window.onYouTubePlayerReady = shareYoutubePlayerReady(window.onYouTubePlayerReady);

            startScript();
        }

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

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

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