Youtube HD Premium

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

ติดตั้งสคริปต์นี้?
สคริปต์ที่แนะนำของผู้เขียน

คุณอาจชื่นชอบ Better Theater Mode for YouTube

ติดตั้งสคริปต์นี้
  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.04.07
  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.notification
  22. // @grant GM_getValue
  23. // @grant GM_setValue
  24. // @grant GM_deleteValue
  25. // @grant GM_listValues
  26. // @grant GM_registerMenuCommand
  27. // @grant GM_unregisterMenuCommand
  28. // @grant GM_notification
  29. // @license MIT
  30. // @description Automatically switches to your pre-selected resolution. Enables premium when possible.
  31. // @description:zh-TW 自動切換到你預先設定的畫質。會優先使用Premium位元率。
  32. // @description:zh-CN 自动切换到你预先设定的画质。会优先使用Premium比特率。
  33. // @description:ja 自動的に設定した画質に替わります。Premiumのビットレートを優先的に選択します。
  34. // @homepage https://greasyfork.org/en/scripts/498145-youtube-hd-premium
  35. // ==/UserScript==
  36.  
  37. /*jshint esversion: 11 */
  38.  
  39. (function () {
  40. "use strict";
  41.  
  42. // -------------------------------
  43. // Default settings (for storage key "settings")
  44. // -------------------------------
  45. const DEFAULT_SETTINGS = {
  46. targetResolution: "hd2160",
  47. expandMenu: false,
  48. debug: false
  49. };
  50.  
  51. // -------------------------------
  52. // Other constants and translations
  53. // -------------------------------
  54. const BROWSER_LANGUAGE = navigator.language || navigator.userLanguage;
  55. const GET_PREFERRED_LANGUAGE = () => {
  56. if (BROWSER_LANGUAGE.startsWith('zh') && BROWSER_LANGUAGE !== 'zh-TW') {
  57. return 'zh-CN';
  58. } else {
  59. return BROWSER_LANGUAGE;
  60. }
  61. };
  62.  
  63. const TRANSLATIONS = {
  64. 'en-US': {
  65. 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.",
  66. qualityMenu: 'Quality Menu',
  67. autoModeName: 'Optimized Auto',
  68. debug: 'DEBUG'
  69. },
  70. 'zh-TW': {
  71. tampermonkeyOutdatedAlertMessage: "看起來您正在使用較舊版本的篡改猴,可能會導致選單問題。為了獲得最佳體驗,請更新至 5.4.6224 或更高版本。",
  72. qualityMenu: '畫質選單',
  73. autoModeName: '優化版自動模式',
  74. debug: '偵錯'
  75. },
  76. 'zh-CN': {
  77. tampermonkeyOutdatedAlertMessage: "看起来您正在使用旧版本的篡改猴,这可能会导致菜单问题。为了获得最佳体验,请更新到 5.4.6224 或更高版本。",
  78. qualityMenu: '画质菜单',
  79. autoModeName: '优化版自动模式',
  80. debug: '调试'
  81. },
  82. 'ja': {
  83. tampermonkeyOutdatedAlertMessage: "ご利用のTampermonkeyのバージョンが古いため、メニューに問題が発生する可能性があります。より良い体験のため、バージョン5.4.6224以上に更新してください。",
  84. qualityMenu: '画質メニュー',
  85. autoModeName: '最適化自動モード',
  86. debug: 'デバッグ'
  87. }
  88. };
  89.  
  90. const GET_LOCALIZED_TEXT = () => {
  91. const language = GET_PREFERRED_LANGUAGE();
  92. return TRANSLATIONS[language] || TRANSLATIONS['en-US'];
  93. };
  94.  
  95. const QUALITIES = {
  96. highres: 4320,
  97. hd2160: 2160,
  98. hd1440: 1440,
  99. hd1080: 1080,
  100. hd720: 720,
  101. large: 480,
  102. medium: 360,
  103. small: 240,
  104. tiny: 144,
  105. auto: 0
  106. };
  107.  
  108. const PREMIUM_INDICATOR_LABEL = "Premium";
  109.  
  110. // -------------------------------
  111. // Global variables
  112. // -------------------------------
  113. let userSettings = { ...DEFAULT_SETTINGS };
  114. let useCompatibilityMode = false;
  115. let menuItems = [];
  116. let moviePlayer = null;
  117. let isIframe = false;
  118. let isOldTampermonkey = false;
  119. const updatedVersions = {
  120. Tampermonkey: '5.4.624',
  121. };
  122. let isScriptRecentlyUpdated = false;
  123.  
  124. // -------------------------------
  125. // GM FUNCTION OVERRIDES
  126. // -------------------------------
  127. const GMCustomRegisterMenuCommand = useCompatibilityMode ? GM_registerMenuCommand : GM.registerMenuCommand;
  128. const GMCustomUnregisterMenuCommand = useCompatibilityMode ? GM_unregisterMenuCommand : GM.unregisterMenuCommand;
  129. const GMCustomGetValue = useCompatibilityMode ? GM_getValue : GM.getValue;
  130. const GMCustomSetValue = useCompatibilityMode ? GM_setValue : GM.setValue;
  131. const GMCustomDeleteValue = useCompatibilityMode ? GM_deleteValue : GM.deleteValue;
  132. const GMCustomListValues = useCompatibilityMode ? GM_listValues : GM.listValues;
  133. const GMCustomNotification = useCompatibilityMode ? GM_notification : GM.notification;
  134.  
  135. // -------------------------------
  136. // Debug logging helper
  137. // -------------------------------
  138. function printDebug(consoleMethod = console.log, ...args) {
  139. if (!userSettings.debug) return;
  140. if (typeof consoleMethod !== 'function') {
  141. args.unshift(consoleMethod);
  142. consoleMethod = console.log;
  143. }
  144.  
  145. consoleMethod(...args);
  146. }
  147.  
  148. // -------------------------------
  149. // Video quality functions
  150. // -------------------------------
  151. function setResolution() {
  152. try {
  153. if (!moviePlayer) throw new Error("Movie player not found.");
  154.  
  155. const videoQualityData = moviePlayer.getAvailableQualityData();
  156. const currentPlaybackQuality = moviePlayer.getPlaybackQuality();
  157. const currentQualityLabel = moviePlayer.getPlaybackQualityLabel();
  158.  
  159. if (isIframe && !videoQualityData.length) { // fixes non-auto-playing iframes
  160. printDebug("Performing iframe magic...");
  161. const videoElement = moviePlayer.querySelector('video');
  162. moviePlayer.setPlaybackQualityRange(userSettings.targetResolution); // Force set quality to user preference. Breaks the UI but quality will be mostly correct.
  163. videoElement.addEventListener('play', setResolution, { once: true }); // Waits for playback to set quality properly. Fixes the UI and guarantees correct quality.
  164. return;
  165. }
  166.  
  167. if (!videoQualityData.length) throw new Error("Quality options missing.");
  168. if (userSettings.targetResolution === 'auto') {
  169. if (!currentPlaybackQuality || !currentQualityLabel) throw new Error("Unable to determine current playback quality.");
  170. const isOptimalQuality =
  171. videoQualityData.filter(q => q.quality == currentPlaybackQuality).length <= 1 ||
  172. currentQualityLabel.trim().endsWith(PREMIUM_INDICATOR_LABEL);
  173. if (!isOptimalQuality) moviePlayer.loadVideoById(moviePlayer.getVideoData().video_id);
  174. printDebug(`Setting quality to: [${GET_LOCALIZED_TEXT().autoModeName}]`);
  175. } else {
  176. let resolvedTarget = findNextAvailableQuality(userSettings.targetResolution, moviePlayer.getAvailableQualityLevels());
  177. const premiumData = videoQualityData.find(q =>
  178. q.quality === resolvedTarget &&
  179. q.qualityLabel?.trim().endsWith(PREMIUM_INDICATOR_LABEL) &&
  180. q.isPlayable
  181. );
  182. moviePlayer.setPlaybackQualityRange(resolvedTarget, resolvedTarget, premiumData?.formatId);
  183. printDebug(`Setting quality to: [${resolvedTarget}${premiumData ? " Premium" : ""}]`);
  184. }
  185. } catch (error) {
  186. printDebug(console.error, "Did not set resolution. ", error);
  187. }
  188. }
  189.  
  190. function findNextAvailableQuality(target, availableQualities) {
  191. const targetValue = QUALITIES[target];
  192. return availableQualities
  193. .filter(q => QUALITIES[q] <= targetValue)
  194. .sort((a, b) => QUALITIES[b] - QUALITIES[a])[0];
  195. }
  196.  
  197. function processVideoLoad(event = null) {
  198. printDebug('Processing video load...');
  199. moviePlayer = event?.target?.player_ ?? document.querySelector('#movie_player');
  200. setResolution();
  201. }
  202.  
  203. // -------------------------------
  204. // Menu functions
  205. // -------------------------------
  206. function processMenuOptions(options, callback) {
  207. Object.values(options).forEach(option => {
  208. if (!option.alwaysShow && !userSettings.expandMenu && !isOldTampermonkey) return;
  209. if (option.items) {
  210. option.items.forEach(item => callback(item));
  211. } else {
  212. callback(option);
  213. }
  214. });
  215. }
  216.  
  217. // The menu callbacks now use the helper "updateSetting" to update the stored settings.
  218. function showMenuOptions() {
  219. const shouldAutoClose = isOldTampermonkey;
  220. removeMenuOptions();
  221. const menuExpandButton = isOldTampermonkey ? {} : {
  222. expandMenu: {
  223. alwaysShow: true,
  224. label: () => `${GET_LOCALIZED_TEXT().qualityMenu} ${userSettings.expandMenu ? "🔼" : "🔽"}`,
  225. menuId: "menuExpandBtn",
  226. handleClick: async function () {
  227. userSettings.expandMenu = !userSettings.expandMenu;
  228. await updateSetting('expandMenu', userSettings.expandMenu);
  229. showMenuOptions();
  230. },
  231. },
  232. };
  233. const menuOptions = {
  234. ...menuExpandButton,
  235. qualities: {
  236. items: Object.entries(QUALITIES).map(([label, resolution]) => ({
  237. label: () => `${resolution === 0 ? GET_LOCALIZED_TEXT().autoModeName : resolution + 'p'} ${label === userSettings.targetResolution ? "✅" : ""}`,
  238. menuId: label,
  239. handleClick: async function () {
  240. if (userSettings.targetResolution === label) return;
  241. userSettings.targetResolution = label;
  242. await updateSetting('targetResolution', label);
  243. setResolution();
  244. showMenuOptions();
  245. },
  246. })),
  247. },
  248. debug: {
  249. label: () => `${GET_LOCALIZED_TEXT().debug} ${userSettings.debug ? "✅" : ""}`,
  250. menuId: "debugBtn",
  251. handleClick: async function () {
  252. userSettings.debug = !userSettings.debug;
  253. await updateSetting('debug', userSettings.debug);
  254. showMenuOptions();
  255. },
  256. },
  257. };
  258.  
  259. processMenuOptions(menuOptions, (item) => {
  260. GMCustomRegisterMenuCommand(item.label(), item.handleClick, {
  261. id: item.menuId,
  262. autoClose: shouldAutoClose,
  263. });
  264. menuItems.push(item.menuId);
  265. });
  266. }
  267.  
  268. function removeMenuOptions() {
  269. while (menuItems.length) {
  270. GMCustomUnregisterMenuCommand(menuItems.pop());
  271. }
  272. }
  273.  
  274. // -------------------------------
  275. // GreaseMonkey / Tampermonkey version checks
  276. // -------------------------------
  277. function compareVersions(v1, v2) {
  278. try {
  279. if (!v1 || !v2) throw new Error("Invalid version string.");
  280. if (v1 === v2) return 0;
  281. const parts1 = v1.split('.').map(Number);
  282. const parts2 = v2.split('.').map(Number);
  283. const len = Math.max(parts1.length, parts2.length);
  284. for (let i = 0; i < len; i++) {
  285. const num1 = parts1[i] ?? 0;
  286. const num2 = parts2[i] ?? 0;
  287. if (num1 > num2) return 1;
  288. if (num1 < num2) return -1;
  289. }
  290. return 0;
  291. } catch (error) {
  292. throw new Error("Error comparing versions: " + error);
  293. }
  294. }
  295.  
  296. function hasGreasyMonkeyAPI() {
  297. if (typeof GM !== 'undefined') return true;
  298. if (typeof GM_info !== 'undefined') {
  299. useCompatibilityMode = true;
  300. printDebug(console.warn, "Running in compatibility mode.");
  301. return true;
  302. }
  303. return false;
  304. }
  305.  
  306. function CheckTampermonkeyUpdated() {
  307. if (GM_info.scriptHandler === "Tampermonkey" &&
  308. compareVersions(GM_info.version, updatedVersions.Tampermonkey) !== 1) {
  309. isOldTampermonkey = true;
  310. if (isScriptRecentlyUpdated) {
  311. GMCustomNotification({
  312. text: GET_LOCALIZED_TEXT().tampermonkeyOutdatedAlertMessage,
  313. timeout: 15000
  314. });
  315. }
  316. }
  317. }
  318.  
  319. // -------------------------------
  320. // Storage helper functions
  321. // -------------------------------
  322.  
  323. /**
  324. * Load user settings from the "settings" key.
  325. * Ensures that only keys existing in DEFAULT_SETTINGS are kept.
  326. * If no stored settings are found, defaults are used.
  327. */
  328. async function loadUserSettings() {
  329. try {
  330. const storedSettings = await GMCustomGetValue('settings', {});
  331. userSettings = Object.keys(DEFAULT_SETTINGS).reduce((accumulator, key) => {
  332. accumulator[key] = storedSettings.hasOwnProperty(key) ? storedSettings[key] : DEFAULT_SETTINGS[key];
  333. return accumulator;
  334. }, {});
  335. await GMCustomSetValue('settings', userSettings);
  336. printDebug(`Loaded user settings: ${JSON.stringify(userSettings)}.`);
  337. } catch (error) {
  338. throw error;
  339. }
  340. }
  341.  
  342. // Update one setting in the stored settings.
  343. async function updateSetting(key, value) {
  344. try {
  345. let currentSettings = await GMCustomGetValue('settings', DEFAULT_SETTINGS);
  346. currentSettings[key] = value;
  347. await GMCustomSetValue('settings', currentSettings);
  348. } catch (error) {
  349. printDebug(console.error, "Error updating setting: ", error);
  350. }
  351. }
  352.  
  353. async function updateScriptInfo() {
  354. try {
  355. const oldScriptInfo = await GMCustomGetValue('scriptInfo', null);
  356. printDebug(`Previous script info: ${JSON.stringify(oldScriptInfo)}`);
  357. const newScriptInfo = {
  358. version: getScriptVersionFromMeta(),
  359. };
  360. await GMCustomSetValue('scriptInfo', newScriptInfo);
  361.  
  362. if (!oldScriptInfo || compareVersions(newScriptInfo.version, oldScriptInfo?.version) !== 0) {
  363. isScriptRecentlyUpdated = true;
  364. }
  365. printDebug(`Updated script info: ${JSON.stringify(newScriptInfo)}`);
  366. } catch (error) {
  367. printDebug(console.error, "Error updating script info: ", error);
  368. }
  369. }
  370.  
  371. // Cleanup any leftover keys from previous versions.
  372. async function cleanupOldStorage() {
  373. try {
  374. const allowedKeys = ['settings', 'scriptInfo'];
  375. const keys = await GMCustomListValues();
  376. for (const key of keys) {
  377. if (!allowedKeys.includes(key)) {
  378. await GMCustomDeleteValue(key);
  379. printDebug(`Deleted leftover key: ${key}`);
  380. }
  381. }
  382. } catch (error) {
  383. printDebug(console.error, "Error cleaning up old storage keys: ", error);
  384. }
  385. }
  386.  
  387. // -------------------------------
  388. // Script metadata extraction
  389. // -------------------------------
  390. function getScriptVersionFromMeta() {
  391. const meta = GM_info.scriptMetaStr;
  392. const versionMatch = meta?.match(/@version\s+([^\r\n]+)/);
  393. return versionMatch ? versionMatch[1].trim() : null;
  394. }
  395.  
  396. // -------------------------------
  397. // Main function: add event listeners and initialize
  398. // -------------------------------
  399. function addEventListeners() {
  400. if (window.location.hostname === "m.youtube.com") {
  401. window.addEventListener('state-navigateend', processVideoLoad, true);
  402. } else {
  403. window.addEventListener('yt-player-updated', processVideoLoad, true);
  404. }
  405. }
  406.  
  407. async function initialize() {
  408. try {
  409. if (!hasGreasyMonkeyAPI()) throw new Error("Did not detect valid Grease Monkey API");
  410. await cleanupOldStorage();
  411. await loadUserSettings();
  412. await updateScriptInfo();
  413. CheckTampermonkeyUpdated();
  414. } catch (error) {
  415. printDebug(console.error, `Error loading user settings: ${error}. Loading with default settings.`);
  416. }
  417.  
  418. window.addEventListener('pageshow', processVideoLoad, true);
  419. if (window.self === window.top) {
  420. addEventListeners();
  421. showMenuOptions();
  422. } else {
  423. isIframe = true;
  424. }
  425. }
  426.  
  427. // -------------------------------
  428. // Entry Point
  429. // -------------------------------
  430. initialize();
  431. })();