Este script no debería instalarse directamente. Es una biblioteca que utilizan otros scripts mediante la meta-directiva de inclusión // @require https://update.greasyfork.org/scripts/549881/1664588/YouTube%20Helper%20API.js
// ==UserScript==
// @name YouTube Helper API
// @author ElectroKnight22
// @namespace electroknight22_helper_api_namespace
// @version 0.2.0
// @license MIT
// @description YouTube Helper API.
// ==/UserScript==
/*jshint esversion: 11 */
/**
* @namespace youtubeHelperApi
* @description A comprehensive helper API for interacting with the YouTube player and page.
* It provides a stable interface for accessing player state, video data, and controlling playback.
*/
window.youtubeHelperApi = (function () {
('use strict');
/**
* @property {Object} SELECTORS - A centralized object for all CSS selectors used in the script.
* @private
*/
const SELECTORS = {
pageManager: 'ytd-page-manager',
shortsPlayer: '#shorts-player',
watchPlayer: '#movie_player',
inlinePlayer: '.inline-preview-player',
videoElement: 'video',
watchFlexy: 'ytd-watch-flexy',
chatFrame: 'ytd-live-chat-frame#chat',
chatContainer: '#chat-container',
};
/**
* @property {Object} POSSIBLE_RESOLUTIONS - A manually updated map of all the possible YouTube's resolution options.
* @public
*/
const POSSIBLE_RESOLUTIONS = Object.freeze({
highres: { p: 4320, label: '8K' },
hd2160: { p: 2160, label: '4K' },
hd1440: { p: 1440, label: '1440p' },
hd1080: { p: 1080, label: '1080p' },
hd720: { p: 720, label: '720p' },
large: { p: 480, label: '480p' },
medium: { p: 360, label: '360p' },
small: { p: 240, label: '240p' },
tiny: { p: 144, label: '144p' },
});
/**
* @property {Proxy} apiProxy - A safe proxy to interact with YouTube's internal player API.
* Prevents errors when the API or its methods are not available.
* @private
*/
const apiProxy = new Proxy(
{},
{
get(target, property) {
return (...args) => {
if (player.api && typeof player.api[property] === 'function') {
return player.api[property](...args);
}
};
},
},
);
/**
* @property {Object} player - Internal state object for the YouTube player.
* @property {Object|null} player.playerObject - The root HTML element of the player.
* @property {Object|null} player.api - The internal YouTube player API object.
* @property {HTMLVideoElement|null} player.videoElement - The <video> element.
* @property {boolean} player.isFullscreen - Whether the player is in fullscreen mode.
* @property {boolean} player.isTheater - Whether the player is in theater mode.
* @private
*/
const player = {
playerObject: null,
api: null,
videoElement: null,
isFullscreen: false,
isTheater: false,
};
/**
* @property {Object} video - Internal state object for the current video's data.
* @private
*/
const video = {
id: '',
title: '',
channel: '',
channelId: '',
rawDescription: '',
rawUploadDate: '',
rawPublishDate: '',
uploadDate: null,
publishDate: null,
lengthSeconds: 0,
viewCount: 0,
likeCount: 0,
isLive: false,
isFamilySafe: false,
thumbnails: [],
playingLanguage: null,
originalLanguage: null,
};
/**
* @property {Object} chat - Internal state object for the live chat component.
* @private
*/
const chat = {
container: null,
iFrame: null,
isCollapsed: false,
};
/**
* @property {Object} page - Internal state object for the current YouTube page.
* @private
*/
const page = {
manager: document.querySelector(SELECTORS.pageManager),
watchFlexy: document.querySelector(SELECTORS.watchFlexy),
isIframe: window.top !== window.self,
isMobile: window.location.hostname === 'm.youtube.com',
type: 'unknown',
};
let detectedAds = false;
/**
* @description Finds the current YouTube player element on the page as a fallback.
* @returns {HTMLElement|null} The player element or null if not found.
* @private
*/
function fallbackGetPlayerApi() {
const pathname = window.location.pathname;
if (pathname.startsWith('/shorts')) return document.querySelector(SELECTORS.shortsPlayer);
if (pathname.startsWith('/watch')) return document.querySelector(SELECTORS.watchPlayer);
return document.querySelector(SELECTORS.inlinePlayer);
}
/**
* @description Saves data to localStorage.
* @param {string} storageKey - The key to save the data under.
* @param {*} data - The data to be saved (will be JSON stringified).
*/
function saveToStorage(storageKey, data) {
try {
localStorage.setItem(storageKey, JSON.stringify(data));
} catch (error) {
console.error(`Error saving data for key "${storageKey}":`, error);
}
}
/**
* @description Loads data from localStorage, merging with default data.
* @param {string} storageKey - The key to load data from.
* @param {Object} defaultData - Default data to return if loading fails or key doesn't exist.
* @returns {Object} The loaded and merged data.
*/
function loadFromStorage(storageKey, defaultData) {
try {
const storedData = JSON.parse(localStorage.getItem(storageKey));
return { ...defaultData, ...storedData };
} catch (error) {
console.error(`Error loading data for key "${storageKey}":`, error);
return defaultData;
}
}
/**
* @description Loads data from localStorage and cleans it, ensuring only keys present in defaultData are kept.
* @param {string} storageKey - The key to load data from.
* @param {Object} defaultData - The object defining the desired shape of the loaded data.
* @returns {Object} The loaded and cleaned data.
*/
function loadAndCleanFromStorage(storageKey, defaultData) {
try {
const storedData = JSON.parse(localStorage.getItem(storageKey)) || {};
const combinedData = { ...defaultData, ...storedData };
const cleanedData = Object.keys(defaultData).reduce((accumulator, currentKey) => {
accumulator[currentKey] = combinedData[currentKey];
return accumulator;
}, {});
return cleanedData;
} catch (error) {
console.error(`Error loading and cleaning data for key "${storageKey}":`, error);
return defaultData;
}
}
/**
* @description Calculates the best playable quality based on a target resolution string and the actual available resolutions.
* @param {string} targetResolutionString - The desired quality (e.g., 'hd1080', 'large').
* @param {boolean} [usePremium=true] - Whether to use premium bitrate versions if available.
* @returns {Object|null} The optimal quality data object from the player API, or null.
*/
function getOptimalResolution(targetResolutionString, usePremium = true) {
const PREMIUM_INDICATOR = 'Premium';
try {
if (!targetResolutionString || !POSSIBLE_RESOLUTIONS[targetResolutionString])
throw new Error(`Invalid target resolution: ${targetResolutionString}`);
const videoQualityData = apiProxy.getAvailableQualityData();
const availableQualities = [...new Set(videoQualityData.map((q) => q.quality))];
const targetValue = POSSIBLE_RESOLUTIONS[targetResolutionString].p;
const bestQualityString = availableQualities
.filter((q) => POSSIBLE_RESOLUTIONS[q] && POSSIBLE_RESOLUTIONS[q].p <= targetValue)
.sort((a, b) => POSSIBLE_RESOLUTIONS[b].p - POSSIBLE_RESOLUTIONS[a].p)[0];
if (!bestQualityString) return null;
let normalCandidate = null;
let premiumCandidate = null;
for (const quality of videoQualityData) {
if (quality.quality === bestQualityString && quality.isPlayable) {
if (usePremium && quality.qualityLabel?.trim().endsWith(PREMIUM_INDICATOR)) premiumCandidate = quality;
else normalCandidate = quality;
}
}
return premiumCandidate || normalCandidate;
} catch (error) {
console.error('Error when resolving optimal quality:', error);
return null;
}
}
/**
* @description Sets the video playback quality.
* @param {string} targetResolution - The desired quality string (e.g., 'hd1080').
* @param {boolean} [ignoreAvailable=false] - If true, forces the quality without checking availability.
* @param {boolean} [usePremium=true] - Whether to use premium bitrate versions if available.
*/
function setPlaybackResolution(targetResolution, ignoreAvailable = false, usePremium = true) {
try {
if (!player.api?.getAvailableQualityData) return;
if (!usePremium && ignoreAvailable) {
apiProxy.setPlaybackQualityRange(targetResolution);
} else {
const optimalQuality = getOptimalResolution(targetResolution, usePremium);
if (optimalQuality)
apiProxy.setPlaybackQualityRange(
optimalQuality.quality,
optimalQuality.quality,
usePremium ? optimalQuality.formatId : null,
);
}
} catch (error) {
console.error('Error when setting resolution:', error);
}
}
/**
* @description Dispatches a custom event to signal that the API is ready and has updated player data.
* @private
*/
function dispatchHelperApiReadyEvent() {
if (!player.api) return;
const event = new CustomEvent('yt-helper-api-ready', { detail: Object.freeze({ ...publicApi }) });
document.dispatchEvent(event);
}
/**
* @description Updates the video language states based on available and playing audio tracks.
* @private
*/
function updateVideoLanguage() {
if (!player.api) return;
const getAudioTrackId = (track) => Object.values(track ?? {}).find((p) => p?.id)?.id ?? null;
const availableTracks = apiProxy.getAvailableAudioTracks();
if (availableTracks.length === 0) return;
const renderer = apiProxy.getPlayerResponse()?.captions?.playerCaptionsTracklistRenderer;
const originalAudioId = renderer?.audioTracks?.[renderer?.defaultAudioTrackIndex]?.audioTrackId;
const playingAudioTrack = apiProxy.getAudioTrack();
const originalAudioTrack = availableTracks.find((track) => getAudioTrackId(track) === originalAudioId);
video.playingLanguage = playingAudioTrack;
video.originalLanguage = originalAudioTrack;
}
/**
* @description Updates the internal video state object with the latest data from the player response.
* @private
*/
function updateVideoState() {
if (!player.api) return;
const playerResponseObject = apiProxy.getPlayerResponse();
video.id = playerResponseObject?.videoDetails?.videoId;
video.title = playerResponseObject?.videoDetails?.title;
video.channel = playerResponseObject?.videoDetails?.author;
video.channelId = playerResponseObject?.videoDetails?.channelId;
video.rawDescription = playerResponseObject?.videoDetails?.shortDescription;
video.rawUploadDate = playerResponseObject?.microformat?.playerMicroformatRenderer?.uploadDate;
video.rawPublishDate = playerResponseObject?.microformat?.playerMicroformatRenderer?.publishDate;
video.uploadDate = video.rawUploadDate ? new Date(video.rawUploadDate) : null;
video.publishDate = video.rawPublishDate ? new Date(video.rawPublishDate) : null;
video.lengthSeconds = parseInt(playerResponseObject?.videoDetails?.lengthSeconds ?? '0', 10);
video.viewCount = parseInt(playerResponseObject?.videoDetails?.viewCount ?? '0', 10);
video.likeCount = parseInt(playerResponseObject?.microformat?.playerMicroformatRenderer?.likeCount ?? '0', 10);
video.isLive = playerResponseObject?.videoDetails?.isLiveContent;
video.isFamilySafe = playerResponseObject?.microformat?.playerMicroformatRenderer?.isFamilySafe;
video.thumbnails = playerResponseObject?.microformat?.playerMicroformatRenderer?.thumbnail?.thumbnails;
}
/**
* @description Updates the internal player state object.
* @param {Event|null} event - The event that triggered the update (e.g., 'yt-player-updated').
* @private
*/
function updatePlayerState(event) {
player.api = event?.target?.player_ ?? fallbackGetPlayerApi();
player.playerObject = event?.target?.playerContainer_?.children[0] ?? fallbackGetPlayerApi();
player.videoElement = player.playerObject?.querySelector(SELECTORS.videoElement);
}
/**
* @description Updates fullscreen state based on the document's fullscreen element.
* @private
*/
function updateFullscreenState() {
player.isFullscreen = !!document.fullscreenElement;
}
/**
* @description Updates theater mode state from a custom YouTube event.
* @param {CustomEvent} event - The 'yt-set-theater-mode-enabled' event.
* @private
*/
function updateTheaterState(event) {
player.isTheater = !!event?.detail?.enabled;
}
/**
* @description Updates the chat state from a custom YouTube event.
* @param {CustomEvent} event - The 'yt-chat-collapsed-changed' event.
* @private
*/
function updateChatState(event) {
chat.iFrame = event?.target ?? document.querySelector(SELECTORS.chatFrame);
chat.container = chat.iFrame?.parentElement ?? document.querySelector(SELECTORS.chatContainer);
chat.isCollapsed = event?.detail ?? true;
document.dispatchEvent(new CustomEvent('yt-helper-api-chat-state-updated', { detail: Object.freeze({ ...chat }) }));
}
/**
* @description Checks if the script is running inside an iframe and dispatches an event if so.
* @private
*/
function checkIsIframe() {
if (page.isIframe) document.dispatchEvent(new Event('yt-helper-api-detected-iframe'));
}
/**
* @description Checks for ad presence by inspecting CSS classes on the player container.
* @returns {boolean|undefined} True if ad is present, otherwise false.
*/
function checkAdPresense() {
try {
const shouldAvoid = player.container.classList.contains('unstarted-mode'); // YouTube doesn't update ad state fully until player is marked as started.
const isAdPresent = player.container.classList.contains('ad-showing') || player.container.classList.contains('ad-interrupting');
return !shouldAvoid && isAdPresent;
} catch (error) {
console.error('Error in checkAdState:', error);
return false;
}
}
/**
* @description A fallback method to check for ads by comparing video duration from different API sources.
* @returns {boolean} True if an ad is detected.
*/
function fallbackCheckAdPresense() {
if (!player.api) return;
try {
const progressState = apiProxy.getProgressState();
const reportedContentDuration = progressState.duration;
const realContentDuration = apiProxy.getDuration() ?? -1;
const durationMismatch = Math.trunc(realContentDuration) !== Math.trunc(reportedContentDuration);
const hasAds = durationMismatch;
if (hasAds) document.dispatchEvent(new CustomEvent('yt-helper-api-ad-detected'));
if (hasAds !== detectedAds)
document.dispatchEvent(new CustomEvent('yt-helper-api-ad-state-changed', { detail: Object.freeze({ adState: hasAds }) }));
detectedAds = hasAds;
return detectedAds;
} catch (error) {
console.error('Error during ad check:', error);
return false;
}
}
/**
* @description Main handler for player updates. Triggers all necessary state update functions.
* @param {Event|null} [event=null] - The event that triggered the update.
* @private
*/
function handlePlayerUpdate(event = null) {
updatePlayerState(event);
updateVideoState();
updateVideoLanguage();
fallbackCheckAdPresense();
dispatchHelperApiReadyEvent();
}
/**
* @description Handler for the 'yt-navigate-finish' event. Updates page state.
* @param {CustomEvent} event - The navigation event.
* @private
*/
function handleNavigationFinish(event) {
page.type = event?.detail?.pageType;
page.manager = document.querySelector(SELECTORS.pageManager);
page.watchFlexy = document.querySelector(SELECTORS.watchFlexy);
}
/**
* @description Adds event listeners related to the player state.
* @private
*/
function addPlayerStateListeners() {
const PLAYER_UPDATE_EVENT = page.isMobile ? 'state-navigateend' : 'yt-player-updated';
document.addEventListener(PLAYER_UPDATE_EVENT, handlePlayerUpdate);
document.addEventListener('fullscreenchange', updateFullscreenState);
document.addEventListener('yt-set-theater-mode-enabled', updateTheaterState);
}
/**
* @description Adds event listeners related to the chat state.
* @private
*/
function addChatStateListeners() {
document.addEventListener('yt-chat-collapsed-changed', updateChatState);
}
/**
* @description Adds event listeners related to page navigation.
* @private
*/
function addNavigationListeners() {
document.addEventListener('yt-navigate-finish', handleNavigationFinish);
}
/**
* @description Initializes the script by setting up all event listeners.
* @private
*/
function initialize() {
window.addEventListener('pageshow', handlePlayerUpdate);
checkIsIframe();
addNavigationListeners();
addPlayerStateListeners();
addChatStateListeners();
}
initialize();
const publicApi = {
/** @type {Object} A read-only copy of the player state object. */
get player() {
return { ...player };
},
/** @type {Object} A read-only copy of the video state object. */
get video() {
return { ...video };
},
/** @type {Object} A read-only copy of the chat state object. */
get chat() {
return { ...chat };
},
/** @type {Object} A read-only copy of the page state object. */
get page() {
return { ...page };
},
/** @type {boolean} A read-only flag indicating if an ad has been detected. */
get isAdPlaying() {
return detectedAds;
},
POSSIBLE_RESOLUTIONS,
checkAdPresense,
fallbackCheckAdPresense,
getOptimalResolution,
setPlaybackResolution,
saveToStorage,
loadFromStorage,
loadAndCleanFromStorage,
apiProxy,
};
return publicApi;
})();