youtube-maze/lib/api.mjs

122 lines
3.6 KiB
JavaScript
Raw Normal View History

2020-11-28 17:07:04 +00:00
import debug from 'debug';
2020-07-15 17:31:09 +00:00
import util from 'util';
import fetch from 'node-fetch';
2017-04-02 00:54:46 +00:00
2020-11-28 17:07:04 +00:00
import { WATCH_BASE } from './util.mjs';
import { TemporaryError } from './retry.mjs';
2020-11-28 17:07:04 +00:00
const log = debug('youtube-maze:api');
const PLAYER_RESP_REGEX = /window\["ytInitialPlayerResponse"\] = (\{.*?\});/;
const PLAYER_CONFIG_REGEX = /ytplayer\.config = (\{.*?\});/;
2017-04-02 00:54:46 +00:00
/**
2020-07-15 17:14:31 +00:00
* Fetch the `ytplayer.config` object for a YouTube video.
2017-04-02 00:54:46 +00:00
*
2020-07-15 17:14:31 +00:00
* @async
* @param String videoId Identifier of the video to fetch.
* @return Object The player configuration object.
2017-04-02 00:54:46 +00:00
*/
2020-11-28 17:07:04 +00:00
export const getPlayerConfig = async (videoId) =>
2017-04-02 00:54:46 +00:00
{
2020-11-28 17:07:04 +00:00
const url = util.format(WATCH_BASE, encodeURIComponent(videoId));
log(`Fetching ${videoId} (${url})`);
const res = await fetch(url);
2020-11-28 17:07:04 +00:00
debugger;
const body = await res.text();
2020-11-28 17:07:04 +00:00
// Look for the initial player response object to check whether the
// video was found
const responseSearch = body.match(PLAYER_RESP_REGEX);
if (responseSearch === null)
{
throw new TemporaryError(`Invalid YouTube response for video ${videoId}`);
}
const response = JSON.parse(responseSearch[1]);
if (response.playabilityStatus.status !== 'OK')
{
throw new Error(`Video ${videoId} is not available; \
status: ${response.playabilityStatus.status}; \
reason: "${response.playabilityStatus.reason}"`);
}
// Look for the definition of ytplayer.config and unserialize it
const configSearch = body.match(PLAYER_CONFIG_REGEX);
2017-04-02 00:54:46 +00:00
if (configSearch === null)
2017-04-02 00:54:46 +00:00
{
2020-11-28 17:07:04 +00:00
throw new TemporaryError(`Unable to find player for video ${videoId}`);
}
2017-04-02 00:54:46 +00:00
const config = JSON.parse(configSearch[1]);
config.args.player_response = JSON.parse(config.args.player_response);
2020-11-28 17:07:04 +00:00
const actualId = config.args.player_response.videoDetails.videoId;
if (videoId !== actualId)
{
throw new Error(`Video ${videoId} has actual id ${actualId}`);
}
return config;
2017-04-02 00:54:46 +00:00
};
/**
2020-07-15 17:14:31 +00:00
* Get metadata about a YouTube video.
2017-04-02 00:54:46 +00:00
*
2020-11-28 17:07:04 +00:00
* @param config The `ytplayer.config` object corresponding to the source
2020-07-15 17:14:31 +00:00
* YouTube video, as obtained from `getPlayerConfig`.
* @return Object containing the video metadata.
2017-04-02 00:54:46 +00:00
*/
2020-11-28 17:07:04 +00:00
export const getVideoMeta = config => ({
videoId: config.args.player_response.videoDetails.videoId,
title: config.args.player_response.videoDetails.title,
2020-07-15 17:14:31 +00:00
});
/**
* Find videos linked from the endscreen of a YouTube video.
*
2020-11-28 17:07:04 +00:00
* @param config The `ytplayer.config` object corresponding to the source
2020-07-15 17:14:31 +00:00
* YouTube video, as obtained from `getPlayerConfig`.
* @return List of identifiers of linked videos.
*/
2020-11-28 17:07:04 +00:00
export const getEndScreenVideos = config =>
2017-04-02 00:54:46 +00:00
{
2020-11-28 17:07:04 +00:00
const response = config.args.player_response;
2017-04-02 00:54:46 +00:00
2020-07-15 17:14:31 +00:00
if (!('endscreen' in response))
2017-04-02 00:54:46 +00:00
{
2020-07-15 17:14:31 +00:00
return [];
}
2017-04-02 00:54:46 +00:00
2020-07-15 17:14:31 +00:00
return response.endscreen.endscreenRenderer.elements
.map(elt => elt.endscreenElementRenderer)
.filter(rdr => 'watchEndpoint' in rdr.endpoint)
.map(rdr => rdr.endpoint.watchEndpoint.videoId);
};
2017-04-02 00:54:46 +00:00
2020-07-15 17:14:31 +00:00
/**
* Find videos linked from as cards from a YouTube video.
*
2020-11-28 17:07:04 +00:00
* @param config The `ytplayer.config` object corresponding to the source
2020-07-15 17:14:31 +00:00
* YouTube video, as obtained from `getPlayerConfig`.
* @return List of identifiers of linked videos.
*/
2020-11-28 17:07:04 +00:00
export const getCardVideos = config =>
2020-07-15 17:14:31 +00:00
{
2020-11-28 17:07:04 +00:00
const response = config.args.player_response;
2020-07-15 17:14:31 +00:00
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);
2017-04-02 00:54:46 +00:00
};