youtube-maze/lib/api.mjs

118 lines
3.2 KiB
JavaScript
Raw Normal View History

2020-11-28 17:07:04 +00:00
import debug from 'debug';
2020-07-15 17:31:09 +00:00
import util from 'util';
import fetch from 'node-fetch';
2021-01-04 19:55:57 +00:00
import fs from 'fs';
2017-04-02 00:54:46 +00:00
2020-11-28 17:07:04 +00:00
import { WATCH_BASE } from './util.mjs';
import { retryable, exclusive } from './retry.mjs';
2020-11-28 17:07:04 +00:00
import { TemporaryError } from './retry.mjs';
2020-11-28 17:07:04 +00:00
const log = debug('youtube-maze:api');
2021-01-04 19:55:57 +00:00
const METADATA_REGEXES = [
/window\["ytInitialPlayerResponse"\] = (\{.*?\});/,
/var ytInitialPlayerResponse = (\{.*?\});/,
];
2017-04-02 00:54:46 +00:00
/**
2021-01-04 19:55:57 +00:00
* Fetch metadata about a YouTube video.
2017-04-02 00:54:46 +00:00
*
2020-07-15 17:14:31 +00:00
* @async
* @param String videoId Identifier of the video to fetch.
2021-01-04 19:55:57 +00:00
* @return Object The video metadata object.
2017-04-02 00:54:46 +00:00
*/
2021-01-04 19:55:57 +00:00
const bareGetMetadata = async (videoId) =>
2017-04-02 00:54:46 +00:00
{
2020-11-28 17:07:04 +00:00
const url = util.format(WATCH_BASE, encodeURIComponent(videoId));
log(`Fetching ${videoId} (${url})`);
const res = await fetch(url);
if (res.status === 429)
{
throw new TemporaryError('Too many requests');
}
else if (res.status !== 200)
{
throw new Error(`Invalid YouTube HTTP status: ${res.status}`);
}
const body = await res.text();
2021-01-04 19:55:57 +00:00
const searchResults = METADATA_REGEXES.map(regex => body.match(regex));
const searchResult = searchResults.find(item => item !== null);
2021-01-04 19:55:57 +00:00
if (searchResult === null)
2020-11-28 17:07:04 +00:00
{
2021-01-04 19:55:57 +00:00
throw new TemporaryError(`Incomplete YouTube response for video ${videoId}`);
2020-11-28 17:07:04 +00:00
}
2021-01-04 19:55:57 +00:00
const metadata = JSON.parse(searchResult[1]);
2020-11-28 17:07:04 +00:00
2021-01-04 19:55:57 +00:00
if (metadata.playabilityStatus.status !== 'OK')
2020-11-28 17:07:04 +00:00
{
throw new Error(`Video ${videoId} is not available; \
2021-01-04 19:55:57 +00:00
status: ${metadata.playabilityStatus.status}; \
reason: "${metadata.playabilityStatus.reason}"`);
2020-11-28 17:07:04 +00:00
}
2021-01-04 19:55:57 +00:00
const actualId = metadata.videoDetails.videoId;
2020-11-28 17:07:04 +00:00
if (videoId !== actualId)
{
throw new Error(`Video ${videoId} has actual id ${actualId}`);
}
log(`Done fetching ${videoId}`);
2021-01-04 19:55:57 +00:00
return metadata;
2017-04-02 00:54:46 +00:00
};
2021-01-04 19:55:57 +00:00
export const getMetadata = exclusive(retryable(bareGetMetadata));
2017-04-02 00:54:46 +00:00
/**
2021-01-04 19:55:57 +00:00
* Get video ID and title.
2017-04-02 00:54:46 +00:00
*
2021-01-04 19:55:57 +00:00
* @param Object metadata The video metadata object from `getMetadata`.
* @return Object Containing the video ID and title.
2017-04-02 00:54:46 +00:00
*/
2021-01-04 19:55:57 +00:00
export const getVideoInfo = metadata => ({
videoId: metadata.videoDetails.videoId,
title: metadata.videoDetails.title,
2020-07-15 17:14:31 +00:00
});
/**
* Find videos linked from the endscreen of a YouTube video.
*
2021-01-04 19:55:57 +00:00
* @param Object metadata The video metadata object from `getMetadata`.
2020-07-15 17:14:31 +00:00
* @return List of identifiers of linked videos.
*/
2021-01-04 19:55:57 +00:00
export const getEndScreenVideos = metadata =>
2017-04-02 00:54:46 +00:00
{
2021-01-04 19:55:57 +00:00
if (!('endscreen' in metadata))
2017-04-02 00:54:46 +00:00
{
2020-07-15 17:14:31 +00:00
return [];
}
2017-04-02 00:54:46 +00:00
2021-01-04 19:55:57 +00:00
return metadata.endscreen.endscreenRenderer.elements
2020-07-15 17:14:31 +00:00
.map(elt => elt.endscreenElementRenderer)
.filter(rdr => 'watchEndpoint' in rdr.endpoint)
.map(rdr => rdr.endpoint.watchEndpoint.videoId);
};
2017-04-02 00:54:46 +00:00
2020-07-15 17:14:31 +00:00
/**
* Find videos linked from as cards from a YouTube video.
*
2021-01-04 19:55:57 +00:00
* @param Object metadata The video metadata object from `getMetadata`.
2020-07-15 17:14:31 +00:00
* @return List of identifiers of linked videos.
*/
2021-01-04 19:55:57 +00:00
export const getCardVideos = metadata =>
2020-07-15 17:14:31 +00:00
{
2021-01-04 19:55:57 +00:00
if (!('cards' in metadata))
2020-07-15 17:14:31 +00:00
{
return [];
}
2021-01-04 19:55:57 +00:00
return metadata.cards.cardCollectionRenderer.cards
2020-07-15 17:14:31 +00:00
.map(card => card.cardRenderer.content)
.filter(content => 'videoInfoCardContentRenderer' in content)
.map(content => content.videoInfoCardContentRenderer)
.map(rdr => rdr.action.watchEndpoint.videoId);
2017-04-02 00:54:46 +00:00
};