scrape-youtube
Version:
A lightning fast package to scrape YouTube search results. This was made for Discord Bots.
191 lines (166 loc) • 7.31 kB
text/typescript
import {
ResultFilter,
ResultType,
SearchOptions,
Video,
Playlist,
Results,
LiveStream,
ChannelResult
} from './interface';
import { getStreamData, getPlaylistData, getVideoData, getChannelRenderData } from './parser';
import { get } from 'https';
export * from './interface';
class Youtube {
/**
* Enable debugging for extra information during each search
*/
public debug = false;
public host = 'https://www.youtube.com';
public path = '/results';
constructor() {}
private getURL(query: string, options: SearchOptions): string {
const url = new URL(this.path, this.host);
let sp = ResultFilter[(options.type || 'video') as ResultType];
url.search = new URLSearchParams({
search_query: query,
...(options.params || {})
}).toString();
if (options.sp) sp = options.sp;
return url.href + '&sp=' + sp;
}
private extractRenderData(page: string): Promise<JSON> {
return new Promise((resolve, reject) => {
try {
// #1 - Remove line breaks
page = page.split('\n').join('');
// #2 - Split at start of data
page = page.split('var ytInitialData')[1];
// #3 - Remove the first equals sign
const spot = page.split('=');
spot.shift();
// #4 - Join the split data and split again at the closing tag
const data = spot.join('=').split(';</script>')[0];
let render = null;
let contents = [];
const primary = JSON.parse(data).contents.twoColumnSearchResultsRenderer.primaryContents;
// The renderer we want. This should contain all search result information
if (primary['sectionListRenderer']) {
if (this.debug) console.log('[ytInitialData] sectionListRenderer');
// Filter only the search results, exclude ads and promoted content
render = primary.sectionListRenderer.contents.filter((item: any) => {
return (
item.itemSectionRenderer &&
item.itemSectionRenderer.contents &&
item.itemSectionRenderer.contents.filter(
(c: any) => c['videoRenderer'] || c['playlistRenderer'] || c['channelRenderer']
).length
);
});
if (render.length) contents = render.shift().itemSectionRenderer.contents;
}
// YouTube occasionally switches to a rich grid renderer.
// More testing will be needed to see how different this is from sectionListRenderer
if (primary['richGridRenderer']) {
if (this.debug) console.log('[ytInitialData] richGridRenderer');
contents = primary.richGridRenderer.contents
.filter((item: any) => {
return item.richItemRenderer && item.richItemRenderer.content;
})
.map((item: any) => item.richItemRenderer.content);
}
resolve(contents);
} catch (e) {
reject(e);
}
});
}
/**
* Parse the data extracted from the page to match each interface
* @param data Video Renderer Data
*/
private parseData(data: any): Promise<Results> {
return new Promise((resolve, reject) => {
try {
const results: Results = {
videos: [],
playlists: [],
streams: [],
channels: []
};
data.forEach((item: any) => {
if (item['channelRenderer']) {
try {
const result: ChannelResult = getChannelRenderData(item['channelRenderer']);
results.channels.push(result);
} catch (e) {
if (this.debug) console.log(e);
}
}
if (item['videoRenderer'] && item['videoRenderer']['lengthText']) {
try {
const result: Video = getVideoData(item['videoRenderer']);
results.videos.push(result);
} catch (e) {
if (this.debug) console.log(e);
}
}
if (item['videoRenderer'] && !item['videoRenderer']['lengthText']) {
try {
const result: LiveStream = getStreamData(item['videoRenderer']);
results.streams.push(result);
} catch (e) {
if (this.debug) console.log(e);
}
}
if (item['playlistRenderer']) {
try {
const result: Playlist = getPlaylistData(item['playlistRenderer']);
results.playlists.push(result);
} catch (e) {
if (this.debug) console.log(e);
}
}
});
resolve(results);
} catch (e) {
console.warn(e);
reject('Fatal error when parsing result data. Please report this on GitHub');
}
});
}
/**
* Load the page and scrape the data
* @param query Search query
* @param options Search options
*/
private load(query: string, options: SearchOptions = {}): Promise<string> {
const url = this.getURL(query, options);
if (this.debug) console.log(url);
const request = options.request || {};
return new Promise((resolve, reject) => {
get(Object.assign(new URL(url), request), (res: any) => {
res.setEncoding('utf8');
let data = '';
res.on('data', (chunk: any) => (data += chunk));
res.on('end', () => resolve(data));
}).on('error', reject);
});
}
public search(query: string, options: SearchOptions = {}): Promise<Results> {
return new Promise(async (resolve, reject) => {
try {
const page = await this.load(query, options);
const data = await this.extractRenderData(page);
const results = await this.parseData(data);
resolve(results);
} catch (e) {
reject(e);
}
});
}
}
export const youtube = new Youtube();
export const search = (query: string, options: SearchOptions = {}): Promise<Results> => {
return youtube.search(query, options);
};