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); };