您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
A helper api for YouTube scripts that provides easy and consistent access for commonly needed functions, objects, and values.
当前为
此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/549881/1668694/YouTube%20Helper%20API.js
// ==UserScript== // @name YouTube Helper API // @author ElectroKnight22 // @namespace electroknight22_helper_api_namespace // @version 0.4.8 // @license MIT // @description A helper api for YouTube scripts that provides easy and consistent access for commonly needed functions, objects, and values. // ==/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. isTimeSpecified: false, isInPlaylist: false, playlistId: '', }; 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 localStorageApi = { get: (key, defaultValue) => { const value = localStorage.getItem(key); if (value === null) return defaultValue; try { return JSON.parse(value); } catch (error) { console.error(`Error parsing JSON for key "${key}":`, error); return value; } }, set: (key, value) => { localStorage.setItem(key, JSON.stringify(value)); }, }; const storageApi = (() => { const gmType = (() => { if (typeof GM !== 'undefined') { return 'modern'; } if (typeof GM_info !== 'undefined') { return 'old'; } return 'none'; })(); switch (gmType) { case 'modern': return { getValue: async (...args) => await GM.getValue(...args), setValue: async (...args) => await GM.setValue(...args), deleteValue: async (...args) => await GM.deleteValue(...args), listValues: async (...args) => await GM.listValues(...args), }; case 'old': return { getValue: async (key, defaultValue) => GM_getValue(key, defaultValue), setValue: async (key, value) => GM_setValue(key, value), deleteValue: async (key) => GM_deleteValue(key), listValues: async () => GM_listValues(), }; case 'none': default: return { getValue: async (key, defaultValue) => localStorageApi.get(key, defaultValue), setValue: async (key, value) => localStorageApi.set(key, value), deleteValue: async (key) => localStorage.removeItem(key), listValues: async () => Object.keys(localStorage), }; } })(); async function saveToStorage(storageKey, data) { const dataToStore = { data: data, metadata: { timestamp: Date.now(), }, }; try { await storageApi.setValue(storageKey, dataToStore); } catch (error) { console.error(`Error saving data for key "${storageKey}":`, error); } } async function loadFromStorage(storageKey, defaultData) { try { const syncedWrapper = await _getSyncedStorageData(storageKey); const storedData = syncedWrapper && !syncedWrapper.metadata ? syncedWrapper : syncedWrapper?.data ?? {}; return { ...defaultData, ...storedData }; } catch (error) { console.error(`Error loading data for key "${storageKey}":`, error); return defaultData; } } async function loadAndCleanFromStorage(storageKey, defaultData) { try { const syncedWrapper = await _getSyncedStorageData(storageKey); const storedData = syncedWrapper && !syncedWrapper.metadata ? syncedWrapper : syncedWrapper?.data ?? {}; 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; } } async function _getSyncedStorageData(storageKey) { if (storageApi.gmType === 'none') { return await storageApi.getValue(storageKey, null); } const [gmData, localData] = await Promise.all([GM.getValue(storageKey, null), localStorageApi.get(storageKey, null)]); const gmTimestamp = gmData?.metadata?.timestamp ?? -1; const localTimestamp = localData?.metadata?.timestamp ?? -1; if (gmTimestamp > localTimestamp) { localStorageApi.set(storageKey, gmData); return gmData; } else if (localTimestamp > gmTimestamp) { await GM.setValue(storageKey, localData); return localData; } return gmData || localData; } const currentlyObservedContainers = new WeakMap(); function fallbackGetPlayerApi() { if (window.location.hostname === 'm.youtube.com') return document.querySelector(SELECTORS.watchPlayer); if (window.location.pathname.startsWith('/shorts')) return document.querySelector(SELECTORS.shortsPlayer); if (window.location.pathname.startsWith('/watch')) return document.querySelector(SELECTORS.watchPlayer); return document.querySelector(SELECTORS.inlinePlayer); } function getOptimalResolution(targetResolutionString, usePremium = true) { 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.paygatedQualityDetails) 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 notifyAdDetected() { if (player.isPlayingAds) document.dispatchEvent( new CustomEvent('yt-helper-api-ad-detected', { detail: Object.freeze({ isPlayingAds: player.isPlayingAds }) }), ); } function checkIsIframe() { if (page.isIframe) document.dispatchEvent(new Event('yt-helper-api-detected-iframe')); } 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; video.isTimeSpecified = !!playerResponseObject?.playerConfig?.playbackStartConfig?.startPosition; video.playlistId = apiProxy.getPlaylistId(); } 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 updateChatStateUpdated(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 updateAdState() { if (!player.playerObject) return; try { const shouldAvoid = player.playerObject.classList.contains('unstarted-mode'); // YouTube doesn't update ad state fully until player is marked as started. const isAdPresent = player.playerObject.classList.contains('ad-showing') || player.playerObject.classList.contains('ad-interrupting'); const isPlayingAds = !shouldAvoid && isAdPresent; player.isPlayingAds = isPlayingAds; notifyAdDetected(); } 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; notifyAdDetected(); } 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.playerObject) return; if (currentlyObservedContainers.has(player.playerObject)) return; const adStateObserver = new MutationObserver(updateAdState); adStateObserver.observe(player.playerObject, { attributes: true, attributeFilter: ['class'] }); currentlyObservedContainers.set(player.playerObject, 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', updateChatStateUpdated); } function addNavigationListeners() { document.addEventListener('yt-navigate-finish', handleNavigationFinish); } function initialize() { window.addEventListener('pageshow', handlePlayerUpdate); checkIsIframe(); if (!page.isIframe) { 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, storageApi, }; return publicApi; })();