Greasy Fork is available in English.

YouTube Fixed Video Quality

Remember video quality without any additional user interface.

// ==UserScript==
// @name         YouTube Fixed Video Quality
// @name:zh-TW   YouTube固定影片畫質
// @name:zh-CN   YouTube固定视频画质

// @description          Remember video quality without any additional user interface.
// @description:zh-TW    記住選取的 YouTube 影片畫質,不需任何額外的操作介面。
// @description:zh-CN    记住选取的 YouTube 视频画质,不需任何额外的操作介面。

// @license MIT
// @namespace    https://greasyfork.org/users/1086571
// @version      1.2
// @author       IzsKon
// @match        https://www.youtube.com/*
// @icon         https://raw.githubusercontent.com/IzsKon/YouTube-Fixed-Video-Quality/main/icon.png
// @grant        GM.getValue
// @grant        GM.setValue
// ==/UserScript==

(async function() {
	'use strict';

	let vidQuality = await GM.getValue( 'videoQuality', 1 );
	let player = null;

	document.addEventListener('yt-player-updated', () => {

		/* Check page type. Video url should be /watch or /live */
		if ( /^\/watch|^\/live/.test(window.location.pathname) ) {
			setVideoQuality();
		}
	});


	async function setVideoQuality() {

		/* Load settings panel. */
		let settingsBtn = document.querySelector('.ytp-settings-button');
		settingsBtn.click();
		settingsBtn.click();

		/* Open quality selection panel. */
		let qualityBtn = document.querySelector('.ytp-menuitem-content div:not(.ytp-menuitem-toggle-checkbox)');
		if (!qualityBtn) { /* Video not loaded: stream not started or other issues. */
			detectVideoStart();
			return;
		}
		qualityBtn.click();
		let qualityOptions = document.querySelectorAll('.ytp-quality-menu .ytp-menuitem:not(:has(.ytp-premium-label))');

		/* Close quality selection panel. */
		settingsBtn.click();
		settingsBtn.click();

		/* Select video quality. */
		let nth_option = qualityOptions.length - vidQuality;
		qualityOptions[ Math.max(0, nth_option) ].click();

		/* Add event listener to quality selection. */
		for ( let i = 0; i < qualityOptions.length; ++i ) {
			qualityOptions[i].addEventListener('click', () => {
				GM.setValue( 'videoQuality', qualityOptions.length - i );
			});
		}
	}


	function detectVideoStart() {

		/* Prevent infinite loop b/w detectVideoStart() & setVideoQuality() in case sth go wrong. */
		if (player) return;

		const observer = new MutationObserver((mutations) => {
			mutations.forEach((mutation) => {
				if (mutation.type != 'attributes') return;
				if (mutation.attributeName != 'class') return;

				/* Detect when a stream starts. */
				if ( player.classList.contains('unstarted-mode') ) return;

				observer.disconnect();
				setVideoQuality();
			});
		});

		player = document.getElementById('movie_player');
		observer.observe(player, { attributes: true });
	}

})();