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'; | ||||
| 
 | ||||
| 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) | ||||
|  |  | |||
|  | @ -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); | ||||
|             } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue