diff --git a/lib/api.mjs b/lib/api.mjs index 6f159bd..5cdcc27 100644 --- a/lib/api.mjs +++ b/lib/api.mjs @@ -1,23 +1,26 @@ import debug from 'debug'; import util from 'util'; import fetch from 'node-fetch'; +import fs from 'fs'; import { WATCH_BASE } from './util.mjs'; 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 +36,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 +61,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 +99,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) diff --git a/lib/explore.mjs b/lib/explore.mjs index 99d6eef..0082394 100644 --- a/lib/explore.mjs +++ b/lib/explore.mjs @@ -31,19 +31,19 @@ export const exploreVideos = async (videoId, onUpdate) => if (!(currentId in videosNodes)) { - const config = await api.getPlayerConfig(currentId); - const meta = api.getVideoMeta(config); + const metadata = await api.getMetadata(currentId); + const info = api.getVideoInfo(metadata); - videosNodes[currentId] = meta; + videosNodes[currentId] = info; nextVideos[currentId] = new Set(); // Add links between this video and the linked ones - api.getEndScreenVideos(config) - .forEach(nextId => nextVideos[meta.videoId].add(nextId)); - api.getCardVideos(config) - .forEach(nextId => nextVideos[meta.videoId].add(nextId)); + api.getEndScreenVideos(metadata) + .forEach(nextId => nextVideos[info.videoId].add(nextId)); + api.getCardVideos(metadata) + .forEach(nextId => nextVideos[info.videoId].add(nextId)); - for (let nextId of nextVideos[meta.videoId]) + for (let nextId of nextVideos[info.videoId]) { queue.push(nextId); }