- // ==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.01.08.1
- // 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_getValue
- // @grant GM_setValue
- // @grant GM_deleteValue
- // @grant GM_listValues
- // @grant GM_registerMenuCommand
- // @grant GM_unregisterMenuCommand
- // @license MIT
- // @description Automcatically 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";
-
- const DEFAULT_SETTINGS = {
- targetResolution: "hd2160",
- expandMenu: false,
- debug: false
- };
-
- 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': {
- qualityMenu: 'Quality Menu',
- debug: 'DEBUG'
- },
- 'zh-TW': {
- qualityMenu: '畫質選單',
- debug: '偵錯'
- },
- 'zh-CN': {
- qualityMenu: '画质菜单',
- debug: '排错'
- },
- 'ja': {
- qualityMenu: '画質メニュー',
- debug: 'デバッグ'
- }
- };
-
- const GET_LOCALIZED_TEXT = () => {
- const language = GET_PREFERRED_LANGUAGE();
- return TRANSLATIONS[language] || TRANSLATIONS['en-US'];
- };
-
- const QUALITIES = {
- highres: 4320,
- hd2880: 2880,
- hd2160: 2160,
- hd1440: 1440,
- hd1080: 1080,
- hd720: 720,
- large: 480,
- medium: 360,
- small: 240,
- tiny: 144,
- };
-
- const PREMIUM_INDICATOR_LABEL = "Premium";
-
- let userSettings = { ...DEFAULT_SETTINGS };
- let useCompatilibtyMode = false;
- let isBrokenOrMissingGMAPI = false;
- let menuItems = [];
- let moviePlayer = null;
-
- // --- CLASS DEFINITIONS -----------
-
- class AllowedExceptionError extends Error {
- constructor(message) {
- super(message);
- this.name = "Allowed Exception";
- }
- }
-
- // --- GM FUNCTION OVERRIDES ------
-
- const GMCustomRegisterMenuCommand = useCompatilibtyMode ? GM_registerMenuCommand : GM.registerMenuCommand;
- const GMCustomUnregisterMenuCommand = useCompatilibtyMode ? GM_unregisterMenuCommand : GM.unregisterMenuCommand;
- const GMCustomGetValue = useCompatilibtyMode ? GM_getValue : GM.getValue;
- const GMCustomSetValue = useCompatilibtyMode ? GM_setValue : GM.setValue;
- const GMCustomListValues = useCompatilibtyMode ? GM_listValues : GM.listValues;
- const GMCustomDeleteValue = useCompatilibtyMode ? GM_deleteValue : GM.deleteValue;
-
- // --- FUNCTIONS ------
-
- function debugLog(message) {
- if (!userSettings.debug) return;
- const stack = new Error().stack;
- const stackLines = stack.split("\n");
- const callerLine = stackLines[2] ? stackLines[2].trim() : "Line not found";
- message += "";
- if (!message.endsWith(".")) {
- message += ".";
- }
- console.log(`[YTHD DEBUG] ${message} Function called ${callerLine}`);
- }
-
- // Attempt to set the video resolution to target quality or the next best quality
- function setResolution(force = false) {
- try {
- if (!moviePlayer ?.getAvailableQualityData().length) throw "Quality options missing.";
- let resolvedTarget = findNextAvailableQuality(userSettings.targetResolution, moviePlayer .getAvailableQualityLevels());
- const premiumData = moviePlayer .getAvailableQualityData().find(q =>
- q.quality === resolvedTarget &&
- q.qualityLabel.trim().endsWith(PREMIUM_INDICATOR_LABEL) &&
- q.isPlayable
- );
- moviePlayer .setPlaybackQualityRange(resolvedTarget, resolvedTarget, premiumData?.formatId);
- debugLog(`Setting quality to: ${resolvedTarget}${premiumData ? " Premium" : ""}`);
- } catch (error) {
- debugLog("Did not set resolution. " + error);
- }
- }
-
- function findNextAvailableQuality(target, availableQualities) {
- const targetValue = QUALITIES[target];
- return availableQualities
- .map(q => ({ quality: q, value: QUALITIES[q] }))
- .find(q => q.value <= targetValue)?.quality;
- }
-
- function processNewPage(element) {
- debugLog('Processing new page...');
- moviePlayer = document.querySelector('#movie_player');
- setResolution();
- }
-
- // ----------------------------------------
- // Functions for the quality selection menu
- function processMenuOptions(options, callback) {
- Object.values(options).forEach(option => {
- if (!option.alwaysShow && !userSettings.expandMenu) return;
- if (option.items) {
- option.items.forEach(item => callback(item));
- } else {
- callback(option);
- }
- });
- }
-
- function showMenuOptions() {
- removeMenuOptions();
- const menuOptions = {
- expandMenu: {
- alwaysShow: true,
- label: () => `${GET_LOCALIZED_TEXT().qualityMenu} ${userSettings.expandMenu ? "🔼" : "🔽"}`,
- menuId: "menuExpandBtn",
- handleClick: function () {
- userSettings.expandMenu = !userSettings.expandMenu;
- GMCustomSetValue('expandMenu', userSettings.expandMenu);
- showMenuOptions();
- },
- },
- qualities: {
- items: Object.entries(QUALITIES).map(([label, resolution]) => ({
- label: () => `${resolution}p ${label === userSettings.targetResolution ? "✅" : ""}`,
- menuId: label,
- handleClick: function () {
- if (userSettings.targetResolution == label) return;
- userSettings.targetResolution = label;
- GMCustomSetValue('targetResolution', label);
- setResolution();
- showMenuOptions();
- },
- })),
- },
- debug: {
- label: () => `${GET_LOCALIZED_TEXT().debug} ${userSettings.debug ? "✅" : ""}`,
- menuId: "debugBtn",
- handleClick: function () {
- userSettings.debug = !userSettings.debug;
- GMCustomSetValue('debug', userSettings.debug);
- showMenuOptions();
- },
- },
- };
-
- processMenuOptions(menuOptions, (item) => {
- GMCustomRegisterMenuCommand(item.label(), item.handleClick, {
- id: item.menuId,
- autoClose: false,
- });
- menuItems.push(item.menuId);
- });
- }
-
- function removeMenuOptions() {
- while (menuItems.length) {
- GMCustomUnregisterMenuCommand(menuItems.pop());
- }
- }
-
- // -----------------------------------------------
- // Verify Grease Monkey API exists and is working.
- function checkGMAPI() {
- if (typeof GM != 'undefined') return;
- if (typeof GM_info != 'undefined') {
- useCompatilibtyMode = true;
- debugLog("Running in compatiblity mode.");
- return;
- }
- isBrokenOrMissingGMAPI = true;
- }
-
- // -----------------------------------------------
- // User setting handling
- async function loadUserSettings() {
- try {
- // Get all keys from GM
- const storedValues = await GMCustomListValues();
- // Write any missing key-value pairs from DEFAULT_SETTINGS to GM
- for (const [key, value] of Object.entries(DEFAULT_SETTINGS)) {
- if (!storedValues.includes(key)) {
- await GMCustomSetValue(key, value);
- }
- }
- // Delete any extra keys in GM that are not in DEFAULT_SETTINGS
- for (const key of storedValues) {
- if (!(key in DEFAULT_SETTINGS)) {
- await GMCustomDeleteValue(key);
- }
- }
- // Retrieve and update user settings from GM
- const keyValuePairs = await Promise.all(
- storedValues.map(async key => [key, await GMCustomGetValue(key)])
- );
-
- keyValuePairs.forEach(([newKey, newValue]) => {
- userSettings[newKey] = newValue;
- });
-
- debugLog(`Loaded user settings: [${Object.entries(userSettings).map(([key, value]) => `${key}: ${value}`).join(", ")}].`);
- } catch (error){
- throw error;
- }
- }
-
- // ----------------
- // Main function
- async function initialize() {
- checkGMAPI();
- try {
- if (isBrokenOrMissingGMAPI) throw "Did not detect valid Grease Monkey API";
- await loadUserSettings();
- } catch (error) {
- debugLog(`Error loading user settings: ${error}. Loading with default settings.`);
- }
- if (window.self == window.top) {
- window.addEventListener('yt-player-updated', processNewPage, true ); //handle desktop site
- window.addEventListener('state-navigateend', processNewPage, true ); //handle mobile site
- showMenuOptions();
- } else {
- window.addEventListener('loadstart', processNewPage, true );
- }
- }
-
- // Entry Point
- initialize();
- })();