YouTube Auto High Quality

Auto select the highest quality on YouTube (incl. Premium, if applicable)

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         YouTube Auto High Quality
// @namespace    https://www.youtube.com
// @license      GPL-3.0
// @version      1.0.0
// @description  Auto select the highest quality on YouTube (incl. Premium, if applicable)
// @author       CJMAXiK
// @license      GPL-3.0
// @match        https://*.youtube.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @website      https://gist.github.com/cjmaxik/889bdc983f95d1b589464a655e0ce5bf
// @grant        none
// ==/UserScript==

// Partially ported from the Chrome extention by Avi
// Code: https://github.com/avi12/youtube-auto-hd
// Avi: https://avi12.com/

const SELECTORS = {
  title: "title",
  video: "video",
  buttonSettings: ".ytp-settings-button",
  sizeToggle: ".ytp-size-button#original-size, .ytp-size-button",
  optionQuality: ".ytp-settings-menu[data-layer] .ytp-menuitem:last-child",
  menuOption: ".ytp-settings-menu[data-layer] .ytp-menuitem",
  menuOptionContent: ".ytp-menuitem-content",
  panelHeaderBack: ".ytp-panel-header button",
  player: ".html5-video-player:not(#inline-preview-player)",
  donationInjectParent: "ytd-comments",
  // Premium
  labelPremium: ".ytp-premium-label",
};

const OBSERVER_OPTIONS = {
  childList: true,
  subtree: true,
};

const SUFFIX_EBR = "ebr";

let qualityChanged = false;

function isElementVisible(element) {
  return element?.offsetWidth > 0 && element?.offsetHeight > 0;
}

async function getCurrentQualityElements() {
  const elPlayer = await waitElement(SELECTORS.player);
  const elMenuOptions = [...elPlayer.querySelectorAll(SELECTORS.menuOption)];
  return elMenuOptions.filter(getIsQualityElement);
}

function convertQualityToNumber(elQuality) {
  const isPremiumQuality = Boolean(
    elQuality.querySelector(SELECTORS.labelPremium)
  );
  const qualityNumber = parseInt(elQuality.textContent.substring(0, 4));

  if (isPremiumQuality) {
    return qualityNumber + SUFFIX_EBR;
  }

  return qualityNumber;
}

async function getAvailableQualities() {
  const elQualities = await getCurrentQualityElements();
  const qualities = elQualities.map(convertQualityToNumber);
  return qualities;
}

function getPlayerDiv(elVideo) {
  const elPlayer = elVideo.closest(SELECTORS.player);
  if (!elPlayer) {
    console.warn(
      "Player div not found. Is the video element correct?",
      elVideo,
      elVideo.parentElement
    );
  }
  return elPlayer;
}

function getIsQualityElement(element) {
  const isQuality = Boolean(element.textContent.match(/\d/));
  const isHasChildren = element.children.length > 1;
  return isQuality && !isHasChildren;
}

async function getIsSettingsMenuOpen() {
  const elButtonSettings = await waitElement(SELECTORS.buttonSettings);
  return elButtonSettings?.ariaExpanded === "true";
}

function getIsLastOptionQuality(elVideo) {
  const elOptionInSettings = getPlayerDiv(elVideo).querySelector(
    SELECTORS.optionQuality
  );

  if (!elOptionInSettings) {
    return false;
  }

  const elQualityName = elOptionInSettings.querySelector(
    SELECTORS.menuOptionContent
  );

  // If the video is a channel trailer, the last option is initially the speed one,
  // and the speed setting can only be a single digit
  const matchNumber = elQualityName?.textContent?.match(/\d+/);

  if (!matchNumber) {
    return false;
  }

  const numberString = matchNumber[0];
  const minQualityCharLength = 3; // e.g. 3 characters in 720p

  return numberString.length >= minQualityCharLength;
}

async function changeQualityAndClose(elVideo, elPlayer) {
  await changeQualityWhenPossible(elVideo);
  await closeMenu(elPlayer);

  qualityChanged = true;
}

async function changeQualityWhenPossible(elVideo) {
  console.log("Trying...");
  openQualityMenu(elVideo);
  await changeQuality();
}

async function changeQuality() {
  const elQualities = await getCurrentQualityElements();
  const qualitiesAvailable = await getAvailableQualities();
  const applyQuality = (iQuality) => {
    const quality = qualitiesAvailable[iQuality];
    console.log(`Setting up the ${quality}`);
    elQualities[iQuality]?.click();
  };

  const isQualityPreferredEBR = qualitiesAvailable[0]
    .toString()
    .endsWith(SUFFIX_EBR);
  if (isQualityPreferredEBR) {
    applyQuality(0);
    return;
  }

  const iQualityFallback = qualitiesAvailable.findIndex(
    (quality) => !quality.toString().endsWith(SUFFIX_EBR)
  );
  applyQuality(iQualityFallback);
}

async function closeMenu(elPlayer) {
  const clickPanelBackIfPossible = () => {
    const elPanelHeaderBack = elPlayer.querySelector(SELECTORS.panelHeaderBack);
    if (elPanelHeaderBack) {
      elPanelHeaderBack.click();
      return true;
    }
    return false;
  };

  if (clickPanelBackIfPossible()) {
    return;
  }

  const observer = new MutationObserver((_, observer) => {
    if (clickPanelBackIfPossible()) {
      observer.disconnect();
    }
  });
  observer.observe(elPlayer, OBSERVER_OPTIONS);
}

async function openQualityMenu(elVideo) {
  const elSettingQuality = getPlayerDiv(elVideo).querySelector(
    SELECTORS.optionQuality
  );
  elSettingQuality.click();
}

function waitElement(selector) {
  return new Promise((resolve) => {
    let element = [...document.querySelectorAll(selector)].find(
      isElementVisible
    );

    if (element) {
      return resolve(element);
    }

    const observer = new MutationObserver((mutations) => {
      let element = [...document.querySelectorAll(selector)].find(
        isElementVisible
      );

      if (element) {
        observer.disconnect();
        resolve(element);
      }
    });

    observer.observe(document.body, OBSERVER_OPTIONS);
  });
}

async function setEventListeners(event) {
  if (qualityChanged) return;

  const elVideo = document.querySelector(SELECTORS.video);
  if (!elVideo) {
    console.error("Auto HD: Video element was not found.");
    return;
  }

  const elPlayer = getPlayerDiv(elVideo);
  if (!elPlayer) {
    console.error("Auto HD: Player div was not found.");
    return;
  }

  const elSettings = elPlayer.querySelector(SELECTORS.buttonSettings);
  if (!elSettings) {
    console.error("Auto HD: Settings button was not found.");
    return;
  }

  const isSettingsMenuOpen = await getIsSettingsMenuOpen();
  if (!isSettingsMenuOpen) {
    elSettings.click();
  }
  elSettings.click();

  await changeQualityAndClose(elVideo, elPlayer);
  elPlayer.querySelector(SELECTORS.buttonSettings).blur();
}

(function () {
  "use strict";
  window.addEventListener(
    "yt-navigate-start",
    () => (qualityChanged = false),
    true
  );
  window.addEventListener("yt-navigate-finish", setEventListeners, true);

  setEventListeners();
})();

// document.addEventListener("yt-navigate-start", function () {
//   console.log("document.yt-navigate-start", arguments);
// });
// document.addEventListener("yt-navigate-finish", function () {
//   console.log("document.yt-navigate-finish", arguments);
// });
// document.addEventListener("yt-navigate-error", function () {
//   console.log("document.yt-navigate-error", arguments);
// });
// document.addEventListener("yt-navigate-redirect", function () {
//   console.log("document.yt-navigate-redirect", arguments);
// });
// document.addEventListener("yt-navigate-cache", function () {
//   console.log("document.yt-navigate-cache", arguments);
// });
// document.addEventListener("yt-navigate-action", function () {
//   console.log("document.yt-navigate-action", arguments);
// });
// document.addEventListener("yt-navigate-home-action", function () {
//   console.log("document.yt-navigate-home-action", arguments);
// });
// document.addEventListener("yt-page-data-fetched", function () {
//   console.log("document.yt-page-data-fetched", arguments);
// });