Raw Source
nascent / Youtube Like/Dislike and Skip Ad Keyboard Shortcuts

// ==UserScript==
// @name         Youtube Like/Dislike and Skip Ad Keyboard Shortcuts
// @namespace    youtube
// @include      https://www.youtube.com/*
// @description  Keybindings: "[" to like, "]" to dislike, and "S" to skip pre-video or banner ads. Browser notifications can be toggled via the menu.
// @version      1.5
// @grant        GM_notification
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @run-at       document-end
// @icon         https://www.google.com/s2/favicons?domain=youtube.com&sz=128
// @updateURL    https://openuserjs.org/meta/nascent/Youtube_likedislike_video_and_skip_ad_keyboard_shortcuts.meta.js
// @license      GPL-3.0-or-later
// ==/UserScript==

/*jshint esversion: 6 */

(function () {
  "use strict";

  // Retrieve the saved setting; if not set, default to true.
  let browserNotifications = GM_getValue("browserNotifications", true);

  // Menu command for toggling browser notifications.
  function toggleBrowserNotifications() {
    browserNotifications = !browserNotifications;
    GM_setValue("browserNotifications", browserNotifications);
    alert(`Browser notifications are now ${browserNotifications ? "enabled" : "disabled"}.`);
  }
  GM_registerMenuCommand("Toggle Browser Notifications", toggleBrowserNotifications);

  let onVideoPage = false,
    skipAd = null,
    skipBannerAd = null,
    like = null,
    dislike = null;

  function findButtons() {
    // Only run on video watch pages.
    if (!/^\/watch/.test(location.pathname)) {
      onVideoPage = false;
      return;
    }
    onVideoPage = true;

    // Find the skip ad buttons.
    skipAd = document.querySelector(".ytp-ad-skip-button");
    skipBannerAd = document.querySelector(".ytp-ad-overlay-close-button");

    // Find the Like/Dislike buttons by custom element tags.
    like = document.querySelector("like-button-view-model button");
    dislike = document.querySelector("dislike-button-view-model button");

    // Fallback: Use attribute selectors if necessary.
    if (!like) {
      like = document.querySelector('button[aria-label*="like this video"]');
    }
    if (!dislike) {
      dislike = document.querySelector('button[aria-label^="Dislike this video"]');
    }
  }

  // Initial search.
  findButtons();

  // Watch for DOM changes (AJAX navigation on YouTube).
  const observer = new MutationObserver(findButtons);
  observer.observe(document.documentElement, { childList: true, subtree: true });

  // Listen for keydown events.
  window.addEventListener("keydown", (e) => {
    if (!onVideoPage) {
      return;
    }

    // Avoid interfering with input fields or editable content.
    const tag = e.target.tagName.toLowerCase();
    if (e.target.isContentEditable || tag === "input" || tag === "textarea") {
      return;
    }

    // "[" key to toggle like.
    if (e.code === "BracketLeft" && like) {
      like.click();
      if (browserNotifications && typeof GM_notification !== "undefined") {
        const action =
          like.getAttribute("aria-pressed") === "true"
            ? "Video Liked"
            : "Like Removed";
        GM_notification({ title: "YouTube", text: action, silent: true, timeout: 2500 });
      }
    }
    // "]" key to toggle dislike.
    else if (e.code === "BracketRight" && dislike) {
      dislike.click();
      if (browserNotifications && typeof GM_notification !== "undefined") {
        const action =
          dislike.getAttribute("aria-pressed") === "true"
            ? "Video Disliked"
            : "Dislike Removed";
        GM_notification({ title: "YouTube", text: action, silent: true, timeout: 2500 });
      }
    }
    // "S" key to skip ads.
    else if (e.code === "KeyS") {
      if (skipAd) {
        skipAd.click();
      } else if (skipBannerAd) {
        skipBannerAd.click();
      }
    }
  });
})();