// ==UserScript== // @name Youtube - Search While Watching Video // @version 2.1.0 // @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://greasyfork.org/users/16080 // @run-at document-start // @grant none // @noframes // @downloadURL none // ==/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(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 = ""; anchor.insertAdjacentHTML("afterend", html); } else { // playlist or live video? anchor = document.querySelector('#related > ytd-watch-next-secondary-results-renderer'); if (anchor) { html = ""; anchor.insertAdjacentHTML("beforebegin", html); } } } else { anchor = document.querySelector('#watch7-sidebar-modules > div:nth-child(2)'); if (anchor) { html = ""; anchor.insertAdjacentHTML("afterbegin", html); } else { // playlist or live video? anchor = document.querySelector('#watch7-sidebar-modules'); if (anchor) { html = ""; 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", "
"); // 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 += "
  • "; strVar += "
    "; strVar += "
    "; strVar += " "; strVar += " " + video.title + "<\/span>"; strVar += " " + video.author + "<\/span>"; strVar += "
    " + video.stats + "<\/div>"; strVar += " <\/a>"; strVar += " <\/div>"; strVar += "
    "; strVar += " "; strVar += " "; strVar += " \"\""; strVar += " <\/span>"; strVar += " <\/a>"; strVar += " "+ video.time +"<\/span>"; strVar += "