diff --git a/goodtube.js b/goodtube.js
new file mode 100644
index 0000000..c25247e
--- /dev/null
+++ b/goodtube.js
@@ -0,0 +1,2680 @@
+(function () {
+ 'use strict';
+
+
+ /* Bypass CSP restrictions (introduced by the latest Chrome updates)
+ ------------------------------------------------------------------------------------------ */
+ if (window.trustedTypes && window.trustedTypes.createPolicy && !window.trustedTypes.defaultPolicy) {
+ window.trustedTypes.createPolicy('default', {
+ createHTML: string => string,
+ createScriptURL: string => string,
+ createScript: string => string
+ });
+ }
+
+
+ /* Helper functions
+ ------------------------------------------------------------------------------------------ */
+ // Setup GET parameters
+ function goodTube_helper_setupGetParams() {
+ let getParams = {};
+
+ document.location.search.replace(/\??(?:([^=]+)=([^&]*)&?)/g, function () {
+ function decode(s) {
+ return decodeURIComponent(s.split("+").join(" "));
+ }
+
+ getParams[decode(arguments[1])] = decode(arguments[2]);
+ });
+
+ // If we're on a playlist, but we don't have a video id in the URL - then get it from the page api
+ if (typeof getParams['list'] !== 'undefined' && typeof getParams['v'] === 'undefined') {
+ if (goodTube_page_api && typeof goodTube_page_api.getVideoData === 'function') {
+ let videoData = goodTube_page_api.getVideoData();
+
+ if (typeof videoData['video_id'] !== 'undefined' && videoData['video_id']) {
+ getParams['v'] = videoData['video_id'];
+ }
+ }
+ }
+
+ return getParams;
+ }
+
+ // Set a cookie
+ function goodTube_helper_setCookie(name, value) {
+ // 399 days
+ document.cookie = name + "=" + encodeURIComponent(value) + ";max-age=" + (399 * 24 * 60 * 60);
+ }
+
+ // Get a cookie
+ function goodTube_helper_getCookie(name) {
+ // Split the cookie string and get all individual name=value pairs in an array
+ let cookies = document.cookie.split(";");
+
+ // Loop through the array elements
+ for (let i = 0; i < cookies.length; i++) {
+ let cookie = cookies[i].split("=");
+
+ // Removing whitespace at the beginning of the cookie name and compare it with the given string
+ if (name == cookie[0].trim()) {
+ // Decode the cookie value and return
+ return decodeURIComponent(cookie[1]);
+ }
+ }
+
+ // Return null if not found
+ return null;
+ }
+
+ // Add CSS classes to show or hide elements / the Youtube player
+ function goodTube_helper_showHide_init() {
+ let style = document.createElement('style');
+ style.textContent = `
+ .goodTube_hidden {
+ position: fixed !important;
+ top: -9999px !important;
+ left: -9999px !important;
+ transform: scale(0) !important;
+ pointer-events: none !important;
+ }
+
+ .goodTube_hiddenPlayer {
+ position: relative;
+ overflow: hidden;
+ z-index: 1;
+ }
+
+ .goodTube_hiddenPlayer::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: #ffffff;
+ z-index: 998;
+ }
+
+ html[dark] .goodTube_hiddenPlayer::before {
+ background: #0f0f0f;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+
+ // Hide an element
+ function goodTube_helper_hideElement(element) {
+ if (element && !element.classList.contains('goodTube_hidden')) {
+ element.classList.add('goodTube_hidden');
+ }
+ }
+
+ // Show an element
+ function goodTube_helper_showElement(element) {
+ if (element && element.classList.contains('goodTube_hidden')) {
+ element.classList.remove('goodTube_hidden');
+ }
+ }
+
+ // Hide the Youtube player
+ function goodTube_helper_hideYoutubePlayer(element) {
+ // Add a wrapping div to help avoid detection
+ if (!element.closest('.goodTube_hiddenPlayer')) {
+ let parent = element.parentNode;
+ let wrapper = document.createElement('div');
+ wrapper.classList.add('goodTube_hiddenPlayer');
+ parent.replaceChild(wrapper, element);
+ wrapper.appendChild(element);
+ }
+ }
+
+
+ /* Global variables
+ ------------------------------------------------------------------------------------------ */
+ // Stores the GET params
+ let goodTube_getParams = goodTube_helper_setupGetParams();
+
+ // Are we on mobile?
+ let goodTube_mobile = false;
+ if (window.location.href.indexOf('m.youtube') !== -1 || (typeof goodTube_getParams['mobile'] !== 'undefined' && goodTube_getParams['mobile'] === 'true')) {
+ goodTube_mobile = true;
+ }
+
+ // A reference to our player's wrapper
+ let goodTube_playerWrapper = false;
+
+ // A reference to our player's iframe
+ let goodTube_player = false;
+
+ // The page api
+ let goodTube_page_api = false;
+
+ // The iframe api
+ let goodTube_iframe_api = false;
+
+ // Are we in picture in picture?
+ let goodTube_pip = false;
+
+ // Are shorts enabled
+ let goodTube_shorts = 'false';
+ if (window.top === window.self) {
+ goodTube_shorts = goodTube_helper_getCookie('goodTube_shorts');
+
+ if (!goodTube_shorts) {
+ goodTube_helper_setCookie('goodTube_shorts', 'false');
+ }
+ }
+
+ // Is autoplay turned on?
+ let goodTube_autoplay = goodTube_helper_getCookie('goodTube_autoplay');
+ if (window.top === window.self) {
+ if (!goodTube_autoplay) {
+ goodTube_helper_setCookie('goodTube_autoplay', 'true');
+ goodTube_autoplay = 'true';
+ }
+ }
+
+
+ /* Youtube functions
+ ------------------------------------------------------------------------------------------ */
+ // Hide ads, shorts, etc using CSS
+ function goodTube_youtube_hideAdsShortsEtc() {
+ let style = document.createElement('style');
+ style.textContent = `
+ .ytd-search ytd-shelf-renderer,
+ ytd-reel-shelf-renderer,
+ ytd-merch-shelf-renderer,
+ ytd-action-companion-ad-renderer,
+ ytd-display-ad-renderer,
+
+ ytd-video-masthead-ad-advertiser-info-renderer,
+ ytd-video-masthead-ad-primary-video-renderer,
+ ytd-in-feed-ad-layout-renderer,
+ ytd-ad-slot-renderer,
+ ytd-statement-banner-renderer,
+ ytd-banner-promo-renderer-background
+ ytd-ad-slot-renderer,
+ ytd-in-feed-ad-layout-renderer,
+ ytd-engagement-panel-section-list-renderer:not(.ytd-popup-container):not([target-id='engagement-panel-clip-create']),
+ ytd-compact-video-renderer:has(.goodTube_hidden),
+ ytd-rich-item-renderer:has(> #content > ytd-ad-slot-renderer)
+ .ytd-video-masthead-ad-v3-renderer,
+ div#root.style-scope.ytd-display-ad-renderer.yt-simple-endpoint,
+ div#sparkles-container.style-scope.ytd-promoted-sparkles-web-renderer,
+ div#main-container.style-scope.ytd-promoted-video-renderer,
+ div#player-ads.style-scope.ytd-watch-flexy,
+ #clarify-box,
+
+ ytm-rich-shelf-renderer,
+ ytm-search ytm-shelf-renderer,
+ ytm-button-renderer.icon-avatar_logged_out,
+ ytm-companion-slot,
+ ytm-reel-shelf-renderer,
+ ytm-merch-shelf-renderer,
+ ytm-action-companion-ad-renderer,
+ ytm-display-ad-renderer,
+ ytm-rich-section-renderer,
+ ytm-video-masthead-ad-advertiser-info-renderer,
+ ytm-video-masthead-ad-primary-video-renderer,
+ ytm-in-feed-ad-layout-renderer,
+ ytm-ad-slot-renderer,
+ ytm-statement-banner-renderer,
+ ytm-banner-promo-renderer-background
+ ytm-ad-slot-renderer,
+ ytm-in-feed-ad-layout-renderer,
+ ytm-compact-video-renderer:has(.goodTube_hidden),
+ ytm-rich-item-renderer:has(> #content > ytm-ad-slot-renderer)
+ .ytm-video-masthead-ad-v3-renderer,
+ div#root.style-scope.ytm-display-ad-renderer.yt-simple-endpoint,
+ div#sparkles-container.style-scope.ytm-promoted-sparkles-web-renderer,
+ div#main-container.style-scope.ytm-promoted-video-renderer,
+ div#player-ads.style-scope.ytm-watch-flexy,
+ ytd-compact-movie-renderer,
+
+ yt-about-this-ad-renderer,
+ masthead-ad,
+ ad-slot-renderer,
+ yt-mealbar-promo-renderer,
+ statement-banner-style-type-compact,
+ ytm-promoted-sparkles-web-renderer,
+ tp-yt-iron-overlay-backdrop,
+ #masthead-ad
+ {
+ display: none !important;
+ }
+
+ .style-scope[page-subtype='channels'] ytd-shelf-renderer,
+ .style-scope[page-subtype='channels'] ytm-shelf-renderer {
+ display: block !important;
+ }
+ `;
+ document.head.appendChild(style);
+
+ // Hide shorts if they're not enabled
+ if (goodTube_shorts === 'false') {
+ let shortsStyle = document.createElement('style');
+ shortsStyle.textContent = `
+ ytm-pivot-bar-item-renderer:has(> .pivot-shorts),
+ ytd-rich-section-renderer {
+ display: none !important;
+ }
+ `;
+ document.head.appendChild(shortsStyle);
+ }
+
+ // Debug message
+ console.log('[GoodTube] Ads removed');
+ }
+
+ // Hide shorts (realtime)
+ function goodTube_youtube_hideShorts() {
+ // Don't do this if shorts are enabled
+ if (goodTube_shorts === 'true') {
+ return;
+ }
+
+ // If we're on a channel page, don't hide shorts
+ if (window.location.href.indexOf('@') !== -1) {
+ return;
+ }
+
+ // Hide shorts links
+ let shortsLinks = document.querySelectorAll('a:not(.goodTube_hidden)');
+ shortsLinks.forEach((element) => {
+ if (element.href.indexOf('shorts/') !== -1) {
+ goodTube_helper_hideElement(element);
+ goodTube_helper_hideElement(element.closest('ytd-video-renderer'));
+ goodTube_helper_hideElement(element.closest('ytd-compact-video-renderer'));
+ goodTube_helper_hideElement(element.closest('ytd-rich-grid-media'));
+ }
+ });
+ }
+
+ // Support timestamp links in comments
+ function goodTube_youtube_timestampLinks() {
+ // Links in video description and comments
+ let timestampLinks = document.querySelectorAll('#description a, ytd-comments .yt-core-attributed-string a, ytm-expandable-video-description-body-renderer a, .comment-content a');
+
+ // For each link
+ timestampLinks.forEach((element) => {
+ // Make sure we've not touched it yet, this stops doubling up on event listeners
+ if (!element.classList.contains('goodTube_timestampLink') && element.getAttribute('href') && element.getAttribute('href').indexOf(goodTube_getParams['v']) !== -1 && element.getAttribute('href').indexOf('t=') !== -1) {
+ element.classList.add('goodTube_timestampLink');
+
+ // Add the event listener to send our player to the correct time
+ element.addEventListener('click', function () {
+ let bits = element.getAttribute('href').split('t=');
+ if (typeof bits[1] !== 'undefined') {
+ let time = bits[1].replace('s', '');
+ goodTube_player_skipTo(time);
+ }
+ });
+ }
+ });
+ }
+
+ // Hide all Youtube players
+ let goodTube_redirectHappened = false;
+ function goodTube_youtube_hidePlayers() {
+ // Don't do this if shorts are enabled
+ if (goodTube_shorts === 'true' && window.location.href.indexOf('/shorts') !== -1) {
+ return;
+ }
+
+ if (window.location.href.indexOf('/shorts') !== -1 && !goodTube_redirectHappened) {
+ window.location.href = 'https://youtube.com';
+ goodTube_redirectHappened = true;
+ }
+
+ // Hide the normal Youtube player
+ let regularPlayers = document.querySelectorAll('#player');
+ regularPlayers.forEach((element) => {
+ goodTube_helper_hideYoutubePlayer(element);
+ });
+
+ // Remove the full screen and theater Youtube player
+ let fullscreenPlayers = document.querySelectorAll('#full-bleed-container');
+ fullscreenPlayers.forEach((element) => {
+ goodTube_helper_hideYoutubePlayer(element);
+ });
+
+ // Hide the mobile controls
+ let mobileControls = document.querySelectorAll('#player-control-container');
+ mobileControls.forEach((element) => {
+ goodTube_helper_hideElement(element);
+ });
+
+ // Hide the Youtube miniplayer
+ let miniPlayers = document.querySelectorAll('ytd-miniplayer');
+ miniPlayers.forEach((element) => {
+ goodTube_helper_hideElement(element);
+ });
+ }
+
+ // Mute, pause and skip ads on all Youtube videos
+ function goodTube_youtube_mutePauseSkipAds() {
+ // Don't do this if shorts are enabled
+ if (goodTube_shorts === 'true' && window.location.href.indexOf('/shorts') !== -1) {
+ return;
+ }
+
+ // Pause and mute all HTML videos on the page
+ let youtubeVideos = document.querySelectorAll('video');
+ youtubeVideos.forEach((element) => {
+ // Don't touch the thumbnail hover player
+ if (!element.closest('#inline-player')) {
+ element.muted = true;
+ element.volume = 0;
+ element.pause();
+ }
+ });
+ }
+
+
+ /* Player functions
+ ------------------------------------------------------------------------------------------ */
+ // Init player
+ let goodTube_proxyIframeLoaded = false;
+ function goodTube_player_init() {
+ // Get the page API
+ goodTube_page_api = document.getElementById('movie_player');
+
+ // Get the video data to check loading state
+ let videoData = false;
+ if (goodTube_page_api && typeof goodTube_page_api.getVideoData === 'function') {
+ videoData = goodTube_page_api.getVideoData();
+ }
+
+ // Keep trying to get the frame API until it exists
+ if (!videoData) {
+ setTimeout(goodTube_player_init, 100);
+ return;
+ }
+
+ // Add CSS styles for the player
+ let style = document.createElement('style');
+ style.textContent = `
+ /* Desktop */
+ #goodTube_playerWrapper {
+ border-radius: 12px;
+ background: transparent;
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: 999;
+ overflow: hidden;
+ }
+
+ html[dark] #goodTube_playerWrapper {
+ background: #0f0f0f;
+ }
+
+ /* Mobile */
+ #goodTube_playerWrapper.goodTube_mobile {
+ position: fixed;
+ background: #000000;
+ border-radius: 0;
+ z-index: 3;
+ }
+
+ /* Theater mode */
+ #goodTube_playerWrapper.goodTube_theater {
+ background: #000000;
+ border-radius: 0;
+ }
+ `;
+ document.head.appendChild(style);
+
+ // Setup player layout
+ let playerWrapper = document.createElement('div');
+ playerWrapper.id = 'goodTube_playerWrapper';
+
+ // Add a mobile class
+ if (goodTube_mobile) {
+ playerWrapper.classList.add('goodTube_mobile');
+ }
+
+ // Add player to the page
+ document.body.appendChild(playerWrapper);
+
+ // Add video iframe embed (via proxy iframe)
+ playerWrapper.innerHTML = `
+
+ `;
+
+ // Expose the player and wrapper globally
+ goodTube_playerWrapper = document.querySelector('#goodTube_playerWrapper');
+ goodTube_player = goodTube_playerWrapper.querySelector('iframe');
+
+ // Setup player dynamic positioning and sizing
+ goodTube_player_positionAndSize();
+
+ // Run the actions
+ goodTube_actions();
+ }
+
+ // Position and size the player
+ let goodTube_loadTimeout = setTimeout(() => {}, 0);
+ function goodTube_player_positionAndSize() {
+ // If we're viewing a video
+ if (window.location.href.indexOf('.com/watch') !== -1) {
+ // Show the GoodTube player
+ goodTube_helper_showElement(goodTube_playerWrapper);
+
+ // This is used to position and size the player
+ let positionElement = false;
+
+ // Desktop
+ if (!goodTube_mobile) {
+ // Theater mode
+ if (document.querySelector('ytd-watch-flexy[theater]')) {
+ positionElement = document.getElementById('full-bleed-container');
+
+ if (!goodTube_playerWrapper.classList.contains('goodTube_theater')) {
+ goodTube_playerWrapper.classList.add('goodTube_theater');
+ }
+ }
+ // Regular mode
+ else {
+ positionElement = document.getElementById('player');
+
+ if (goodTube_playerWrapper.classList.contains('goodTube_theater')) {
+ goodTube_playerWrapper.classList.remove('goodTube_theater');
+ }
+ }
+
+ // Position the player
+ if (positionElement && positionElement.offsetHeight > 0) {
+ // Our wrapper has "position: absolute" so take into account the window scroll
+ let rect = positionElement.getBoundingClientRect();
+ goodTube_playerWrapper.style.top = (rect.top + window.scrollY) + 'px';
+ goodTube_playerWrapper.style.left = (rect.left + window.scrollX) + 'px';
+
+ // Match the size of the position element
+ goodTube_playerWrapper.style.width = positionElement.offsetWidth + 'px';
+ goodTube_playerWrapper.style.height = positionElement.offsetHeight + 'px';
+ }
+ }
+
+ // Mobile
+ else {
+ positionElement = document.getElementById('player');
+
+ // Position the player
+ if (positionElement && positionElement.offsetHeight > 0) {
+ // Our wrapper has "position: absolute" so don't take into account the window scroll
+ let rect = positionElement.getBoundingClientRect();
+ goodTube_playerWrapper.style.top = rect.top + 'px';
+ goodTube_playerWrapper.style.left = rect.left + 'px';
+
+ // Match the size of the position element
+ goodTube_playerWrapper.style.width = positionElement.offsetWidth + 'px';
+ goodTube_playerWrapper.style.height = positionElement.offsetHeight + 'px';
+ }
+ }
+ }
+
+ // Call this function again on next draw frame
+ window.requestAnimationFrame(function () {
+ goodTube_player_positionAndSize();
+ });
+ }
+
+ // Load a video
+ function goodTube_player_load() {
+ // Pause the video first (this helps to prevent audio flashes)
+ goodTube_player_pause();
+
+ // Make sure the proxy iframe has loaded
+ if (!goodTube_proxyIframeLoaded) {
+ clearTimeout(goodTube_loadTimeout);
+ goodTube_loadTimeout = setTimeout(goodTube_player_load, 100);
+ return;
+ }
+
+ // If we're not in picture in picture mode
+ if (!goodTube_pip) {
+ // Ensure we're still viewing a video (sometimes you can browse to another page before the iframe loads)
+ if (window.location.href.indexOf('.com/watch') !== -1) {
+ // If a restore time exists, skip to it
+ if (typeof goodTube_getParams['t'] !== 'undefined') {
+ goodTube_player_skipTo(goodTube_getParams['t'].replace('s', ''));
+ }
+ }
+ // If we're not still viewing a video
+ else {
+ // Clear and hide the player
+ goodTube_player_clear();
+ }
+
+
+ // Set the video source
+ // This also tells the embed if it's mobile or not
+ let mobileText = 'false';
+ if (goodTube_mobile) {
+ mobileText = 'true';
+ }
+ goodTube_player.contentWindow.postMessage('goodTube_src_https://www.youtube.com/embed/' + goodTube_getParams['v'] + '?autoplay=1&mobile=' + mobileText + '&goodTube_autoplay=' + goodTube_autoplay, '*');
+ }
+ // If we are in picture in picture mode
+ else {
+ // Load the video via the iframe api
+ goodTube_player.contentWindow.postMessage('goodTube_load_' + goodTube_getParams['v'], '*');
+ }
+
+
+ // Show the player
+ goodTube_helper_showElement(goodTube_playerWrapper);
+ }
+
+ // Clear and hide the player
+ function goodTube_player_clear() {
+ // Stop the video via the iframe api (but not if we're in picture in picture)
+ if (!goodTube_pip) {
+ goodTube_player.contentWindow.postMessage('goodTube_stopVideo', '*');
+ }
+
+ // Hide the player
+ goodTube_helper_hideElement(goodTube_playerWrapper);
+ }
+
+ // Skip to time
+ function goodTube_player_skipTo(time) {
+ goodTube_player.contentWindow.postMessage('goodTube_skipTo_' + time, '*');
+ }
+
+ // Pause
+ function goodTube_player_pause() {
+ goodTube_player.contentWindow.postMessage('goodTube_pause', '*');
+ }
+
+ // Play
+ function goodTube_player_play() {
+ goodTube_player.contentWindow.postMessage('goodTube_play', '*');
+ }
+
+
+ /* Keyboard shortcuts
+ ------------------------------------------------------------------------------------------ */
+ // Add keyboard shortcuts
+ function goodTube_shortcuts_init() {
+ document.addEventListener('keydown', function (event) {
+ // Don't do anything if we're holding control
+ if (event.ctrlKey) {
+ return;
+ }
+
+ // Get the key pressed in lower case
+ let keyPressed = event.key.toLowerCase();
+
+ // If we're not focused on a HTML form element
+ let focusedElement = event.srcElement;
+ let focusedElement_tag = false;
+ let focusedElement_id = false;
+ if (focusedElement) {
+ if (typeof focusedElement.nodeName !== 'undefined') {
+ focusedElement_tag = focusedElement.nodeName.toLowerCase();
+ }
+
+ if (typeof focusedElement.getAttribute !== 'undefined') {
+ focusedElement_id = focusedElement.getAttribute('id');
+ }
+ }
+
+ if (
+ !focusedElement ||
+ (
+ focusedElement_tag.indexOf('input') === -1 &&
+ focusedElement_tag.indexOf('label') === -1 &&
+ focusedElement_tag.indexOf('select') === -1 &&
+ focusedElement_tag.indexOf('textarea') === -1 &&
+ focusedElement_tag.indexOf('fieldset') === -1 &&
+ focusedElement_tag.indexOf('legend') === -1 &&
+ focusedElement_tag.indexOf('datalist') === -1 &&
+ focusedElement_tag.indexOf('output') === -1 &&
+ focusedElement_tag.indexOf('option') === -1 &&
+ focusedElement_tag.indexOf('optgroup') === -1 &&
+ focusedElement_id !== 'contenteditable-root'
+ )
+ ) {
+ if (
+ // Fullscreen
+ keyPressed === 'f' ||
+ // Speed up playback
+ keyPressed === '>' ||
+ // Slow down playback
+ keyPressed === '<'
+ ) {
+ event.preventDefault();
+ event.stopImmediatePropagation();
+
+ // Pass the keyboard shortcut to the iframe
+ goodTube_player.contentWindow.postMessage('goodTube_shortcut_' + keyPressed, '*');
+ }
+
+ // If we're not holding down the shift key
+ if (!event.shiftKey) {
+ // If we're focused on the video element
+ if (focusedElement && typeof focusedElement.closest !== 'undefined' && focusedElement.closest('#goodTube_player')) {
+ // Theater mode (focus the body, this makes the default youtube shortcut work)
+ if (keyPressed === 't') {
+ document.querySelector('body').focus();
+ }
+ }
+
+ if (
+ // Prev frame (24fps calculation)
+ keyPressed === ',' ||
+ // Next frame (24fps calculation)
+ keyPressed === '.' ||
+ // Prev 5 seconds
+ keyPressed === 'arrowleft' ||
+ // Next 5 seconds
+ keyPressed === 'arrowright' ||
+ // Toggle play/pause
+ keyPressed === ' ' || keyPressed === 'k' ||
+ // Toggle mute
+ keyPressed === 'm' ||
+ // Toggle fullscreen
+ keyPressed === 'f' ||
+ // Prev 10 seconds
+ keyPressed === 'j' ||
+ // Next 10 seconds
+ keyPressed === 'l' ||
+ // Start of video
+ keyPressed === 'home' ||
+ // End of video
+ keyPressed === 'end' ||
+ // Skip to percentage
+ keyPressed === '0' ||
+ keyPressed === '1' ||
+ keyPressed === '2' ||
+ keyPressed === '3' ||
+ keyPressed === '4' ||
+ keyPressed === '5' ||
+ keyPressed === '6' ||
+ keyPressed === '7' ||
+ keyPressed === '8' ||
+ keyPressed === '9'
+ ) {
+ event.preventDefault();
+ event.stopImmediatePropagation();
+
+ // Pass the keyboard shortcut to the iframe
+ goodTube_player.contentWindow.postMessage('goodTube_shortcut_' + keyPressed, '*');
+ }
+
+ // Toggle picture in picture
+ if (keyPressed === 'i') {
+ event.preventDefault();
+ event.stopImmediatePropagation();
+
+ // Tell the iframe to toggle pip
+ goodTube_player.contentWindow.postMessage('goodTube_pip', '*');
+ }
+ }
+ }
+ }, true);
+ }
+
+ // Trigger a keyboard shortcut
+ function goodTube_shortcuts_trigger(shortcut) {
+ // Focus the body first
+ document.querySelector('body').focus();
+
+ // Setup the keyboard shortcut
+ let theKey = false;
+ let keyCode = false;
+ let shiftKey = false;
+
+ if (shortcut === 'theater') {
+ theKey = 't';
+ keyCode = 84;
+ shiftKey = false;
+ }
+ else {
+ return;
+ }
+
+ // Trigger the keyboard shortcut
+ let e = false;
+ e = new window.KeyboardEvent('focus', {
+ bubbles: true,
+ key: theKey,
+ keyCode: keyCode,
+ shiftKey: shiftKey,
+ charCode: 0,
+ });
+ document.dispatchEvent(e);
+
+ e = new window.KeyboardEvent('keydown', {
+ bubbles: true,
+ key: theKey,
+ keyCode: keyCode,
+ shiftKey: shiftKey,
+ charCode: 0,
+ });
+ document.dispatchEvent(e);
+
+ e = new window.KeyboardEvent('beforeinput', {
+ bubbles: true,
+ key: theKey,
+ keyCode: keyCode,
+ shiftKey: shiftKey,
+ charCode: 0,
+ });
+ document.dispatchEvent(e);
+
+ e = new window.KeyboardEvent('keypress', {
+ bubbles: true,
+ key: theKey,
+ keyCode: keyCode,
+ shiftKey: shiftKey,
+ charCode: 0,
+ });
+ document.dispatchEvent(e);
+
+ e = new window.KeyboardEvent('input', {
+ bubbles: true,
+ key: theKey,
+ keyCode: keyCode,
+ shiftKey: shiftKey,
+ charCode: 0,
+ });
+ document.dispatchEvent(e);
+
+ e = new window.KeyboardEvent('change', {
+ bubbles: true,
+ key: theKey,
+ keyCode: keyCode,
+ shiftKey: shiftKey,
+ charCode: 0,
+ });
+ document.dispatchEvent(e);
+
+ e = new window.KeyboardEvent('keyup', {
+ bubbles: true,
+ key: theKey,
+ keyCode: keyCode,
+ shiftKey: shiftKey,
+ charCode: 0,
+ });
+ document.dispatchEvent(e);
+ }
+
+
+ /* Navigation (playlists and autoplay)
+ ------------------------------------------------------------------------------------------ */
+ // Have we opened the playlist (mobile)
+ let goodTube_nav_clickedPlaylistOpen = false;
+
+ // A reference to the previous video
+ let goodTube_nav_prevVideo = [];
+
+ // Are the next and previous buttons enabled?
+ let goodTube_nav_nextButton = true;
+ let goodTube_nav_prevButton = false;
+
+ // Generate playlist links (these are internally used to help us navigate through playlists and use autoplay)
+ function goodTube_nav_generatePlaylistLinks() {
+ // If we're not viewing a playlist, just return.
+ if (typeof goodTube_getParams['i'] === 'undefined' && typeof goodTube_getParams['index'] === 'undefined' && typeof goodTube_getParams['list'] === 'undefined') {
+ return;
+ }
+
+ // Get the playlist items
+ let playlistLinks = false;
+ let playlistTitles = false;
+
+ // Desktop
+ if (!goodTube_mobile) {
+ playlistLinks = document.querySelectorAll('#playlist-items > a');
+ playlistTitles = document.querySelectorAll('#playlist-items #video-title');
+ }
+ // Mobile
+ else {
+ playlistLinks = document.querySelectorAll('ytm-playlist-panel-renderer a.compact-media-item-image');
+ playlistTitles = document.querySelectorAll('ytm-playlist-panel-renderer .compact-media-item-headline span');
+ }
+
+ // If the playlist links exist
+ if (playlistLinks.length > 0) {
+ // Target the playlist container
+ let playlistContainer = document.getElementById('goodTube_playlistContainer');
+
+ // Add the playlist container if we don't have it
+ if (!playlistContainer) {
+ playlistContainer = document.createElement('div');
+ playlistContainer.setAttribute('id', 'goodTube_playlistContainer');
+ playlistContainer.style.display = 'none';
+ document.body.appendChild(playlistContainer);
+ }
+
+ // Empty the playlist container
+ playlistContainer.innerHTML = '';
+
+ // For each playlist item
+ let i = 0;
+ playlistLinks.forEach((playlistItem) => {
+ // Create a link element
+ let playlistItemElement = document.createElement('a');
+
+ // Set the href
+ playlistItemElement.href = playlistItem.href;
+
+ // Set the title
+ playlistItemElement.innerHTML = playlistTitles[i].innerHTML.trim();
+
+ // If we're currently on this item, set the selected class
+ if (playlistItem.href.indexOf('v=' + goodTube_getParams['v']) !== -1) {
+ playlistItemElement.classList.add('goodTube_selected');
+ }
+
+ // Add the item to the playlist container
+ playlistContainer.appendChild(playlistItemElement);
+
+ i++;
+ });
+ }
+ }
+
+ // Play the previous video
+ function goodTube_nav_prev() {
+ // Check if we clicked a playlist item
+ let clickedPlaylistItem = false;
+
+ // If we are viewing a playlist
+ if (typeof goodTube_getParams['i'] !== 'undefined' || typeof goodTube_getParams['index'] !== 'undefined' || typeof goodTube_getParams['list'] !== 'undefined') {
+ // Get the playlist items
+ let playlistItems = document.querySelectorAll('#goodTube_playlistContainer a');
+
+ // For each playlist item
+ let clickNext = false;
+
+ // Loop in reverse
+ for (let i = (playlistItems.length - 1); i >= 0; i--) {
+ let playlistItem = playlistItems[i];
+
+ if (clickNext) {
+ // Find the matching playlist item on the page and click it
+ let bits = playlistItem.href.split('/watch');
+ let findUrl = '/watch' + bits[1];
+
+ // Desktop
+ if (!goodTube_mobile) {
+ clickedPlaylistItem = true;
+ document.querySelector('#playlist-items > a[href="' + findUrl + '"]')?.click();
+ }
+ // Mobile
+ else {
+ clickedPlaylistItem = true;
+ document.querySelector('ytm-playlist-panel-renderer a.compact-media-item-image[href="' + findUrl + '"]')?.click();
+ }
+
+ if (clickedPlaylistItem) {
+ clickedPlaylistItem = true;
+
+ // Double check that the playlist is open, if not - open it.
+ let playlistContainer = document.querySelector('ytm-playlist-panel-renderer');
+ if (!playlistContainer) {
+ let openButton = document.querySelector('ytm-playlist-panel-entry-point');
+
+ if (openButton && !goodTube_nav_clickedPlaylistOpen) {
+ goodTube_nav_clickedPlaylistOpen = true;
+ openButton.click();
+ setTimeout(goodTube_nav_prev, 500);
+ }
+
+ return;
+ }
+
+ goodTube_nav_clickedPlaylistOpen = false;
+
+ // Click the matching playlist item
+ document.querySelector('ytm-playlist-panel-renderer a.compact-media-item-image[href="' + findUrl + '"]')?.click();
+ }
+ }
+
+ if (playlistItem.classList.contains('goodTube_selected')) {
+ clickNext = true;
+ }
+ else {
+ clickNext = false;
+ }
+ }
+ }
+
+ // If we didn't click a playlist item, play previous video (if it exists in our history)
+ if (!clickedPlaylistItem && goodTube_nav_prevVideo[goodTube_nav_prevVideo.length - 2] && goodTube_nav_prevVideo[goodTube_nav_prevVideo.length - 2] !== window.location.href) {
+ // Debug message
+ console.log('[GoodTube] Playing previous video...');
+
+ // Go back to the previous video
+ goodTube_helper_setCookie('goodTube_previous', 'true');
+ window.history.go(-1);
+ }
+ }
+
+ // Play the next video
+ function goodTube_nav_next(pressedButton = false) {
+ // Check if we clicked a playlist item
+ let clickedPlaylistItem = false;
+
+ // If we are viewing a playlist
+ if (typeof goodTube_getParams['i'] !== 'undefined' || typeof goodTube_getParams['index'] !== 'undefined' || typeof goodTube_getParams['list'] !== 'undefined') {
+ // Get the playlist items
+ let playlistItems = document.querySelectorAll('#goodTube_playlistContainer a');
+
+ // For each playlist item
+ let clickNext = false;
+
+ playlistItems.forEach((playlistItem) => {
+ if (clickNext) {
+ // Find the matching playlist item on the page and click it
+ let bits = playlistItem.href.split('/watch');
+ let findUrl = '/watch' + bits[1];
+
+ // Desktop
+ if (!goodTube_mobile) {
+ clickedPlaylistItem = true;
+ document.querySelector('#playlist-items > a[href="' + findUrl + '"]')?.click();
+ }
+ // Mobile
+ else {
+ clickedPlaylistItem = true;
+
+ // Double check that the playlist is open, if not - open it.
+ let playlistContainer = document.querySelector('ytm-playlist-panel-renderer');
+ if (!playlistContainer) {
+ let openButton = document.querySelector('ytm-playlist-panel-entry-point');
+
+ if (openButton && !goodTube_nav_clickedPlaylistOpen) {
+ goodTube_nav_clickedPlaylistOpen = true;
+ openButton.click();
+ setTimeout(goodTube_nav_next, 500);
+ }
+
+ return;
+ }
+
+ goodTube_nav_clickedPlaylistOpen = false;
+
+ // Click the matching playlist item
+ document.querySelector('ytm-playlist-panel-renderer a.compact-media-item-image[href="' + findUrl + '"]')?.click();
+ }
+
+ if (clickedPlaylistItem) {
+ // Debug message
+ console.log('[GoodTube] Playing next video in playlist...');
+ }
+ }
+
+ if (playlistItem.classList.contains('goodTube_selected')) {
+ clickNext = true;
+ }
+ else {
+ clickNext = false;
+ }
+ });
+ }
+
+ // If we didn't click a playlist item, autoplay next video (only if they pressed the next button or autoplay is on)
+ if (!clickedPlaylistItem && (goodTube_autoplay === 'true' || pressedButton)) {
+ // Re fetch the page API (this fixes issues on mobile)
+ goodTube_page_api = document.getElementById('movie_player');
+
+ // Make sure it exists
+ if (goodTube_page_api && typeof goodTube_page_api.nextVideo === 'function') {
+ // Play the next video
+ goodTube_page_api.nextVideo();
+ }
+
+ // Debug message
+ console.log('[GoodTube] Autoplaying next video...');
+ }
+ }
+
+ // Setup the previous button history
+ function goodTube_nav_setupPrevHistory() {
+ // If we've hit the previous button
+ if (goodTube_helper_getCookie('goodTube_previous') === 'true') {
+ // Remove the last item from the previous video array
+ goodTube_nav_prevVideo.pop();
+
+ goodTube_helper_setCookie('goodTube_previous', 'false');
+ }
+ // Otherwise it's a normal video load
+ else {
+ // Add this page to the previous video array
+ goodTube_nav_prevVideo.push(window.location.href);
+ }
+ }
+
+ // Show or hide the next and previous button
+ function goodTube_nav_showHideNextPrevButtons() {
+ let prevButton = false;
+ let nextButton = true;
+
+ // Don't show next / prev unless we're viewing a video
+ if (typeof goodTube_getParams['v'] === 'undefined') {
+ prevButton = false;
+ nextButton = false;
+ }
+ // For the regular player
+ else {
+ // If we're viewing a playlist
+ if (typeof goodTube_getParams['i'] !== 'undefined' || typeof goodTube_getParams['index'] !== 'undefined' || typeof goodTube_getParams['list'] !== 'undefined') {
+ let playlist = document.querySelectorAll('#goodTube_playlistContainer a');
+
+ if (!playlist || !playlist.length) {
+ return;
+ }
+
+ // If the first video is NOT selected
+ if (!playlist[0].classList.contains('goodTube_selected')) {
+ // Enable the previous button
+ prevButton = true;
+ }
+ }
+ // Otherwise we're not in a playlist, so if a previous video exists
+ else if (goodTube_nav_prevVideo[goodTube_nav_prevVideo.length - 2] && goodTube_nav_prevVideo[goodTube_nav_prevVideo.length - 2] !== window.location.href) {
+ // Enable the previous button
+ prevButton = true;
+ }
+ }
+
+ // Tell the iframe to show or hide the previous button
+ if (prevButton) {
+ goodTube_nav_prevButton = true;
+ goodTube_player.contentWindow.postMessage('goodTube_prevButton_show', '*');
+ }
+ else {
+ goodTube_nav_prevButton = false;
+ goodTube_player.contentWindow.postMessage('goodTube_prevButton_hide', '*');
+ }
+
+ // Tell the iframe to show or hide the next button
+ if (nextButton) {
+ goodTube_nav_nextButton = true;
+ goodTube_player.contentWindow.postMessage('goodTube_nextButton_show', '*');
+ }
+ else {
+ goodTube_nav_nextButton = false;
+ goodTube_player.contentWindow.postMessage('goodTube_nextButton_hide', '*');
+ }
+ }
+
+
+ /* Usage stats
+ ------------------------------------------------------------------------------------------ */
+ // Don't worry everyone - this is just a counter that totals unique users / how many videos were played with GoodTube.
+ // It's only in here so I can have some fun and see how many people use this thing I made - no private info is tracked.
+
+ // Count unique users
+ function goodTube_stats_user() {
+ // If there's no cookie
+ if (!goodTube_helper_getCookie('goodTube_uniqueUserStat')) {
+ // Count a unique user
+ fetch('\x68\x74\x74\x70\x73\x3a\x2f\x2f\x6a\x61\x6d\x65\x6e\x6c\x79\x6e\x64\x6f\x6e\x2e\x63\x6f\x6d\x2f\x5f\x6f\x74\x68\x65\x72\x2f\x73\x74\x61\x74\x73\x2f\x75\x73\x65\x72\x2e\x70\x68\x70');
+
+ // Set a cookie to only count unique users once
+ goodTube_helper_setCookie('goodTube_uniqueUserStat', 'true');
+ }
+ }
+
+ // Count videos
+ function goodTube_stats_video() {
+ fetch('\x68\x74\x74\x70\x73\x3a\x2f\x2f\x6a\x61\x6d\x65\x6e\x6c\x79\x6e\x64\x6f\x6e\x2e\x63\x6f\x6d\x2f\x5f\x6f\x74\x68\x65\x72\x2f\x73\x74\x61\x74\x73\x2f\x76\x69\x64\x65\x6f\x2e\x70\x68\x70');
+ }
+
+
+ /* Core functions
+ ------------------------------------------------------------------------------------------ */
+ // Init
+ function goodTube_init() {
+ /* Disable Youtube
+ -------------------------------------------------- */
+ // Mute, pause and skip ads
+ goodTube_youtube_mutePauseSkipAds();
+ setInterval(goodTube_youtube_mutePauseSkipAds, 1);
+
+ // Add CSS classes to hide elements (without Youtube knowing)
+ goodTube_helper_showHide_init();
+
+ // Hide the youtube players
+ goodTube_youtube_hidePlayers();
+ setInterval(goodTube_youtube_hidePlayers, 100);
+
+ // Add CSS to hide ads, shorts, etc
+ goodTube_youtube_hideAdsShortsEtc();
+
+ // Hide shorts that popup as you use the site (like video results)
+ setInterval(goodTube_youtube_hideShorts, 100);
+
+
+ /* Load GoodTube
+ -------------------------------------------------- */
+ // Init our player (after DOM is loaded)
+ document.addEventListener('DOMContentLoaded', goodTube_player_init);
+
+ // Also check if the DOM is already loaded, as if it is, the above event listener will not trigger
+ if (document.readyState === 'interactive' || document.readyState === 'complete') {
+ goodTube_player_init();
+ }
+
+ // Usage stats
+ goodTube_stats_user();
+
+ // Keyboard shortcuts (desktop only)
+ if (!goodTube_mobile) {
+ goodTube_shortcuts_init();
+ }
+
+ // Listen for messages from the iframe
+ window.addEventListener('message', goodTube_receiveMessage);
+
+ // Init the menu
+ document.addEventListener('DOMContentLoaded', goodTube_menu);
+
+ // Also check if the DOM is already loaded, as if it is, the above event listener will not trigger
+ if (document.readyState === 'interactive' || document.readyState === 'complete') {
+ goodTube_menu();
+ }
+ }
+
+ // Listen for messages from the iframe
+ function goodTube_receiveMessage(event) {
+ // Make sure some data exists
+ if (typeof event.data !== 'string') {
+ return;
+ }
+
+ // Proxy iframe has loaded
+ else if (event.data === 'goodTube_proxyIframe_loaded') {
+ goodTube_proxyIframeLoaded = true;
+ }
+
+ // Player iframe has loaded
+ else if (event.data === 'goodTube_playerIframe_loaded') {
+ goodTube_player.style.display = 'block';
+ }
+
+ // Picture in picture
+ if (event.data.indexOf('goodTube_pip_') !== -1) {
+ let pipEnabled = event.data.replace('goodTube_pip_', '');
+
+ if (pipEnabled === 'true') {
+ goodTube_pip = true;
+ }
+ else {
+ goodTube_pip = false;
+
+ // If we're not viewing a video
+ if (typeof goodTube_getParams['v'] === 'undefined') {
+ // Clear the player
+ goodTube_player_clear();
+ }
+ }
+ }
+
+ // Previous video
+ else if (event.data === 'goodTube_prevVideo') {
+ goodTube_nav_prev();
+ }
+
+ // Next video
+ else if (event.data === 'goodTube_nextVideo') {
+ goodTube_nav_next();
+ }
+
+ // Theater mode (toggle)
+ else if (event.data === 'goodTube_theater') {
+ goodTube_shortcuts_trigger('theater');
+ }
+
+ // Autoplay
+ else if (event.data === 'goodTube_autoplay_false') {
+ goodTube_helper_setCookie('goodTube_autoplay', 'false');
+ goodTube_autoplay = 'false';
+ }
+ else if (event.data === 'goodTube_autoplay_true') {
+ goodTube_helper_setCookie('goodTube_autoplay', 'true');
+ goodTube_autoplay = 'true';
+ }
+ }
+
+ // Actions
+ let goodTube_previousUrl = false;
+ function goodTube_actions() {
+ // Get the previous and current URL
+
+ // Remove hashes, these mess with things sometimes
+ // Also remove "index="
+ let previousUrl = goodTube_previousUrl;
+ if (previousUrl) {
+ previousUrl = previousUrl.split('#')[0];
+ previousUrl = previousUrl.split('index=')[0];
+ }
+
+ let currentUrl = window.location.href;
+ if (currentUrl) {
+ currentUrl = currentUrl.split('#')[0];
+ currentUrl = currentUrl.split('index=')[0];
+ }
+
+ // If the URL has changed (this will always fire on first page load)
+ if (previousUrl !== currentUrl) {
+ // The URL has changed, so setup our player
+ // ----------------------------------------------------------------------------------------------------
+ // Setup GET parameters
+ goodTube_getParams = goodTube_helper_setupGetParams();
+
+ // If we're viewing a video
+ if (window.location.href.indexOf('.com/watch') !== -1) {
+ // Setup the previous button history
+ goodTube_nav_setupPrevHistory();
+
+ // Load the video
+ goodTube_player_load();
+
+ // Usage stats
+ goodTube_stats_video();
+ }
+ // Otherwise if we're not viewing a video
+ else {
+ // Clear the player
+ goodTube_player_clear();
+ }
+
+ // Set the previous URL (which pauses this function until the URL changes again)
+ goodTube_previousUrl = window.location.href;
+ }
+
+ // Generate the playlist links (used to navigate playlists correctly)
+ goodTube_nav_generatePlaylistLinks();
+
+ // Show or hide the next / prev buttons
+ goodTube_nav_showHideNextPrevButtons();
+
+ // Support timestamp links
+ goodTube_youtube_timestampLinks();
+
+ // Run actions again in 100ms to loop this function
+ setTimeout(goodTube_actions, 100);
+ }
+
+ // Init menu
+ function goodTube_menu() {
+ // Create the menu container
+ let menuContainer = document.createElement('div');
+
+ // Add the menu container to the page
+ document.body.appendChild(menuContainer);
+
+ // Configure the settings to show their actual values
+ let shortsEnabled = ' checked';
+ if (goodTube_shorts === 'true') {
+ shortsEnabled = '';
+ }
+
+ // Add content to the menu container
+ menuContainer.innerHTML = `
+
+
+
+
+ ✖
+
+
+
+