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