Support new YouTube HTML output
This commit is contained in:
parent
1490ea6ad7
commit
13e49d9a59
81
lib/api.mjs
81
lib/api.mjs
|
@ -7,17 +7,19 @@ import { retryable, exclusive } from './retry.mjs';
|
||||||
import { TemporaryError } from './retry.mjs';
|
import { TemporaryError } from './retry.mjs';
|
||||||
|
|
||||||
const log = debug('youtube-maze:api');
|
const log = debug('youtube-maze:api');
|
||||||
const PLAYER_RESP_REGEX = /window\["ytInitialPlayerResponse"\] = (\{.*?\});/;
|
const METADATA_REGEXES = [
|
||||||
const PLAYER_CONFIG_REGEX = /ytplayer\.config = (\{.*?\});/;
|
/window\["ytInitialPlayerResponse"\] = (\{.*?\});/,
|
||||||
|
/var ytInitialPlayerResponse = (\{.*?\});/,
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch the `ytplayer.config` object for a YouTube video.
|
* Fetch metadata about a YouTube video.
|
||||||
*
|
*
|
||||||
* @async
|
* @async
|
||||||
* @param String videoId Identifier of the video to fetch.
|
* @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));
|
const url = util.format(WATCH_BASE, encodeURIComponent(videoId));
|
||||||
log(`Fetching ${videoId} (${url})`);
|
log(`Fetching ${videoId} (${url})`);
|
||||||
|
@ -33,36 +35,24 @@ const bareGetPlayerConfig = async (videoId) =>
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = await res.text();
|
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
|
if (searchResult === null)
|
||||||
// video was found
|
|
||||||
const responseSearch = body.match(PLAYER_RESP_REGEX);
|
|
||||||
|
|
||||||
if (responseSearch === 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; \
|
throw new Error(`Video ${videoId} is not available; \
|
||||||
status: ${response.playabilityStatus.status}; \
|
status: ${metadata.playabilityStatus.status}; \
|
||||||
reason: "${response.playabilityStatus.reason}"`);
|
reason: "${metadata.playabilityStatus.reason}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look for the definition of ytplayer.config and unserialize it
|
const actualId = metadata.videoDetails.videoId;
|
||||||
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;
|
|
||||||
|
|
||||||
if (videoId !== actualId)
|
if (videoId !== actualId)
|
||||||
{
|
{
|
||||||
|
@ -70,40 +60,36 @@ reason: "${response.playabilityStatus.reason}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
log(`Done fetching ${videoId}`);
|
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
|
* @param Object metadata The video metadata object from `getMetadata`.
|
||||||
* YouTube video, as obtained from `getPlayerConfig`.
|
* @return Object Containing the video ID and title.
|
||||||
* @return Object containing the video metadata.
|
|
||||||
*/
|
*/
|
||||||
export const getVideoMeta = config => ({
|
export const getVideoInfo = metadata => ({
|
||||||
videoId: config.args.player_response.videoDetails.videoId,
|
videoId: metadata.videoDetails.videoId,
|
||||||
title: config.args.player_response.videoDetails.title,
|
title: metadata.videoDetails.title,
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find videos linked from the endscreen of a YouTube video.
|
* Find videos linked from the endscreen of a YouTube video.
|
||||||
*
|
*
|
||||||
* @param config The `ytplayer.config` object corresponding to the source
|
* @param Object metadata The video metadata object from `getMetadata`.
|
||||||
* YouTube video, as obtained from `getPlayerConfig`.
|
|
||||||
* @return List of identifiers of linked videos.
|
* @return List of identifiers of linked videos.
|
||||||
*/
|
*/
|
||||||
export const getEndScreenVideos = config =>
|
export const getEndScreenVideos = metadata =>
|
||||||
{
|
{
|
||||||
const response = config.args.player_response;
|
if (!('endscreen' in metadata))
|
||||||
|
|
||||||
if (!('endscreen' in response))
|
|
||||||
{
|
{
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.endscreen.endscreenRenderer.elements
|
return metadata.endscreen.endscreenRenderer.elements
|
||||||
.map(elt => elt.endscreenElementRenderer)
|
.map(elt => elt.endscreenElementRenderer)
|
||||||
.filter(rdr => 'watchEndpoint' in rdr.endpoint)
|
.filter(rdr => 'watchEndpoint' in rdr.endpoint)
|
||||||
.map(rdr => rdr.endpoint.watchEndpoint.videoId);
|
.map(rdr => rdr.endpoint.watchEndpoint.videoId);
|
||||||
|
@ -112,20 +98,17 @@ export const getEndScreenVideos = config =>
|
||||||
/**
|
/**
|
||||||
* Find videos linked from as cards from a YouTube video.
|
* Find videos linked from as cards from a YouTube video.
|
||||||
*
|
*
|
||||||
* @param config The `ytplayer.config` object corresponding to the source
|
* @param Object metadata The video metadata object from `getMetadata`.
|
||||||
* YouTube video, as obtained from `getPlayerConfig`.
|
|
||||||
* @return List of identifiers of linked videos.
|
* @return List of identifiers of linked videos.
|
||||||
*/
|
*/
|
||||||
export const getCardVideos = config =>
|
export const getCardVideos = metadata =>
|
||||||
{
|
{
|
||||||
const response = config.args.player_response;
|
if (!('cards' in metadata))
|
||||||
|
|
||||||
if (!('cards' in response))
|
|
||||||
{
|
{
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.cards.cardCollectionRenderer.cards
|
return metadata.cards.cardCollectionRenderer.cards
|
||||||
.map(card => card.cardRenderer.content)
|
.map(card => card.cardRenderer.content)
|
||||||
.filter(content => 'videoInfoCardContentRenderer' in content)
|
.filter(content => 'videoInfoCardContentRenderer' in content)
|
||||||
.map(content => content.videoInfoCardContentRenderer)
|
.map(content => content.videoInfoCardContentRenderer)
|
||||||
|
|
|
@ -31,19 +31,19 @@ export const exploreVideos = async (videoId, onUpdate) =>
|
||||||
|
|
||||||
if (!(currentId in videosNodes))
|
if (!(currentId in videosNodes))
|
||||||
{
|
{
|
||||||
const config = await api.getPlayerConfig(currentId);
|
const metadata = await api.getMetadata(currentId);
|
||||||
const meta = api.getVideoMeta(config);
|
const info = api.getVideoInfo(metadata);
|
||||||
|
|
||||||
videosNodes[currentId] = meta;
|
videosNodes[currentId] = info;
|
||||||
nextVideos[currentId] = new Set();
|
nextVideos[currentId] = new Set();
|
||||||
|
|
||||||
// Add links between this video and the linked ones
|
// Add links between this video and the linked ones
|
||||||
api.getEndScreenVideos(config)
|
api.getEndScreenVideos(metadata)
|
||||||
.forEach(nextId => nextVideos[meta.videoId].add(nextId));
|
.forEach(nextId => nextVideos[info.videoId].add(nextId));
|
||||||
api.getCardVideos(config)
|
api.getCardVideos(metadata)
|
||||||
.forEach(nextId => nextVideos[meta.videoId].add(nextId));
|
.forEach(nextId => nextVideos[info.videoId].add(nextId));
|
||||||
|
|
||||||
for (let nextId of nextVideos[meta.videoId])
|
for (let nextId of nextVideos[info.videoId])
|
||||||
{
|
{
|
||||||
queue.push(nextId);
|
queue.push(nextId);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue