|
|
@ -7,17 +7,19 @@ import { retryable, exclusive } from './retry.mjs'; |
|
|
|
import { TemporaryError } from './retry.mjs'; |
|
|
|
|
|
|
|
const log = debug('youtube-maze:api'); |
|
|
|
const PLAYER_RESP_REGEX = /window\["ytInitialPlayerResponse"\] = (\{.*?\});/; |
|
|
|
const PLAYER_CONFIG_REGEX = /ytplayer\.config = (\{.*?\});/; |
|
|
|
const METADATA_REGEXES = [ |
|
|
|
/window\["ytInitialPlayerResponse"\] = (\{.*?\});/, |
|
|
|
/var ytInitialPlayerResponse = (\{.*?\});/, |
|
|
|
]; |
|
|
|
|
|
|
|
/** |
|
|
|
* Fetch the `ytplayer.config` object for a YouTube video. |
|
|
|
* Fetch metadata about a YouTube video. |
|
|
|
* |
|
|
|
* @async |
|
|
|
* @param String videoId Identifier of the video to fetch. |
|
|
|
* @return Object The player configuration object. |
|
|
|
* @return Object The video metadata object. |
|
|
|
*/ |
|
|
|
const bareGetPlayerConfig = async (videoId) => |
|
|
|
const bareGetMetadata = async (videoId) => |
|
|
|
{ |
|
|
|
const url = util.format(WATCH_BASE, encodeURIComponent(videoId)); |
|
|
|
log(`Fetching ${videoId} (${url})`); |
|
|
@ -33,36 +35,24 @@ const bareGetPlayerConfig = async (videoId) => |
|
|
|
} |
|
|
|
|
|
|
|
const body = await res.text(); |
|
|
|
const searchResults = METADATA_REGEXES.map(regex => body.match(regex)); |
|
|
|
const searchResult = searchResults.find(item => item !== null); |
|
|
|
|
|
|
|
// Look for the initial player response object to check whether the
|
|
|
|
// video was found
|
|
|
|
const responseSearch = body.match(PLAYER_RESP_REGEX); |
|
|
|
|
|
|
|
if (responseSearch === null) |
|
|
|
if (searchResult === null) |
|
|
|
{ |
|
|
|
throw new TemporaryError(`Invalid YouTube response for video ${videoId}`); |
|
|
|
throw new TemporaryError(`Incomplete YouTube response for video ${videoId}`); |
|
|
|
} |
|
|
|
|
|
|
|
const response = JSON.parse(responseSearch[1]); |
|
|
|
const metadata = JSON.parse(searchResult[1]); |
|
|
|
|
|
|
|
if (response.playabilityStatus.status !== 'OK') |
|
|
|
if (metadata.playabilityStatus.status !== 'OK') |
|
|
|
{ |
|
|
|
throw new Error(`Video ${videoId} is not available; \
|
|
|
|
status: ${response.playabilityStatus.status}; \ |
|
|
|
reason: "${response.playabilityStatus.reason}"`);
|
|
|
|
status: ${metadata.playabilityStatus.status}; \ |
|
|
|
reason: "${metadata.playabilityStatus.reason}"`);
|
|
|
|
} |
|
|
|
|
|
|
|
// Look for the definition of ytplayer.config and unserialize it
|
|
|
|
const configSearch = body.match(PLAYER_CONFIG_REGEX); |
|
|
|
|
|
|
|
if (configSearch === null) |
|
|
|
{ |
|
|
|
throw new TemporaryError(`Unable to find player for video ${videoId}`); |
|
|
|
} |
|
|
|
|
|
|
|
const config = JSON.parse(configSearch[1]); |
|
|
|
config.args.player_response = JSON.parse(config.args.player_response); |
|
|
|
const actualId = config.args.player_response.videoDetails.videoId; |
|
|
|
const actualId = metadata.videoDetails.videoId; |
|
|
|
|
|
|
|
if (videoId !== actualId) |
|
|
|
{ |
|
|
@ -70,40 +60,36 @@ reason: "${response.playabilityStatus.reason}"`); |
|
|
|
} |
|
|
|
|
|
|
|
log(`Done fetching ${videoId}`); |
|
|
|
return config; |
|
|
|
return metadata; |
|
|
|
}; |
|
|
|
|
|
|
|
export const getPlayerConfig = exclusive(retryable(bareGetPlayerConfig)); |
|
|
|
export const getMetadata = exclusive(retryable(bareGetMetadata)); |
|
|
|
|
|
|
|
/** |
|
|
|
* Get metadata about a YouTube video. |
|
|
|
* Get video ID and title. |
|
|
|
* |
|
|
|
* @param config The `ytplayer.config` object corresponding to the source |
|
|
|
* YouTube video, as obtained from `getPlayerConfig`. |
|
|
|
* @return Object containing the video metadata. |
|
|
|
* @param Object metadata The video metadata object from `getMetadata`. |
|
|
|
* @return Object Containing the video ID and title. |
|
|
|
*/ |
|
|
|
export const getVideoMeta = config => ({ |
|
|
|
videoId: config.args.player_response.videoDetails.videoId, |
|
|
|
title: config.args.player_response.videoDetails.title, |
|
|
|
export const getVideoInfo = metadata => ({ |
|
|
|
videoId: metadata.videoDetails.videoId, |
|
|
|
title: metadata.videoDetails.title, |
|
|
|
}); |
|
|
|
|
|
|
|
/** |
|
|
|
* Find videos linked from the endscreen of a YouTube video. |
|
|
|
* |
|
|
|
* @param config The `ytplayer.config` object corresponding to the source |
|
|
|
* YouTube video, as obtained from `getPlayerConfig`. |
|
|
|
* @param Object metadata The video metadata object from `getMetadata`. |
|
|
|
* @return List of identifiers of linked videos. |
|
|
|
*/ |
|
|
|
export const getEndScreenVideos = config => |
|
|
|
export const getEndScreenVideos = metadata => |
|
|
|
{ |
|
|
|
const response = config.args.player_response; |
|
|
|
|
|
|
|
if (!('endscreen' in response)) |
|
|
|
if (!('endscreen' in metadata)) |
|
|
|
{ |
|
|
|
return []; |
|
|
|
} |
|
|
|
|
|
|
|
return response.endscreen.endscreenRenderer.elements |
|
|
|
return metadata.endscreen.endscreenRenderer.elements |
|
|
|
.map(elt => elt.endscreenElementRenderer) |
|
|
|
.filter(rdr => 'watchEndpoint' in rdr.endpoint) |
|
|
|
.map(rdr => rdr.endpoint.watchEndpoint.videoId); |
|
|
@ -112,20 +98,17 @@ export const getEndScreenVideos = config => |
|
|
|
/** |
|
|
|
* Find videos linked from as cards from a YouTube video. |
|
|
|
* |
|
|
|
* @param config The `ytplayer.config` object corresponding to the source |
|
|
|
* YouTube video, as obtained from `getPlayerConfig`. |
|
|
|
* @param Object metadata The video metadata object from `getMetadata`. |
|
|
|
* @return List of identifiers of linked videos. |
|
|
|
*/ |
|
|
|
export const getCardVideos = config => |
|
|
|
export const getCardVideos = metadata => |
|
|
|
{ |
|
|
|
const response = config.args.player_response; |
|
|
|
|
|
|
|
if (!('cards' in response)) |
|
|
|
if (!('cards' in metadata)) |
|
|
|
{ |
|
|
|
return []; |
|
|
|
} |
|
|
|
|
|
|
|
return response.cards.cardCollectionRenderer.cards |
|
|
|
return metadata.cards.cardCollectionRenderer.cards |
|
|
|
.map(card => card.cardRenderer.content) |
|
|
|
.filter(content => 'videoInfoCardContentRenderer' in content) |
|
|
|
.map(content => content.videoInfoCardContentRenderer) |
|
|
|