您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Auto select the highest quality on YouTube (incl. Premium, if applicable)
// ==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); // });