이 스크립트는 직접 설치하는 용도가 아닙니다. 다른 스크립트에서 메타 지시문 // @require https://update.greasyfork.org/scripts/549881/1664276/YouTube%20Helper%20API.js
을(를) 사용하여 포함하는 라이브러리입니다.
// ==UserScript==
// @name YouTube Helper API
// @author ElectroKnight22
// @namespace electroknight22_helper_api_namespace
// @version 0.0.8.1
// @license MIT
// @description YouTube Helper API.
// ==/UserScript==
/*jshint esversion: 11 */
window.youtubeHelperApi = (function () {
'use strict';
const player = {
playerObject: null,
api: null,
videoElement: null,
isFullscreen: false,
isTheater: 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,
};
const chat = {
container: null,
iFrame: null,
isCollapsed: false,
};
const page = {
manager: document.querySelector('.ytd-page-manager'),
isIframe: window.top !== window.self,
isMobile: window.location.hostname === 'm.youtube.com',
type: 'unknown',
};
let detectedAds = false;
function fallbackGetPlayerApi() {
updatePageType();
if (page.type === 'shorts') return document.querySelector('#shorts-player');
if (page.type === 'watch') return document.querySelector('#movie_player');
return document.querySelector('.inline-preview-player');
}
function getOptimalResolution(targetResolutionString, usePremium = true) {
const QUALITIES = {
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 PREMIUM_INDICATOR = 'Premium';
try {
if (!targetResolutionString || !QUALITIES[targetResolutionString])
throw new Error(`Invalid target resolution: ${targetResolutionString}`);
const videoQualityData = player.api.getAvailableQualityData();
const availableQualities = [...new Set(videoQualityData.map((q) => q.quality))];
const targetValue = QUALITIES[targetResolutionString].p;
const bestQualityString = availableQualities
.filter((q) => QUALITIES[q] && QUALITIES[q].p <= targetValue)
.sort((a, b) => QUALITIES[b].p - QUALITIES[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) {
player.api.setPlaybackQualityRange(targetResolution);
} else {
const optimalQuality = getOptimalResolution(targetResolution, usePremium);
if (optimalQuality)
player.api.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 = player.api.getAvailableAudioTracks();
if (availableTracks.length === 0) return;
const renderer = player.api.getPlayerResponse()?.captions?.playerCaptionsTracklistRenderer;
const originalAudioId = renderer?.audioTracks?.[renderer?.defaultAudioTrackIndex]?.audioTrackId;
const playingAudioTrack = player.api.getAudioTrack();
const originalAudioTrack = availableTracks.find((track) => getAudioTrackId(track) === originalAudioId);
video.playingLanguage = playingAudioTrack;
video.originalLanguage = originalAudioTrack;
}
function updateVideoState() {
if (!player.api) return;
const playerResponseObject = player.api.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('video');
}
function updateFullscreenState() {
player.isFullscreen = !!document.fullscreenElement;
}
function updateTheaterState(event) {
player.isTheater = !!event?.detail?.enabled;
}
function updateChatState(event) {
chat.iFrame = event.target ?? document.querySelector('ytd-watch-flexy');
chat.container = chat.iFrame?.parentElement ?? document.querySelector('#chat-container');
chat.isCollapsed = event.detail ?? true;
}
function updatePageType() {
const knownPagePathnames = {
homepage: '/',
profile: '/@',
watch: '/watch',
shorts: '/shorts',
live: '/live',
results: '/results',
chat: '/live_chat',
};
page.type =
knownPagePathnames[Object.keys(knownPagePathnames).find((key) => window.location.pathname.startsWith(knownPagePathnames[key]))];
}
function checkIsIframe() {
if (page.isIframe) {
document.dispatchEvent(new Event('yt-helper-api-detected-iframe'));
}
}
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);
}
}
function fallbackCheckAdPresense() {
if (!player.api) return;
try {
const progressState = player.api.getProgressState();
const reportedContentDuration = progressState.duration;
const realContentDuration = player.api.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;
}
}
function handlePlayerUpdate() {
try {
updatePageType();
updatePlayerState();
updateVideoState();
updateVideoLanguage();
fallbackCheckAdPresense()
dispatchHelperApiReadyEvent();
} catch (error) {
console.error('Error in handlePlayerUpdate:', error);
}
}
function handleNavigationFinish() {
updatePageType()
page.manager = document.querySelector('.ytd-page-manager');
}
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() {
checkIsIframe();
updatePlayerState();
addNavigationListeners();
addPlayerStateListeners();
addChatStateListeners();
}
initialize();
const publicApi = {
get player() { return { ...player }; },
get video() { return { ...video }; },
get chat() { return { ...chat }; },
get page() { return { ...page }; },
get isAdPlaying() { return detectedAds; },
checkAdPresense,
fallbackCheckAdPresense,
getOptimalResolution,
setPlaybackResolution,
};
return publicApi;
})();