YouTube Helper API

YouTube Helper API.

Ce script ne devrait pas être installé directement. C'est une librairie créée pour d'autres scripts. Elle doit être inclus avec la commande // @require https://update.greasyforks.org/scripts/549881/1663214/YouTube%20Helper%20API.js

// ==UserScript==
// @name            YouTube Helper API
// @author          ElectroKnight22
// @namespace       electroknight22_helper_api_namespace
// @version         0.0.3
// @license         MIT
// @description     YouTube Helper API.
// ==/UserScript==

/*jshint esversion: 11 */

window.youtubeHelperApi = (function () {
    'use strict';

    const player = {
        container: 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 = {
        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,
                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) {
        const useFallback = !event?.target?.player_;
        player.container = event?.target;
        player.api = event?.target?.player_;
        if (useFallback) {
            player.api = fallbackGetPlayerApi();
            player.container = player.api.parentElement;
        }
        player.videoElement = player.container.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();
        checkAdPresense();
        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 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 initialize() {
        checkIsIframe();
        updatePlayerState();
        addNavigationListeners();
        addPlayerStateListeners();
        addChatStateListeners();
    }

    initialize();

    const publicApi = {
        getPlayer: () => ({ ...player }),
        getVideo: () => ({ ...video }),
        getChat: () => ({ ...chat }),
        getYoutubePage: () => ({ ...page }),
        getAdState: () => detectedAds,
        checkAdPresense: checkAdPresense,
        getOptimalResolution: getOptimalResolution,
        setPlaybackResolution: setPlaybackResolution,
    };

    return publicApi;
})();
长期地址
遇到问题?请前往 GitHub 提 Issues。