- // ==UserScript==
- // @name Youtube HD Premium
- // @name:zh-TW Youtube HD Premium
- // @name:zh-CN Youtube HD Premium
- // @name:ja Youtube HD Premium
- // @icon https://www.youtube.com/img/favicon_48.png
- // @author ElectroKnight22
- // @namespace electroknight22_youtube_hd_namespace
- // @version 2025.04.07
- // I would prefer semantic versioning but it's a bit too late to change it at this point. Calendar versioning was originally chosen to maintain similarity to the adisib's code.
- // @match *://www.youtube.com/*
- // @match *://m.youtube.com/*
- // @match *://www.youtube-nocookie.com/*
- // @exclude *://www.youtube.com/live_chat*
- // @grant GM.getValue
- // @grant GM.setValue
- // @grant GM.deleteValue
- // @grant GM.listValues
- // @grant GM.registerMenuCommand
- // @grant GM.unregisterMenuCommand
- // @grant GM.notification
- // @grant GM_getValue
- // @grant GM_setValue
- // @grant GM_deleteValue
- // @grant GM_listValues
- // @grant GM_registerMenuCommand
- // @grant GM_unregisterMenuCommand
- // @grant GM_notification
- // @license MIT
- // @description Automatically switches to your pre-selected resolution. Enables premium when possible.
- // @description:zh-TW 自動切換到你預先設定的畫質。會優先使用Premium位元率。
- // @description:zh-CN 自动切换到你预先设定的画质。会优先使用Premium比特率。
- // @description:ja 自動的に設定した画質に替わります。Premiumのビットレートを優先的に選択します。
- // @homepage https://greasyfork.org/en/scripts/498145-youtube-hd-premium
- // ==/UserScript==
-
- /*jshint esversion: 11 */
-
- (function () {
- "use strict";
-
- // -------------------------------
- // Default settings (for storage key "settings")
- // -------------------------------
- const DEFAULT_SETTINGS = {
- targetResolution: "hd2160",
- expandMenu: false,
- debug: false
- };
-
- // -------------------------------
- // Other constants and translations
- // -------------------------------
- const BROWSER_LANGUAGE = navigator.language || navigator.userLanguage;
- const GET_PREFERRED_LANGUAGE = () => {
- if (BROWSER_LANGUAGE.startsWith('zh') && BROWSER_LANGUAGE !== 'zh-TW') {
- return 'zh-CN';
- } else {
- return BROWSER_LANGUAGE;
- }
- };
-
- const TRANSLATIONS = {
- 'en-US': {
- tampermonkeyOutdatedAlertMessage: "It looks like you're using an older version of Tampermonkey that might cause menu issues. For the best experience, please update to version 5.4.6224 or later.",
- qualityMenu: 'Quality Menu',
- autoModeName: 'Optimized Auto',
- debug: 'DEBUG'
- },
- 'zh-TW': {
- tampermonkeyOutdatedAlertMessage: "看起來您正在使用較舊版本的篡改猴,可能會導致選單問題。為了獲得最佳體驗,請更新至 5.4.6224 或更高版本。",
- qualityMenu: '畫質選單',
- autoModeName: '優化版自動模式',
- debug: '偵錯'
- },
- 'zh-CN': {
- tampermonkeyOutdatedAlertMessage: "看起来您正在使用旧版本的篡改猴,这可能会导致菜单问题。为了获得最佳体验,请更新到 5.4.6224 或更高版本。",
- qualityMenu: '画质菜单',
- autoModeName: '优化版自动模式',
- debug: '调试'
- },
- 'ja': {
- tampermonkeyOutdatedAlertMessage: "ご利用のTampermonkeyのバージョンが古いため、メニューに問題が発生する可能性があります。より良い体験のため、バージョン5.4.6224以上に更新してください。",
- qualityMenu: '画質メニュー',
- autoModeName: '最適化自動モード',
- debug: 'デバッグ'
- }
- };
-
- const GET_LOCALIZED_TEXT = () => {
- const language = GET_PREFERRED_LANGUAGE();
- return TRANSLATIONS[language] || TRANSLATIONS['en-US'];
- };
-
- const QUALITIES = {
- highres: 4320,
- hd2160: 2160,
- hd1440: 1440,
- hd1080: 1080,
- hd720: 720,
- large: 480,
- medium: 360,
- small: 240,
- tiny: 144,
- auto: 0
- };
-
- const PREMIUM_INDICATOR_LABEL = "Premium";
-
- // -------------------------------
- // Global variables
- // -------------------------------
- let userSettings = { ...DEFAULT_SETTINGS };
- let useCompatibilityMode = false;
- let menuItems = [];
- let moviePlayer = null;
- let isIframe = false;
- let isOldTampermonkey = false;
- const updatedVersions = {
- Tampermonkey: '5.4.624',
- };
- let isScriptRecentlyUpdated = false;
-
- // -------------------------------
- // GM FUNCTION OVERRIDES
- // -------------------------------
- const GMCustomRegisterMenuCommand = useCompatibilityMode ? GM_registerMenuCommand : GM.registerMenuCommand;
- const GMCustomUnregisterMenuCommand = useCompatibilityMode ? GM_unregisterMenuCommand : GM.unregisterMenuCommand;
- const GMCustomGetValue = useCompatibilityMode ? GM_getValue : GM.getValue;
- const GMCustomSetValue = useCompatibilityMode ? GM_setValue : GM.setValue;
- const GMCustomDeleteValue = useCompatibilityMode ? GM_deleteValue : GM.deleteValue;
- const GMCustomListValues = useCompatibilityMode ? GM_listValues : GM.listValues;
- const GMCustomNotification = useCompatibilityMode ? GM_notification : GM.notification;
-
- // -------------------------------
- // Debug logging helper
- // -------------------------------
- function printDebug(consoleMethod = console.log, ...args) {
- if (!userSettings.debug) return;
- if (typeof consoleMethod !== 'function') {
- args.unshift(consoleMethod);
- consoleMethod = console.log;
- }
-
- consoleMethod(...args);
- }
-
- // -------------------------------
- // Video quality functions
- // -------------------------------
- function setResolution() {
- try {
- if (!moviePlayer) throw new Error("Movie player not found.");
-
- const videoQualityData = moviePlayer.getAvailableQualityData();
- const currentPlaybackQuality = moviePlayer.getPlaybackQuality();
- const currentQualityLabel = moviePlayer.getPlaybackQualityLabel();
-
- if (isIframe && !videoQualityData.length) { // fixes non-auto-playing iframes
- printDebug("Performing iframe magic...");
- const videoElement = moviePlayer.querySelector('video');
- moviePlayer.setPlaybackQualityRange(userSettings.targetResolution); // Force set quality to user preference. Breaks the UI but quality will be mostly correct.
- videoElement.addEventListener('play', setResolution, { once: true }); // Waits for playback to set quality properly. Fixes the UI and guarantees correct quality.
- return;
- }
-
- if (!videoQualityData.length) throw new Error("Quality options missing.");
- if (userSettings.targetResolution === 'auto') {
- if (!currentPlaybackQuality || !currentQualityLabel) throw new Error("Unable to determine current playback quality.");
- const isOptimalQuality =
- videoQualityData.filter(q => q.quality == currentPlaybackQuality).length <= 1 ||
- currentQualityLabel.trim().endsWith(PREMIUM_INDICATOR_LABEL);
- if (!isOptimalQuality) moviePlayer.loadVideoById(moviePlayer.getVideoData().video_id);
- printDebug(`Setting quality to: [${GET_LOCALIZED_TEXT().autoModeName}]`);
- } else {
- let resolvedTarget = findNextAvailableQuality(userSettings.targetResolution, moviePlayer.getAvailableQualityLevels());
- const premiumData = videoQualityData.find(q =>
- q.quality === resolvedTarget &&
- q.qualityLabel?.trim().endsWith(PREMIUM_INDICATOR_LABEL) &&
- q.isPlayable
- );
- moviePlayer.setPlaybackQualityRange(resolvedTarget, resolvedTarget, premiumData?.formatId);
- printDebug(`Setting quality to: [${resolvedTarget}${premiumData ? " Premium" : ""}]`);
- }
- } catch (error) {
- printDebug(console.error, "Did not set resolution. ", error);
- }
- }
-
- function findNextAvailableQuality(target, availableQualities) {
- const targetValue = QUALITIES[target];
- return availableQualities
- .filter(q => QUALITIES[q] <= targetValue)
- .sort((a, b) => QUALITIES[b] - QUALITIES[a])[0];
- }
-
- function processVideoLoad(event = null) {
- printDebug('Processing video load...');
- moviePlayer = event?.target?.player_ ?? document.querySelector('#movie_player');
- setResolution();
- }
-
- // -------------------------------
- // Menu functions
- // -------------------------------
- function processMenuOptions(options, callback) {
- Object.values(options).forEach(option => {
- if (!option.alwaysShow && !userSettings.expandMenu && !isOldTampermonkey) return;
- if (option.items) {
- option.items.forEach(item => callback(item));
- } else {
- callback(option);
- }
- });
- }
-
- // The menu callbacks now use the helper "updateSetting" to update the stored settings.
- function showMenuOptions() {
- const shouldAutoClose = isOldTampermonkey;
- removeMenuOptions();
- const menuExpandButton = isOldTampermonkey ? {} : {
- expandMenu: {
- alwaysShow: true,
- label: () => `${GET_LOCALIZED_TEXT().qualityMenu} ${userSettings.expandMenu ? "🔼" : "🔽"}`,
- menuId: "menuExpandBtn",
- handleClick: async function () {
- userSettings.expandMenu = !userSettings.expandMenu;
- await updateSetting('expandMenu', userSettings.expandMenu);
- showMenuOptions();
- },
- },
- };
- const menuOptions = {
- ...menuExpandButton,
- qualities: {
- items: Object.entries(QUALITIES).map(([label, resolution]) => ({
- label: () => `${resolution === 0 ? GET_LOCALIZED_TEXT().autoModeName : resolution + 'p'} ${label === userSettings.targetResolution ? "✅" : ""}`,
- menuId: label,
- handleClick: async function () {
- if (userSettings.targetResolution === label) return;
- userSettings.targetResolution = label;
- await updateSetting('targetResolution', label);
- setResolution();
- showMenuOptions();
- },
- })),
- },
- debug: {
- label: () => `${GET_LOCALIZED_TEXT().debug} ${userSettings.debug ? "✅" : ""}`,
- menuId: "debugBtn",
- handleClick: async function () {
- userSettings.debug = !userSettings.debug;
- await updateSetting('debug', userSettings.debug);
- showMenuOptions();
- },
- },
- };
-
- processMenuOptions(menuOptions, (item) => {
- GMCustomRegisterMenuCommand(item.label(), item.handleClick, {
- id: item.menuId,
- autoClose: shouldAutoClose,
- });
- menuItems.push(item.menuId);
- });
- }
-
- function removeMenuOptions() {
- while (menuItems.length) {
- GMCustomUnregisterMenuCommand(menuItems.pop());
- }
- }
-
- // -------------------------------
- // GreaseMonkey / Tampermonkey version checks
- // -------------------------------
- function compareVersions(v1, v2) {
- try {
- if (!v1 || !v2) throw new Error("Invalid version string.");
- if (v1 === v2) return 0;
- const parts1 = v1.split('.').map(Number);
- const parts2 = v2.split('.').map(Number);
- const len = Math.max(parts1.length, parts2.length);
- for (let i = 0; i < len; i++) {
- const num1 = parts1[i] ?? 0;
- const num2 = parts2[i] ?? 0;
- if (num1 > num2) return 1;
- if (num1 < num2) return -1;
- }
- return 0;
- } catch (error) {
- throw new Error("Error comparing versions: " + error);
- }
- }
-
- function hasGreasyMonkeyAPI() {
- if (typeof GM !== 'undefined') return true;
- if (typeof GM_info !== 'undefined') {
- useCompatibilityMode = true;
- printDebug(console.warn, "Running in compatibility mode.");
- return true;
- }
- return false;
- }
-
- function CheckTampermonkeyUpdated() {
- if (GM_info.scriptHandler === "Tampermonkey" &&
- compareVersions(GM_info.version, updatedVersions.Tampermonkey) !== 1) {
- isOldTampermonkey = true;
- if (isScriptRecentlyUpdated) {
- GMCustomNotification({
- text: GET_LOCALIZED_TEXT().tampermonkeyOutdatedAlertMessage,
- timeout: 15000
- });
- }
- }
- }
-
- // -------------------------------
- // Storage helper functions
- // -------------------------------
-
- /**
- * Load user settings from the "settings" key.
- * Ensures that only keys existing in DEFAULT_SETTINGS are kept.
- * If no stored settings are found, defaults are used.
- */
- async function loadUserSettings() {
- try {
- const storedSettings = await GMCustomGetValue('settings', {});
- userSettings = Object.keys(DEFAULT_SETTINGS).reduce((accumulator, key) => {
- accumulator[key] = storedSettings.hasOwnProperty(key) ? storedSettings[key] : DEFAULT_SETTINGS[key];
- return accumulator;
- }, {});
- await GMCustomSetValue('settings', userSettings);
- printDebug(`Loaded user settings: ${JSON.stringify(userSettings)}.`);
- } catch (error) {
- throw error;
- }
- }
-
- // Update one setting in the stored settings.
- async function updateSetting(key, value) {
- try {
- let currentSettings = await GMCustomGetValue('settings', DEFAULT_SETTINGS);
- currentSettings[key] = value;
- await GMCustomSetValue('settings', currentSettings);
- } catch (error) {
- printDebug(console.error, "Error updating setting: ", error);
- }
- }
-
- async function updateScriptInfo() {
- try {
- const oldScriptInfo = await GMCustomGetValue('scriptInfo', null);
- printDebug(`Previous script info: ${JSON.stringify(oldScriptInfo)}`);
- const newScriptInfo = {
- version: getScriptVersionFromMeta(),
- };
- await GMCustomSetValue('scriptInfo', newScriptInfo);
-
- if (!oldScriptInfo || compareVersions(newScriptInfo.version, oldScriptInfo?.version) !== 0) {
- isScriptRecentlyUpdated = true;
- }
- printDebug(`Updated script info: ${JSON.stringify(newScriptInfo)}`);
- } catch (error) {
- printDebug(console.error, "Error updating script info: ", error);
- }
- }
-
- // Cleanup any leftover keys from previous versions.
- async function cleanupOldStorage() {
- try {
- const allowedKeys = ['settings', 'scriptInfo'];
- const keys = await GMCustomListValues();
- for (const key of keys) {
- if (!allowedKeys.includes(key)) {
- await GMCustomDeleteValue(key);
- printDebug(`Deleted leftover key: ${key}`);
- }
- }
- } catch (error) {
- printDebug(console.error, "Error cleaning up old storage keys: ", error);
- }
- }
-
- // -------------------------------
- // Script metadata extraction
- // -------------------------------
- function getScriptVersionFromMeta() {
- const meta = GM_info.scriptMetaStr;
- const versionMatch = meta?.match(/@version\s+([^\r\n]+)/);
- return versionMatch ? versionMatch[1].trim() : null;
- }
-
- // -------------------------------
- // Main function: add event listeners and initialize
- // -------------------------------
- function addEventListeners() {
- if (window.location.hostname === "m.youtube.com") {
- window.addEventListener('state-navigateend', processVideoLoad, true);
- } else {
- window.addEventListener('yt-player-updated', processVideoLoad, true);
- }
- }
-
- async function initialize() {
- try {
- if (!hasGreasyMonkeyAPI()) throw new Error("Did not detect valid Grease Monkey API");
- await cleanupOldStorage();
- await loadUserSettings();
- await updateScriptInfo();
- CheckTampermonkeyUpdated();
- } catch (error) {
- printDebug(console.error, `Error loading user settings: ${error}. Loading with default settings.`);
- }
-
- window.addEventListener('pageshow', processVideoLoad, true);
- if (window.self === window.top) {
- addEventListeners();
- showMenuOptions();
- } else {
- isIframe = true;
- }
- }
-
- // -------------------------------
- // Entry Point
- // -------------------------------
- initialize();
- })();