Greasy Fork is available in English.

Youtube HD Premium

Automcatically switches to your pre-selected resolution. Enables premium when possible.

  1. // ==UserScript==
  2. // @name Youtube HD Premium
  3. // @name:zh-TW Youtube HD Premium
  4. // @name:zh-CN Youtube HD Premium
  5. // @name:ja Youtube HD Premium
  6. // @icon https://www.youtube.com/img/favicon_48.png
  7. // @author ElectroKnight22
  8. // @namespace electroknight22_youtube_hd_namespace
  9. // @version 2025.01.08.1
  10. // 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.
  11. // @match *://www.youtube.com/*
  12. // @match *://m.youtube.com/*
  13. // @match *://www.youtube-nocookie.com/*
  14. // @exclude *://www.youtube.com/live_chat*
  15. // @grant GM.getValue
  16. // @grant GM.setValue
  17. // @grant GM.deleteValue
  18. // @grant GM.listValues
  19. // @grant GM.registerMenuCommand
  20. // @grant GM.unregisterMenuCommand
  21. // @grant GM_getValue
  22. // @grant GM_setValue
  23. // @grant GM_deleteValue
  24. // @grant GM_listValues
  25. // @grant GM_registerMenuCommand
  26. // @grant GM_unregisterMenuCommand
  27. // @license MIT
  28. // @description Automcatically switches to your pre-selected resolution. Enables premium when possible.
  29. // @description:zh-TW 自動切換到你預先設定的畫質。會優先使用Premium位元率。
  30. // @description:zh-CN 自动切换到你预先设定的画质。会优先使用Premium比特率。
  31. // @description:ja 自動的に設定した画質に替わります。Premiumのビットレートを優先的に選択します。
  32. // @homepage https://greasyfork.org/en/scripts/498145-youtube-hd-premium
  33. // ==/UserScript==
  34.  
  35. /*jshint esversion: 11 */
  36.  
  37. (function() {
  38. "use strict";
  39.  
  40. const DEFAULT_SETTINGS = {
  41. targetResolution: "hd2160",
  42. expandMenu: false,
  43. debug: false
  44. };
  45.  
  46. const BROWSER_LANGUAGE = navigator.language || navigator.userLanguage;
  47. const GET_PREFERRED_LANGUAGE = () => {
  48. if (BROWSER_LANGUAGE.startsWith('zh') && BROWSER_LANGUAGE !== 'zh-TW'){
  49. return 'zh-CN';
  50. } else {
  51. return BROWSER_LANGUAGE;
  52. }
  53. };
  54.  
  55. const TRANSLATIONS = {
  56. 'en-US': {
  57. qualityMenu: 'Quality Menu',
  58. debug: 'DEBUG'
  59. },
  60. 'zh-TW': {
  61. qualityMenu: '畫質選單',
  62. debug: '偵錯'
  63. },
  64. 'zh-CN': {
  65. qualityMenu: '画质菜单',
  66. debug: '排错'
  67. },
  68. 'ja': {
  69. qualityMenu: '画質メニュー',
  70. debug: 'デバッグ'
  71. }
  72. };
  73.  
  74. const GET_LOCALIZED_TEXT = () => {
  75. const language = GET_PREFERRED_LANGUAGE();
  76. return TRANSLATIONS[language] || TRANSLATIONS['en-US'];
  77. };
  78.  
  79. const QUALITIES = {
  80. highres: 4320,
  81. hd2880: 2880,
  82. hd2160: 2160,
  83. hd1440: 1440,
  84. hd1080: 1080,
  85. hd720: 720,
  86. large: 480,
  87. medium: 360,
  88. small: 240,
  89. tiny: 144,
  90. };
  91.  
  92. const PREMIUM_INDICATOR_LABEL = "Premium";
  93.  
  94. let userSettings = { ...DEFAULT_SETTINGS };
  95. let useCompatilibtyMode = false;
  96. let isBrokenOrMissingGMAPI = false;
  97. let menuItems = [];
  98. let moviePlayer = null;
  99.  
  100. // --- CLASS DEFINITIONS -----------
  101.  
  102. class AllowedExceptionError extends Error {
  103. constructor(message) {
  104. super(message);
  105. this.name = "Allowed Exception";
  106. }
  107. }
  108.  
  109. // --- GM FUNCTION OVERRIDES ------
  110.  
  111. const GMCustomRegisterMenuCommand = useCompatilibtyMode ? GM_registerMenuCommand : GM.registerMenuCommand;
  112. const GMCustomUnregisterMenuCommand = useCompatilibtyMode ? GM_unregisterMenuCommand : GM.unregisterMenuCommand;
  113. const GMCustomGetValue = useCompatilibtyMode ? GM_getValue : GM.getValue;
  114. const GMCustomSetValue = useCompatilibtyMode ? GM_setValue : GM.setValue;
  115. const GMCustomListValues = useCompatilibtyMode ? GM_listValues : GM.listValues;
  116. const GMCustomDeleteValue = useCompatilibtyMode ? GM_deleteValue : GM.deleteValue;
  117.  
  118. // --- FUNCTIONS ------
  119.  
  120. function debugLog(message) {
  121. if (!userSettings.debug) return;
  122. const stack = new Error().stack;
  123. const stackLines = stack.split("\n");
  124. const callerLine = stackLines[2] ? stackLines[2].trim() : "Line not found";
  125. message += "";
  126. if (!message.endsWith(".")) {
  127. message += ".";
  128. }
  129. console.log(`[YTHD DEBUG] ${message} Function called ${callerLine}`);
  130. }
  131.  
  132. // Attempt to set the video resolution to target quality or the next best quality
  133. function setResolution(force = false) {
  134. try {
  135. if (!moviePlayer ?.getAvailableQualityData().length) throw "Quality options missing.";
  136. let resolvedTarget = findNextAvailableQuality(userSettings.targetResolution, moviePlayer .getAvailableQualityLevels());
  137. const premiumData = moviePlayer .getAvailableQualityData().find(q =>
  138. q.quality === resolvedTarget &&
  139. q.qualityLabel.trim().endsWith(PREMIUM_INDICATOR_LABEL) &&
  140. q.isPlayable
  141. );
  142. moviePlayer .setPlaybackQualityRange(resolvedTarget, resolvedTarget, premiumData?.formatId);
  143. debugLog(`Setting quality to: ${resolvedTarget}${premiumData ? " Premium" : ""}`);
  144. } catch (error) {
  145. debugLog("Did not set resolution. " + error);
  146. }
  147. }
  148.  
  149. function findNextAvailableQuality(target, availableQualities) {
  150. const targetValue = QUALITIES[target];
  151. return availableQualities
  152. .map(q => ({ quality: q, value: QUALITIES[q] }))
  153. .find(q => q.value <= targetValue)?.quality;
  154. }
  155.  
  156. function processNewPage(element) {
  157. debugLog('Processing new page...');
  158. moviePlayer = document.querySelector('#movie_player');
  159. setResolution();
  160. }
  161.  
  162. // ----------------------------------------
  163. // Functions for the quality selection menu
  164. function processMenuOptions(options, callback) {
  165. Object.values(options).forEach(option => {
  166. if (!option.alwaysShow && !userSettings.expandMenu) return;
  167. if (option.items) {
  168. option.items.forEach(item => callback(item));
  169. } else {
  170. callback(option);
  171. }
  172. });
  173. }
  174.  
  175. function showMenuOptions() {
  176. removeMenuOptions();
  177. const menuOptions = {
  178. expandMenu: {
  179. alwaysShow: true,
  180. label: () => `${GET_LOCALIZED_TEXT().qualityMenu} ${userSettings.expandMenu ? "🔼" : "🔽"}`,
  181. menuId: "menuExpandBtn",
  182. handleClick: function () {
  183. userSettings.expandMenu = !userSettings.expandMenu;
  184. GMCustomSetValue('expandMenu', userSettings.expandMenu);
  185. showMenuOptions();
  186. },
  187. },
  188. qualities: {
  189. items: Object.entries(QUALITIES).map(([label, resolution]) => ({
  190. label: () => `${resolution}p ${label === userSettings.targetResolution ? "✅" : ""}`,
  191. menuId: label,
  192. handleClick: function () {
  193. if (userSettings.targetResolution == label) return;
  194. userSettings.targetResolution = label;
  195. GMCustomSetValue('targetResolution', label);
  196. setResolution();
  197. showMenuOptions();
  198. },
  199. })),
  200. },
  201. debug: {
  202. label: () => `${GET_LOCALIZED_TEXT().debug} ${userSettings.debug ? "✅" : ""}`,
  203. menuId: "debugBtn",
  204. handleClick: function () {
  205. userSettings.debug = !userSettings.debug;
  206. GMCustomSetValue('debug', userSettings.debug);
  207. showMenuOptions();
  208. },
  209. },
  210. };
  211.  
  212. processMenuOptions(menuOptions, (item) => {
  213. GMCustomRegisterMenuCommand(item.label(), item.handleClick, {
  214. id: item.menuId,
  215. autoClose: false,
  216. });
  217. menuItems.push(item.menuId);
  218. });
  219. }
  220.  
  221. function removeMenuOptions() {
  222. while (menuItems.length) {
  223. GMCustomUnregisterMenuCommand(menuItems.pop());
  224. }
  225. }
  226.  
  227. // -----------------------------------------------
  228. // Verify Grease Monkey API exists and is working.
  229. function checkGMAPI() {
  230. if (typeof GM != 'undefined') return;
  231. if (typeof GM_info != 'undefined') {
  232. useCompatilibtyMode = true;
  233. debugLog("Running in compatiblity mode.");
  234. return;
  235. }
  236. isBrokenOrMissingGMAPI = true;
  237. }
  238.  
  239. // -----------------------------------------------
  240. // User setting handling
  241. async function loadUserSettings() {
  242. try {
  243. // Get all keys from GM
  244. const storedValues = await GMCustomListValues();
  245. // Write any missing key-value pairs from DEFAULT_SETTINGS to GM
  246. for (const [key, value] of Object.entries(DEFAULT_SETTINGS)) {
  247. if (!storedValues.includes(key)) {
  248. await GMCustomSetValue(key, value);
  249. }
  250. }
  251. // Delete any extra keys in GM that are not in DEFAULT_SETTINGS
  252. for (const key of storedValues) {
  253. if (!(key in DEFAULT_SETTINGS)) {
  254. await GMCustomDeleteValue(key);
  255. }
  256. }
  257. // Retrieve and update user settings from GM
  258. const keyValuePairs = await Promise.all(
  259. storedValues.map(async key => [key, await GMCustomGetValue(key)])
  260. );
  261.  
  262. keyValuePairs.forEach(([newKey, newValue]) => {
  263. userSettings[newKey] = newValue;
  264. });
  265.  
  266. debugLog(`Loaded user settings: [${Object.entries(userSettings).map(([key, value]) => `${key}: ${value}`).join(", ")}].`);
  267. } catch (error){
  268. throw error;
  269. }
  270. }
  271.  
  272. // ----------------
  273. // Main function
  274. async function initialize() {
  275. checkGMAPI();
  276. try {
  277. if (isBrokenOrMissingGMAPI) throw "Did not detect valid Grease Monkey API";
  278. await loadUserSettings();
  279. } catch (error) {
  280. debugLog(`Error loading user settings: ${error}. Loading with default settings.`);
  281. }
  282. if (window.self == window.top) {
  283. window.addEventListener('yt-player-updated', processNewPage, true ); //handle desktop site
  284. window.addEventListener('state-navigateend', processNewPage, true ); //handle mobile site
  285. showMenuOptions();
  286. } else {
  287. window.addEventListener('loadstart', processNewPage, true );
  288. }
  289. }
  290.  
  291. // Entry Point
  292. initialize();
  293. })();