UNPKG

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
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); };