🎉 欢迎访问GreasyFork.Org 镜像站!本镜像站由公众号【爱吃馍】搭建,用于分享脚本。联系邮箱📮

Greasy fork 爱吃馍镜像

YouTube Helper API

YouTube Helper API.

Stan na 27-09-2025. Zobacz najnowsza wersja.

Ten skrypt nie powinien być instalowany bezpośrednio. Jest to biblioteka dla innych skyptów do włączenia dyrektywą meta // @require https://update.greasyfork.org/scripts/549881/1667760/YouTube%20Helper%20API.js

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

🚀 安装遇到问题?关注公众号获取帮助

公众号二维码

扫码关注【爱吃馍】

回复【脚本】获取最新教程和防失联地址

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

🚀 安装遇到问题?关注公众号获取帮助

公众号二维码

扫码关注【爱吃馍】

回复【脚本】获取最新教程和防失联地址

// ==UserScript==
// @name                YouTube Helper API
// @author              ElectroKnight22
// @namespace           electroknight22_helper_api_namespace
// @version             0.4.3
// @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 localStorageApi = {
        get: (key, defaultValue) => {
            const value = localStorage.getItem(key);
            if (value === null) return defaultValue;
            try {
                return JSON.parse(value);
            } catch (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);
            let storedData = {};
            if (syncedWrapper && !syncedWrapper.metadata) {
                storedData = syncedWrapper;
            } else {
                storedData = 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);
            let storedData = {};
            if (syncedWrapper && !syncedWrapper.metadata) {
                storedData = syncedWrapper;
            } else if (syncedWrapper?.data) {
                storedData = 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() {
        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 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() {
        if (!player.container) return;
        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;
})();