您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
YouTube Helper API.
当前为
此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.greasyfork.icu/scripts/549881/1664260/YouTube%20Helper%20API.js
// ==UserScript== // @name YouTube Helper API // @author ElectroKnight22 // @namespace electroknight22_helper_api_namespace // @version 0.0.6 // @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; // Pass the youtube player to consumer scripts with a custom event. const event = new CustomEvent('yt-api-helper-api-ready', { detail: Object.freeze({ ...player }) }); document.dispatchEvent(event); } function updateVideoLanguage() { 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.getPlayerResponse()?.captions?.playerCaptionsTracklistRenderer; const originalAudioId = renderer?.audioTracks?.[renderer?.defaultAudioTrackIndex]?.audioTrackId; const playingAudioTrack = player.getAudioTrack(); const originalAudioTrack = availableTracks.find((track) => getAudioTrackId(track) === originalAudioId); video.playingLanguage = playingAudioTrack; video.originalLanguage = originalAudioTrack; } function updatePlayerState(event) { player.api = event?.target?.player_ ?? fallbackGetPlayerApi(); player.playerObject = event?.target?.playerContainer_?.children[0] ?? fallbackGetPlayerApi(); player.videoElement = player.playerObject?.querySelector('video'); 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; updateVideoLanguage(); fallbackCheckAdPresense(); dispatchHelperApiReadyEvent(); // Dispatch an event so consumer scripts can react. } 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() { 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 addPlayerStateListeners() { const PLAYER_UPDATE_EVENT = page.isMobile ? 'state-navigateend' : 'yt-player-updated'; document.addEventListener(PLAYER_UPDATE_EVENT, updatePlayerState); 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', updatePageType); } function deepFreeze(object) { const propNames = Object.getOwnPropertyNames(object); for (const name of propNames) { const value = object[name]; if (value && typeof value === 'object') { deepFreeze(value); } } return Object.freeze(object); } function initialize() { checkIsIframe(); updatePlayerState(); addNavigationListeners(); addPlayerStateListeners(); addChatStateListeners(); } initialize(); const publicApi = { get player() { return deepFreeze({ ...player }); }, get video() { return deepFreeze({ ...video }); }, get chat() { return deepFreeze({ ...chat }); }, get page() { return deepFreeze({ ...page }); }, get isAdPlaying() { return detectedAds; }, checkAdPresense, fallbackCheckAdPresense, getOptimalResolution, setPlaybackResolution, }; return publicApi; })();