// ==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 2024.11.12.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 videoId = '';
let resolvedTarget = '';
let targetLabel = '';
let ytPlayer = document.getElementById("movie_player") || document.getElementsByClassName("html5-video-player")[0];
let video = document.querySelector('video');
// --- 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 {
checkVideoValid(force);
resolvedTarget = findNextAvailableQuality(userSettings.targetResolution, ytPlayer.getAvailableQualityLevels());
const premiumData = ytPlayer.getAvailableQualityData().find(q => q.quality === resolvedTarget && q.qualityLabel.trim().endsWith(PREMIUM_INDICATOR_LABEL) && q.isPlayable);
ytPlayer.setPlaybackQualityRange(resolvedTarget, resolvedTarget, premiumData?.formatId);
targetLabel = QUALITIES[resolvedTarget] + "p" + (premiumData ? " " + PREMIUM_INDICATOR_LABEL : "");
videoId = ytPlayer.getVideoData().video_id;
debugLog(`Setting quality to: ${targetLabel}`);
} catch (error) {
debugLog("Did not set resolution. " + error);
}
}
function checkVideoValid(force) {
try {
if (/^https?:\/\/(www\.)?youtube\.com\/shorts\//.test(window.location.href)) {
throw new AllowedExceptionError("Skipping Youtube Shorts.");
}
if (!ytPlayer?.getAvailableQualityLabels().length) {
throw "Video data missing.";
}
if (videoId == ytPlayer.getVideoData().video_id && !force) {
throw new AllowedExceptionError("Duplicate Load. Skipping.");
}
} catch (error) {
throw 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 compareQualityLabelButIgnoreFrameRate(label1, label2) {
const normalize = label => label.replace(new RegExp(`(\\d+p)\\d*( ${PREMIUM_INDICATOR_LABEL})?$`), '$1').trim();
return normalize(label1) === normalize(label2);
}
// Tries to validate setting changes. If this fails to detect a successful update it will call setResolution to attempt to force the playback quality to update again.
function verifyChange(retries = 10) {
const retryDelay = 500;
try {
checkVideoValid(true);
let success = ytPlayer.getPreferredQuality() !== "auto";
let currentQualityLabel = ytPlayer.getPlaybackQualityLabel();
let isManualChange = !compareQualityLabelButIgnoreFrameRate(currentQualityLabel, targetLabel);
if (success) {
debugLog(`Quality is ${currentQualityLabel}. ${isManualChange ? "Manual change detected!" : "Change verified!"}`);
} else {
throw "Unable to verify change";
}
} catch (error) {
if (error instanceof AllowedExceptionError) {
debugLog(error);
} else if (retries) {
debugLog(`Error when verifying quality change. [${error}] Attempting quality change and reverifying after ${retryDelay}ms...`);
setResolution(true);
setTimeout(() => verifyChange(retries - 1), retryDelay);
} else {
debugLog(`Cannot verify quality change after retrying.`);
}
}
}
function processNewVideo() {
try {
video?.removeEventListener('playing', verifyChange, true);
videoId = '';
ytPlayer = document.getElementById("movie_player") || document.getElementsByClassName("html5-video-player")[0] || ytPlayer;
video = ytPlayer?.querySelector('video') || document.querySelector('video');
if (!video) throw "Video element not found.";
video.addEventListener('playing', verifyChange, true);
setResolution();
} catch (error) {
debugLog("Load error: " + error);
}
}
// ----------------------------------------
// Functions for the quality selection menu
function removeMenuOptions() {
while (menuItems.length) {
GMCustomUnregisterMenuCommand(menuItems.pop());
}
}
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(true);
showMenuOptions();
}
}))
},
debug: {
label: () => `${GET_LOCALIZED_TEXT().debug} ${userSettings.debug ? "✅" : ""}`,
menuId: "debugBtn",
handleClick: function() {
userSettings.debug = !userSettings.debug;
GMCustomSetValue('debug', userSettings.debug);
showMenuOptions();
}
}
}
function registerItem(item) {
GMCustomRegisterMenuCommand(item.label(), () => {
item.handleClick();
}, {
id: item.menuId,
autoClose: false
});
menuItems.push(item.menuId);
}
Object.values(menuOptions).forEach(option => {
if (!option.alwaysShow && !userSettings.expandMenu) return;
if (option.items){
option.items.forEach(item => { registerItem(item); });
return;
}
registerItem(option);
});
}
// -----------------------------------------------
// 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() {
// 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(", ")}].`);
}
// ----------------
// 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.`);
}
window.addEventListener('popstate', () => { videoId = ''; });
if (window.self == window.top) {
// handle youtube website both desktop and mobile version
if (window.location.hostname === "m.youtube.com") {
window.addEventListener('loadstart', processNewVideo, true );
} else {
window.addEventListener("yt-navigate-finish", processNewVideo, true );
}
showMenuOptions();
} else {
// handle iframes
window.addEventListener('load', processNewVideo, true );
}
}
// Entry Point
initialize();
})();