This script should not be not be installed directly. It is a library for other scripts to include with the meta directive // @require https://update.greasyfork.org/scripts/549881/1665363/YouTube%20Helper%20API.js
// ==UserScript==
// @name YouTube Helper API
// @author ElectroKnight22
// @namespace electroknight22_helper_api_namespace
// @version 0.3.0
// @license MIT
// @description YouTube Helper API.
// ==/UserScript==
/*jshint esversion: 11 */
window.youtubeHelperApi = (function () {
('use strict');
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',
};
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' },
});
const apiProxy = new Proxy(
{},
{
get(target, property) {
return (...args) => {
if (player.api && typeof player.api[property] === 'function') {
return player.api[property](...args);
}
};
},
},
);
const player = {
playerObject: null,
api: null,
videoElement: null,
isFullscreen: false,
isTheater: false,
isPlayingAds: false,
};
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,
realCurrentProgress: -1, // YouTube can return the progress of the ad playing instead of the video content so we need implement our own progress tracking.
};
const chat = {
container: null,
iFrame: null,
isCollapsed: false,
};
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',
};
const currentlyObservedContainers = new WeakMap();
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);
}
function saveToStorage(storageKey, data) {
try {
localStorage.setItem(storageKey, JSON.stringify(data));
} catch (error) {
console.error(`Error saving data for key "${storageKey}":`, error);
}
}
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;
}
}
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;
}
}
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;
}
}
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);
}
}
function dispatchHelperApiReadyEvent() {
if (!player.api) return;
const event = new CustomEvent('yt-helper-api-ready', { detail: Object.freeze({ ...publicApi }) });
document.dispatchEvent(event);
}
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; // Either no alternative languages exist or YouTube's API failed.
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;
}
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;
}
function updatePlayerState(event) {
player.api = event?.target?.player_ ?? fallbackGetPlayerApi();
player.playerObject = event?.target?.playerContainer_?.children[0] ?? fallbackGetPlayerApi();
player.videoElement = player.playerObject?.querySelector(SELECTORS.videoElement);
}
function updateFullscreenState() {
player.isFullscreen = !!document.fullscreenElement;
}
function updateTheaterState(event) {
player.isTheater = !!event?.detail?.enabled;
}
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 }) }));
}
function checkIsIframe() {
if (page.isIframe) document.dispatchEvent(new Event('yt-helper-api-detected-iframe'));
}
function notifyAdState() {
if (player.isPlayingAds) document.dispatchEvent(
new CustomEvent('yt-helper-api-ad-detected', { detail: Object.freeze({ isPlayingAds: player.isPlayingAds }) }),
);
}
function updateAdState() {
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');
const isPlayingAds = !shouldAvoid && isAdPresent;
player.isPlayingAds = isPlayingAds;
notifyAdState();
} catch (error) {
console.error('Error in checkAdState:', error);
return false;
}
}
function fallbackUpdateAdState() {
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 isPlayingAds = durationMismatch;
player.isPlayingAds = isPlayingAds;
notifyAdState();
} catch (error) {
console.error('Error during ad check:', error);
return false;
}
}
function reloadToCurrentProgress() {
if (!player.api) return
apiProxy.loadVideoById(video.id, Math.max(0, video.realCurrentProgress));
}
function trackPlaybackProgress() {
if (!player.videoElement) return;
const updateProgress = () => {
if (!player.isPlayingAds && player.videoElement.currentTime > 0) video.realCurrentProgress = player.videoElement.currentTime;
};
player.videoElement.addEventListener('timeupdate', updateProgress);
}
function trackAdState() {
if (!player.container) return;
if (currentlyObservedContainers.has(player.container)) return;
const adStateObserver = new MutationObserver(updateAdState);
adStateObserver.observe(player.container, { attributes: true, attributeFilter: ['class'] });
currentlyObservedContainers.set(player.container, adStateObserver);
}
function handlePlayerUpdate(event = null) {
updatePlayerState(event);
updateVideoState();
updateVideoLanguage();
updateAdState();
trackAdState();
trackPlaybackProgress();
dispatchHelperApiReadyEvent();
}
function handleNavigationFinish(event) {
page.type = event?.detail?.pageType;
page.manager = document.querySelector(SELECTORS.pageManager);
page.watchFlexy = document.querySelector(SELECTORS.watchFlexy);
}
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);
}
function addChatStateListeners() {
document.addEventListener('yt-chat-collapsed-changed', updateChatState);
}
function addNavigationListeners() {
document.addEventListener('yt-navigate-finish', handleNavigationFinish);
}
function initialize() {
window.addEventListener('pageshow', handlePlayerUpdate);
checkIsIframe();
addNavigationListeners();
addPlayerStateListeners();
addChatStateListeners();
}
initialize();
const publicApi = {
get player() {
return { ...player };
},
get video() {
return { ...video };
},
get chat() {
return { ...chat };
},
get page() {
return { ...page };
},
POSSIBLE_RESOLUTIONS,
updateAdState,
fallbackUpdateAdState,
getOptimalResolution,
setPlaybackResolution,
saveToStorage,
loadFromStorage,
loadAndCleanFromStorage,
reloadToCurrentProgress,
apiProxy,
};
return publicApi;
})();