// ==UserScript==
// @name Youtube HD Premium
// @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAAqhJREFUaEPtmc9rE0EUxz+DjSh6UAQRxP4F9uhBRKjipef+FwqtoZdYEk3U4jGn0FJ6KrQnj6X0EKVKKIi9tAotPZSCYilFoq0/sK1Z92V329XGENiZSRZ2LtllZ9+8z/e9ncy8UcS8qZj7TwLQ7ggmEUgiEFGB/6aQAxeBq8Al4GxonDPAydD9+dB1qkFfefy9iZ9fgRrwC/jh96v6vz+Bj8B7BduNbBwDcOA6UABuAyciCqTr9d/ACxf0oYI3YaOHAA71KfWpq8QDF6BTP27H9/GRArk+ctSBZ0BGl2SG7YwoyB4COF66lDtY+X/1EPVvKXhVTxUHKsANw6rpNl9RcFM50A1sxEj9QAiJQrcA9LvT5XPd8liy1y8Ad4GSpQF1D3NPAO4DRd2WLdlL6wUYH4dKBSYnLfmPZoDZWejrg/l5GByE5WXTIIYAxO1aDaamYGgIthsuY3TAGQQI3KtWoVCAUgkODnQ4HbZhASAYbnUV0mmYm9MJYREgcHtmxvs+1td1gLQBQNze24OxMchmYXc3CkibAOQDl6k2k4GtrZgBLC56KbSwEMXx4F2LEdjchHweJia8KVZPswCwvw+jo5DLwc6OHrePrBgGKJdhYABWVnQ7bjiF1ta8OV+WFmab5ghMT8PSEhSL3lRpvmkGSKVAct5eqwPEfkMT+y3lZeBDbDf1kq6xLqv4AL3AyxhFQUoqvQpeh2ujI+46cdjeBBJppL9Li34UBCYP5Do4ErKIeiLV82PF3UAPB64Bj4E7biW4K5JO+l6WvajUbqW8/jZsttkBxwWgB7gCnPZfCg4z5P6UH6lzTfyUgxGp7ctBRdBkBxNsjiWXv4Seyd93+DDkG/AJeKfgc6NxOvUcoOXYJQAtS2WoYxIBQ8K2bDaJQMtSGer4B8aT1sve/dr7AAAAAElFTkSuQmCC
// @author ElectroKnight22
// @namespace electroknight22_youtube_hd_namespace
// @version 2024.07.02
// @match *://www.youtube.com/*
// @grant GM.getValue
// @grant GM.setValue
// @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のビットレートを優先的に選択します。
// ==/UserScript==
/*jshint esversion: 11 */
(function() {
"use strict";
// --- SETTINGS -------
// !!! PLEASE NOTE:
// !!! Having the incorrect "overwriteStoredSettings" value maybe cause your settings to not save.
let settings = {
// true = use the setting here. Use this when changing settings here.
// false = use stored setting. Use this when changing settings from the "Storage" tab.
overwriteStoredSettings: false,
// true = use the setting you last chose on YouTube.
// false = ignore YouTube settings.
importYoutubeSettings: false,
// The target resolution. If not available, the next best available resolution will be used.
targetRes: "hd2160"
// Choices for targetRes are currently:
// "highres" >= ( 8K / 4320p / QUHD )
// "hd2880" = ( 5K / 2880p / UHD+ )
// "hd2160" = ( 4K / 2160p / UHD )
// "hd1440" = ( 1440p / QHD )
// "hd1080" = ( 1080p / FHD )
// "hd720" = ( 720p / HD )
// "large" = ( 480p )
// "medium" = ( 360p )
// "small" = ( 240p )
// "tiny" = ( 144p )
// "auto" = ( auto )
};
// --------------------
// --- GLOBALS --------
// --------------------
const DEBUG = false;
const resolutions = ['highres', 'hd2880', 'hd2160', 'hd1440', 'hd1080', 'hd720', 'large', 'medium', 'small', 'tiny', 'auto'];
const quality = {
highres: 4320,
hd2880: 2880,
hd2160: 2160,
hd1440: 1440,
hd1080: 1080,
hd720: 720,
large: 480,
medium: 360,
small: 240,
tiny: 144,
auto: 0
};
const qualityLevels = Object.fromEntries(
Object.entries(quality).map(([key, value]) => [value, key])
);
let doc = document, win = window;
let vidId = null;
// --------------------
// --- FUNCTIONS ------
// --------------------
function debugLog(message, shouldShow = true) {
if (DEBUG && shouldShow) {
console.log("YTHD DEBUG | " + message);
}
}
// --------------------
// Attempt to set the video resolution to target quality or the next best quality
function setResolution(target) {
let ytPlayer = doc.getElementById("movie_player") || doc.getElementsByClassName("html5-video-player")[0];
if (!isValidVideo(ytPlayer)) return;
vidId = ytPlayer.getVideoData().video_id;
let localItem = localStorage.getItem("yt-player-quality");
if (localItem && settings.importYoutubeSettings) {
let youtubeSettings = fetchLocalSettings(localItem);
if (qualityLevels[youtubeSettings.quality]) {
target = qualityLevels[youtubeSettings.quality];
}
}
let limitList = ytPlayer.getAvailableQualityLevels();
let limit = limitList[0];
if (quality[target] > quality[limit]) {
target = limit;
}
let premiumIndicator = "Premium";
let premiumData = ytPlayer.getAvailableQualityData().find(q => q.quality == target && q.qualityLabel.includes(premiumIndicator) && q.isPlayable);
ytPlayer.setPlaybackQualityRange(target, target, premiumData?.formatId);
debugLog("Set quality to: " + target + (premiumData ? " Premium" : ""));
if (localItem){
localStorage.setItem("yt-player-quality",localItem);
}
else {
localStorage.removeItem("yt-player-quality");
}
}
function isValidVideo(ytPlayer) {
if (!ytPlayer?.getAvailableQualityLabels()[0]) {
debugLog("Video data missing");
return false;
}
if (vidId == ytPlayer.getVideoData().video_id) {
debugLog("Duplicate load");
return false;
}
return true;
}
function main() {
let target = settings.targetRes.toLowerCase();
if (target != 'auto') {
setResolution(target);
win.addEventListener("loadstart", () => { setResolution(target); }, true);
}
}
function fetchLocalSettings(localItem) {
let quality = 0;
let previousQuality = 0;
let expiration = 0;
let creation = 0;
if (localItem) {
localItem = JSON.parse(localItem);
let localData = JSON.parse(localItem.data);
quality = localData.quality;
previousQuality = localData.previousQuality;
expiration = localItem.expiration;
creation = localItem.creation;
}
return { quality, previousQuality, expiration, creation };
}
async function applySettings() {
if (typeof GM != 'undefined' && GM.getValue && GM.setValue && GM.deleteValue && GM.listValues) {
if (settings.overwriteStoredSettings) {
// Clear all values from GM
const keys = await GM.listValues();
await Promise.all(keys.map(key => GM.deleteValue(key)));
// Write each key-value pair from settings
await Promise.all(Object.entries(settings).map(([k, v]) => GM.setValue(k, v)));
} else {
// Get all keys from GM
const gmKeys = await GM.listValues();
// Write any missing key-value pairs from settings to GM
await Promise.all(Object.entries(settings).map(async ([k, v]) => {
if (!gmKeys.includes(k)) {
await GM.setValue(k, v);
}
}));
// Delete any extra keys in GM that are not in settings
await Promise.all(gmKeys.map(async key => {
if (!(key in settings)) {
await GM.deleteValue(key);
}
}));
// Update the value for .overwriteStoredSettings in GM
await GM.setValue('overwriteStoredSettings', settings.overwriteStoredSettings);
// Retrieve and update settings from GM
await Promise.all(
gmKeys.map(k => GM.getValue(k).then(v => [k, v]))
).then(c => c.forEach(([nk, nv]) => {
if (settings[nk] !== null && nk !== "overwriteStoredSettings") {
settings[nk] = nv;
}
}));
}
debugLog(Object.entries(settings).map(([k, v]) => k + ": " + v).join(", "));
}
}
applySettings().then(main);
})();