YouTube Auto HD and FPS

Auto select the highest quality on YouTube

  1. // ==UserScript==
  2. // @name YouTube Auto HD and FPS
  3. // @namespace https://github.com/jlhg/youtube-auto-hd
  4. // @license GPL-3.0
  5. // @version 0.1.0
  6. // @description Auto select the highest quality on YouTube
  7. // @description:zh-TW YouTube 自動選最高畫質
  8. // @author jlhg
  9. // @homepage https://github.com/jlhg/youtube-auto-hd
  10. // @supportURL https://github.com/jlhg/youtube-auto-hd/issues
  11. // @match https://www.youtube.com/watch*
  12. // @grant none
  13. // ==/UserScript==
  14.  
  15. (function() {
  16. 'use strict';
  17.  
  18. const SELECTORS = {
  19. buttonSettings: '.ytp-settings-button',
  20. video: 'video',
  21. player: '.html5-video-player:not(#inline-preview-player)',
  22. menuOption: '.ytp-settings-menu[data-layer] .ytp-menuitem',
  23. menuOptionContent: ".ytp-menuitem-content",
  24. optionQuality: ".ytp-settings-menu[data-layer] .ytp-menuitem:last-child",
  25. panelHeaderBack: ".ytp-panel-header button",
  26. labelPremium: '.ytp-premium-label'
  27. };
  28.  
  29. const OBSERVER_OPTIONS = {
  30. childList: true,
  31. subtree: true
  32. };
  33.  
  34. const SUFFIX_EBR = 'ebr';
  35.  
  36. const fpsSupported = [60, 50, 30];
  37. const qualities = [4320, 2160, 1440, 1080, 720, 480, 360, 240, 144];
  38.  
  39. function isElementVisible(element) {
  40. return element?.offsetWidth > 0 && element?.offsetHeight > 0;
  41. }
  42.  
  43. async function getCurrentQualityElements() {
  44. return waitElement(SELECTORS.player).then((el) => {
  45. const elMenuOptions = [...el.querySelectorAll(SELECTORS.menuOption)];
  46. return elMenuOptions.filter(getIsQualityElement);
  47. });
  48. }
  49.  
  50. function convertQualityToNumber(elQuality) {
  51. const isPremiumQuality = Boolean(elQuality.querySelector(SELECTORS.labelPremium));
  52. const qualityNumber = parseInt(elQuality.textContent);
  53. if (isPremiumQuality) {
  54. return (qualityNumber + SUFFIX_EBR);
  55. }
  56.  
  57. return qualityNumber;
  58. }
  59.  
  60. async function getAvailableQualities() {
  61. const elQualities = await getCurrentQualityElements();
  62. return elQualities.map(convertQualityToNumber);
  63. }
  64.  
  65. function getPlayerDiv(elVideo) {
  66. return elVideo.closest(SELECTORS.player);
  67. }
  68.  
  69. function getVideoFPS() {
  70. const elQualities = getCurrentQualityElements();
  71. const labelQuality = elQualities[0]?.textContent;
  72. if (!labelQuality) {
  73. return 30;
  74. }
  75. const fpsMatch = labelQuality.match(/[ps](\d+)/);
  76. return fpsMatch ? Number(fpsMatch[1]) : 30;
  77. }
  78.  
  79. function getFpsFromRange(qualities, fpsToCheck) {
  80. const fpsList = Object.keys(qualities)
  81. .map(fps => parseInt(fps))
  82. .sort((a, b) => b - a);
  83. return fpsList.find(fps => fps <= fpsToCheck) || fpsList.at(-1);
  84. }
  85.  
  86. function getIsQualityElement(element) {
  87. const isQuality = Boolean(element.textContent.match(/\d/));
  88. const isHasChildren = element.children.length > 1;
  89. return isQuality && !isHasChildren;
  90. }
  91.  
  92. async function getIsSettingsMenuOpen() {
  93. waitElement(SELECTORS.buttonSettings).then((el) => {
  94. const elButtonSettings = el;
  95. return elButtonSettings?.ariaExpanded === "true";
  96. });
  97. }
  98.  
  99. function getIsLastOptionQuality(elVideo) {
  100. const elOptionInSettings = getPlayerDiv(elVideo).querySelector(SELECTORS.optionQuality);
  101.  
  102. if (!elOptionInSettings) {
  103. return false;
  104. }
  105.  
  106. const elQualityName = elOptionInSettings.querySelector(SELECTORS.menuOptionContent);
  107.  
  108. // If the video is a channel trailer, the last option is initially the speed one,
  109. // and the speed setting can only be a single digit
  110. const matchNumber = elQualityName?.textContent?.match(/\d+/);
  111. if (!matchNumber) {
  112. return false;
  113. }
  114.  
  115. const numberString = matchNumber[0];
  116. const minQualityCharLength = 3; // e.g. 3 characters in 720p
  117.  
  118. return numberString.length >= minQualityCharLength;
  119. }
  120.  
  121. async function changeQualityAndClose(elVideo, elPlayer) {
  122. await changeQualityWhenPossible(elVideo);
  123. await closeMenu(elPlayer);
  124. }
  125.  
  126. function openQualityMenu(elVideo) {
  127. const elSettingQuality = getPlayerDiv(elVideo).querySelector(SELECTORS.optionQuality);
  128. elSettingQuality.click();
  129. }
  130.  
  131. async function changeQuality() {
  132. const elQualities = await getCurrentQualityElements();
  133. const qualitiesAvailable = await getAvailableQualities();
  134. const applyQuality = (iQuality) => {
  135. elQualities[iQuality]?.click();
  136. };
  137.  
  138. const isQualityPreferredEBR = qualitiesAvailable[0].toString().endsWith(SUFFIX_EBR);
  139. if (isQualityPreferredEBR) {
  140. applyQuality(0);
  141. return;
  142. }
  143.  
  144. const iQualityFallback = qualitiesAvailable.findIndex(quality => !quality.toString().endsWith(SUFFIX_EBR));
  145. applyQuality(iQualityFallback);
  146. }
  147.  
  148. async function changeQualityWhenPossible(elVideo) {
  149. if (!getIsLastOptionQuality(elVideo)) {
  150. elVideo.addEventListener("canplay", () => changeQualityWhenPossible(elVideo), { once: true });
  151. return;
  152. }
  153.  
  154. openQualityMenu(elVideo);
  155. await changeQuality();
  156. }
  157.  
  158. async function closeMenu(elPlayer) {
  159. const clickPanelBackIfPossible = () => {
  160. const elPanelHeaderBack = elPlayer.querySelector(SELECTORS.panelHeaderBack);
  161. if (elPanelHeaderBack) {
  162. elPanelHeaderBack.click();
  163. return true;
  164. }
  165. return false;
  166. };
  167.  
  168. if (clickPanelBackIfPossible()) {
  169. return;
  170. }
  171.  
  172. new MutationObserver((_, observer) => {
  173. if (clickPanelBackIfPossible()) {
  174. observer.disconnect();
  175. }
  176. }).observe(elPlayer, OBSERVER_OPTIONS);
  177. }
  178.  
  179. function waitElement(selector) {
  180. return new Promise(resolve => {
  181. let element = [...document.querySelectorAll(selector)]
  182. .find(isElementVisible);
  183.  
  184. if (element) {
  185. return resolve(element);
  186. }
  187.  
  188. const observer = new MutationObserver(mutations => {
  189. let element = [...document.querySelectorAll(selector)]
  190. .find(isElementVisible);
  191.  
  192. if (element) {
  193. observer.disconnect();
  194. resolve(element);
  195. }
  196. });
  197.  
  198. observer.observe(document.body, OBSERVER_OPTIONS);
  199. });
  200. }
  201.  
  202. waitElement(SELECTORS.video).then(async (elVideo) => {
  203. const elPlayer = getPlayerDiv(elVideo);
  204. const elSettings = elPlayer.querySelector(SELECTORS.buttonSettings);
  205. if (!elSettings) {
  206. return;
  207. }
  208.  
  209. const isSettingsMenuOpen = await getIsSettingsMenuOpen();
  210. if (!isSettingsMenuOpen) {
  211. elSettings.click();
  212. }
  213. elSettings.click();
  214.  
  215. await changeQualityAndClose(elVideo, elPlayer);
  216. elPlayer.querySelector(SELECTORS.buttonSettings).blur();
  217. });
  218. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址