youtube-maze/explore/api.mjs

109 lines
3.4 KiB
JavaScript

import util from 'util';
import fetch from 'node-fetch';
const YOUTUBE_BASE = 'https://www.youtube.com/%s';
const WATCH_BASE = util.format(YOUTUBE_BASE, 'watch?v=%s');
const PLAYER_CONFIG_REGEX = /ytplayer\.config = (\{.*?\});/;
/**
* Make a promise that is resolved in a point in the future.
*
* @param Number time Time to wait before resolving the promise (ms).
* @return null
*/
const sleep = time => new Promise(resolve => setTimeout(resolve, time));
/**
* Fetch the `ytplayer.config` object for a YouTube video.
*
* @async
* @param String videoId Identifier of the video to fetch.
* @param Number retries Allowed number of retries after fetch errors.
* @param Number cooldown Time to wait before retrying (ms).
* @return Object The player configuration object.
*/
export const getPlayerConfig = async (videoId, retries = 3, cooldown = 1000) =>
{
const url = util.format(WATCH_BASE, videoId);
const res = await fetch(url);
const body = await res.text();
// Look for the definition of ytplayer.config and unserialize it
const configSearch = body.match(PLAYER_CONFIG_REGEX);
if (configSearch === null)
{
if (retries > 0)
{
console.info(`Error while fetching ${videoId}, retrying in ${cooldown} ms`);
await sleep(cooldown);
return getPlayerConfig(videoId, retries - 1, cooldown * 2);
}
else
{
throw new Error('Unable to find player configuration');
}
}
const config = JSON.parse(configSearch[1]);
config.args.player_response = JSON.parse(config.args.player_response);
return config;
};
/**
* Get metadata about a YouTube video.
*
* @param playerConfig The `ytplayer.config` object corresponding to the source
* YouTube video, as obtained from `getPlayerConfig`.
* @return Object containing the video metadata.
*/
export const getVideoMeta = playerConfig => ({
videoId: playerConfig.args.player_response.videoDetails.videoId,
title: playerConfig.args.player_response.videoDetails.title,
});
/**
* Find videos linked from the endscreen of a YouTube video.
*
* @param playerConfig The `ytplayer.config` object corresponding to the source
* YouTube video, as obtained from `getPlayerConfig`.
* @return List of identifiers of linked videos.
*/
export const getEndScreenVideos = playerConfig =>
{
const response = playerConfig.args.player_response;
if (!('endscreen' in response))
{
return [];
}
return response.endscreen.endscreenRenderer.elements
.map(elt => elt.endscreenElementRenderer)
.filter(rdr => 'watchEndpoint' in rdr.endpoint)
.map(rdr => rdr.endpoint.watchEndpoint.videoId);
};
/**
* Find videos linked from as cards from a YouTube video.
*
* @param playerConfig The `ytplayer.config` object corresponding to the source
* YouTube video, as obtained from `getPlayerConfig`.
* @return List of identifiers of linked videos.
*/
export const getCardVideos = playerConfig =>
{
const response = playerConfig.args.player_response;
if (!('cards' in response))
{
return [];
}
return response.cards.cardCollectionRenderer.cards
.map(card => card.cardRenderer.content)
.filter(content => 'videoInfoCardContentRenderer' in content)
.map(content => content.videoInfoCardContentRenderer)
.map(rdr => rdr.action.watchEndpoint.videoId);
};