From 398b89a7071d784ecd3dc40dfb236a28bcad074a Mon Sep 17 00:00:00 2001 From: "C++, JS, TS, Python Developer" <65385476+killer069@users.noreply.github.com> Date: Fri, 13 Aug 2021 15:17:35 +0530 Subject: [PATCH] YouTube WIP (#5) YouTube downloader and searcher added --- node-youtube-dl/YouTube/README.md | 83 +++++++++ node-youtube-dl/YouTube/classes/Channel.ts | 63 +++++++ node-youtube-dl/YouTube/classes/Playlist.ts | 140 +++++++++++++++ node-youtube-dl/YouTube/classes/Thumbnail.ts | 48 +++++ node-youtube-dl/YouTube/classes/Video.ts | 107 ++++++++++++ node-youtube-dl/YouTube/index.ts | 3 + node-youtube-dl/YouTube/search.ts | 33 ++++ node-youtube-dl/YouTube/utils/cipher.ts | 175 +++++++++++++++++++ node-youtube-dl/YouTube/utils/extractor.ts | 174 ++++++++++++++++++ node-youtube-dl/YouTube/utils/index.ts | 1 + node-youtube-dl/YouTube/utils/parser.ts | 175 +++++++++++++++++++ node-youtube-dl/YouTube/utils/request.ts | 12 ++ node-youtube-dl/index.ts | 12 ++ package-lock.json | 175 +++++++++++++++++++ package.json | 26 +++ tsconfig.json | 23 +++ 16 files changed, 1250 insertions(+) create mode 100644 node-youtube-dl/YouTube/README.md create mode 100644 node-youtube-dl/YouTube/classes/Channel.ts create mode 100644 node-youtube-dl/YouTube/classes/Playlist.ts create mode 100644 node-youtube-dl/YouTube/classes/Thumbnail.ts create mode 100644 node-youtube-dl/YouTube/classes/Video.ts create mode 100644 node-youtube-dl/YouTube/index.ts create mode 100644 node-youtube-dl/YouTube/search.ts create mode 100644 node-youtube-dl/YouTube/utils/cipher.ts create mode 100644 node-youtube-dl/YouTube/utils/extractor.ts create mode 100644 node-youtube-dl/YouTube/utils/index.ts create mode 100644 node-youtube-dl/YouTube/utils/parser.ts create mode 100644 node-youtube-dl/YouTube/utils/request.ts create mode 100644 node-youtube-dl/index.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 tsconfig.json diff --git a/node-youtube-dl/YouTube/README.md b/node-youtube-dl/YouTube/README.md new file mode 100644 index 0000000..b93f0ac --- /dev/null +++ b/node-youtube-dl/YouTube/README.md @@ -0,0 +1,83 @@ +# YouTube Downloader/Search +### Downloades youtube videos, playlist and also searches song + +This is a light-weight youtube downloader and searcher. + +- searches by video, playlist, channel +- obtains audio playback url. + +## Video commands usage :- +### 1. video_basic_info(url : `string`) +*This is what downloader gets first.* +```js +let video = await video_basic_info(url) +``` +### 2. video_info(url : `string`) +*This contains everything with deciphered formats along with video_details.* +```js +let video = await video_info(url) +``` +### 3. formats +*This shows all formats availiable of a video* +```js +let video = await video_info(url) +console.log(video.format) +``` + +## Playlist commands usage :- +### 1. playlist_info(url : `string`) +*This containes every thing about a playlist* +```js +let playlist = await playlist_info(url) //This only fetches first 100 songs from a playlist +``` + +#### 2. playlist.fetch() +*This fetches whole playlist.* +```js +let playlist = await playlist_info(url) //This only fetches first 100 songs from a playlist + +await playlist.fetch() // This one fetches all songs from a playlist. +``` +#### 3. playlist.page(page_number : `number`) +*This gives you no. of videos from a page* +> Pages : every 100 songs have been divided into pages. +> So for example: There are 782 songs in a playlist, so there will be 8 pages. + +```js +let playlist = await playlist_info(url) //This only fetches first 100 songs from a playlist + +await playlist.fetch() // This one fetches all songs from a playlist. + +console.log(playlist.page(1)) // This displays first 100 songs of a playlist +``` +#### 4. playlist.total_videos +*This tells you total no of videos that have been fetched so far.* +```js +let playlist = await playlist_info(url) //This only fetches first 100 songs from a playlist + +await playlist.fetch() // This one fetches all songs from a playlist. + +console.log(playlist.total_videos) // This displays total no. of videos fetched so far. +``` +#### 5. playlist.videoCount +*This tells total no. of songs in a playlist.* +```js +let playlist = await playlist_info(url) //This only fetches first 100 songs from a playlist + +await playlist.fetch() // This one fetches all songs from a playlist. + +console.log(playlist.videoCount) // This displays total no. of videos in a playlist +``` + +## Search Command Usage :- +### 1. search(url : `string`, options? : [SearchOptions](https://github.com/node-youtube-dl/node-youtube-dl/tree/Killer/node-youtube-dl/YouTube#searchoptions)) +*This enables all searching mechanism (video, channel, playlist)* +```js +let result = await search('Rick Roll') +console.log(result[0].url) +``` +### SearchOptions +``` +type?: "video" | "playlist" | "channel" | "all"; +limit?: number; +``` diff --git a/node-youtube-dl/YouTube/classes/Channel.ts b/node-youtube-dl/YouTube/classes/Channel.ts new file mode 100644 index 0000000..d1bafa3 --- /dev/null +++ b/node-youtube-dl/YouTube/classes/Channel.ts @@ -0,0 +1,63 @@ +export interface ChannelIconInterface { + url?: string; + width: number; + height: number; +} + +export class Channel { + name?: string; + verified?: boolean; + id?: string; + url?: string; + icon?: ChannelIconInterface; + subscribers?: string; + + constructor(data: any) { + if (!data) throw new Error(`Cannot instantiate the ${this.constructor.name} class without data!`); + + this._patch(data); + } + + private _patch(data: any): void { + if (!data) data = {}; + + this.name = data.name || null; + this.verified = !!data.verified || false; + this.id = data.id || null; + this.url = data.url || null; + this.icon = data.icon || { url: null, width: 0, height: 0 }; + this.subscribers = data.subscribers || null; + } + + /** + * Returns channel icon url + * @param {object} options Icon options + * @param {number} [options.size=0] Icon size. **Default is 0** + */ + iconURL(options = { size: 0 }): string | undefined{ + if (typeof options.size !== "number" || options.size < 0) throw new Error("invalid icon size"); + if (!this.icon?.url) return undefined; + const def = this.icon.url.split("=s")[1].split("-c")[0]; + return this.icon.url.replace(`=s${def}-c`, `=s${options.size}-c`); + } + + get type(): "channel" { + return "channel"; + } + + toString(): string { + return this.name || ""; + } + + toJSON() { + return { + name: this.name, + verified: this.verified, + id: this.id, + url: this.url, + iconURL: this.iconURL(), + type: this.type, + subscribers: this.subscribers + }; + } +} \ No newline at end of file diff --git a/node-youtube-dl/YouTube/classes/Playlist.ts b/node-youtube-dl/YouTube/classes/Playlist.ts new file mode 100644 index 0000000..2eb5ec2 --- /dev/null +++ b/node-youtube-dl/YouTube/classes/Playlist.ts @@ -0,0 +1,140 @@ +import { getPlaylistVideos, getContinuationToken } from "../utils/extractor"; +import { url_get } from "../utils/request"; +import { Thumbnail } from "./Thumbnail"; +import { Channel } from "./Channel"; +import { Video } from "./Video"; +const BASE_API = "https://www.youtube.com/youtubei/v1/browse?key="; + +export class PlayList{ + id?: string; + title?: string; + videoCount?: number; + lastUpdate?: string; + views?: number; + url?: string; + link?: string; + channel?: Channel; + thumbnail?: Thumbnail; + private videos?: []; + private fetched_videos : Map + private _continuation: { api?: string; token?: string; clientVersion?: string } = {}; + private __count : number + + constructor(data : any, searchResult : Boolean = false){ + if (!data) throw new Error(`Cannot instantiate the ${this.constructor.name} class without data!`); + this.__count = 0 + this.fetched_videos = new Map() + if(searchResult) this.__patchSearch(data) + else this.__patch(data) + } + + private __patch(data:any){ + this.id = data.id || undefined; + this.url = data.url || undefined; + this.title = data.title || undefined; + this.videoCount = data.videoCount || 0; + this.lastUpdate = data.lastUpdate || undefined; + this.views = data.views || 0; + this.link = data.link || undefined; + this.channel = data.author || undefined; + this.thumbnail = data.thumbnail || undefined; + this.videos = data.videos || []; + this.__count ++ + this.fetched_videos.set(`page${this.__count}`, this.videos as Video[]) + this._continuation.api = data.continuation?.api ?? undefined; + this._continuation.token = data.continuation?.token ?? undefined; + this._continuation.clientVersion = data.continuation?.clientVersion ?? ""; + } + + private __patchSearch(data: any){ + this.id = data.id || undefined; + this.url = this.id ? `https://www.youtube.com/playlist?list=${this.id}` : undefined; + this.title = data.title || undefined; + this.thumbnail = data.thumbnail || undefined; + this.channel = data.channel || undefined; + this.videos = []; + this.videoCount = data.videos || 0; + this.link = undefined; + this.lastUpdate = undefined; + this.views = 0; + } + + async next(limit: number = Infinity): Promise { + if (!this._continuation || !this._continuation.token) return []; + + let nextPage = await url_get(`${BASE_API}${this._continuation.api}`, { + method: "POST", + body: JSON.stringify({ + continuation: this._continuation.token, + context: { + client: { + utcOffsetMinutes: 0, + gl: "US", + hl: "en", + clientName: "WEB", + clientVersion: this._continuation.clientVersion + }, + user: {}, + request: {} + } + }) + }); + + let contents = JSON.parse(nextPage)?.onResponseReceivedActions[0]?.appendContinuationItemsAction?.continuationItems + if(!contents) return [] + + let playlist_videos = getPlaylistVideos(contents, limit) + this.fetched_videos.set(`page${this.__count}`, playlist_videos) + this._continuation.token = getContinuationToken(contents) + return playlist_videos + } + + async fetch(max: number = Infinity) { + let continuation = this._continuation.token; + if (!continuation) return this; + if (max < 1) max = Infinity; + + while (typeof this._continuation.token === "string" && this._continuation.token.length) { + if (this.videos?.length as number >= max) break; + this.__count++ + const res = await this.next(); + if (!res.length) break; + } + + return this; + } + + get type(): "playlist" { + return "playlist"; + } + + page(number : number): Video[]{ + if(!number) throw new Error('Given Page number is not provided') + if(!this.fetched_videos.has(`page${number}`)) throw new Error('Given Page number is invalid') + return this.fetched_videos.get(`page${number}`) as Video[] + } + + get total_pages(){ + return this.fetched_videos.size + } + + get total_videos(){ + let page_number: number = this.total_pages + return (page_number - 1) * 100 + (this.fetched_videos.get(`page${page_number}`) as Video[]).length + } + + toJSON() { + return { + id: this.id, + title: this.title, + thumbnail: this.thumbnail, + channel: { + name : this.channel?.name, + id : this.channel?.id, + icon : this.channel?.iconURL() + }, + url: this.url, + videos: this.videos + }; + } +} \ No newline at end of file diff --git a/node-youtube-dl/YouTube/classes/Thumbnail.ts b/node-youtube-dl/YouTube/classes/Thumbnail.ts new file mode 100644 index 0000000..4ea8db5 --- /dev/null +++ b/node-youtube-dl/YouTube/classes/Thumbnail.ts @@ -0,0 +1,48 @@ +type ThumbnailType = "default" | "hqdefault" | "mqdefault" | "sddefault" | "maxresdefault" | "ultrares"; + +export class Thumbnail { + id?: string; + width?: number; + height?: number; + url?: string; + + constructor(data: any) { + if (!data) throw new Error(`Cannot instantiate the ${this.constructor.name} class without data!`); + + this._patch(data); + } + + private _patch(data: any) { + if (!data) data = {}; + + this.id = data.id || undefined; + this.width = data.width || 0; + this.height = data.height || 0; + this.url = data.url || undefined; + } + + displayThumbnailURL(thumbnailType: ThumbnailType = "maxresdefault"): string { + if (!["default", "hqdefault", "mqdefault", "sddefault", "maxresdefault", "ultrares"].includes(thumbnailType)) throw new Error(`Invalid thumbnail type "${thumbnailType}"!`); + if (thumbnailType === "ultrares") return this.url as string; + return `https://i3.ytimg.com/vi/${this.id}/${thumbnailType}.jpg`; + } + + defaultThumbnailURL(id: "0" | "1" | "2" | "3" | "4"): string { + if (!id) id = "0"; + if (!["0", "1", "2", "3", "4"].includes(id)) throw new Error(`Invalid thumbnail id "${id}"!`); + return `https://i3.ytimg.com/vi/${this.id}/${id}.jpg`; + } + + toString(): string { + return this.url ? `${this.url}` : ""; + } + + toJSON() { + return { + id: this.id, + width: this.width, + height: this.height, + url: this.url + }; + } +} diff --git a/node-youtube-dl/YouTube/classes/Video.ts b/node-youtube-dl/YouTube/classes/Video.ts new file mode 100644 index 0000000..bb1969e --- /dev/null +++ b/node-youtube-dl/YouTube/classes/Video.ts @@ -0,0 +1,107 @@ +import { Channel } from "./Channel"; +import { Thumbnail } from "./Thumbnail"; + +interface VideoOptions { + id?: string; + url? : string; + title?: string; + description?: string; + duration_formatted: string; + duration: number; + uploadedAt?: string; + views: number; + thumbnail?: { + id: string | undefined; + width: number | undefined ; + height: number | undefined; + url: string | undefined; + }; + channel?: { + name : string, + id : string, + icon : string + }; + videos?: Video[]; + type : string; + ratings : { + likes: number; + dislikes: number; + } + live: boolean; + private: boolean; + tags: string[]; +} + +export class Video { + id?: string; + url? : string; + title?: string; + description?: string; + durationFormatted: string; + duration: number; + uploadedAt?: string; + views: number; + thumbnail?: Thumbnail; + channel?: Channel; + videos?: Video[]; + likes: number; + dislikes: number; + live: boolean; + private: boolean; + tags: string[]; + + constructor(data : any){ + if(!data) throw new Error(`Can not initiate ${this.constructor.name} without data`) + + this.id = data.id || undefined; + this.url = `https://www.youtube.com/watch?v=${this.id}` + this.title = data.title || undefined; + this.description = data.description || undefined; + this.durationFormatted = data.duration_raw || "0:00"; + this.duration = (data.duration < 0 ? 0 : data.duration) || 0; + this.uploadedAt = data.uploadedAt || undefined; + this.views = parseInt(data.views) || 0; + this.thumbnail = data.thumbnail || {}; + this.channel = data.channel || {}; + this.likes = data.ratings?.likes as number || 0; + this.dislikes = data.ratings?.dislikes || 0; + this.live = !!data.live; + this.private = !!data.private; + this.tags = data.tags || []; + } + + get type(): "video" { + return "video"; + } + + get toString(): string { + return this.url || ""; + } + + get toJSON(): VideoOptions{ + return { + id: this.id, + url: this.url, + title: this.title, + description: this.description, + duration: this.duration, + duration_formatted: this.durationFormatted, + uploadedAt: this.uploadedAt, + thumbnail: this.thumbnail?.toJSON(), + channel: { + name: this.channel?.name as string, + id: this.channel?.id as string, + icon: this.channel?.iconURL() as string + }, + views: this.views, + type: this.type, + tags: this.tags, + ratings: { + likes: this.likes, + dislikes: this.dislikes + }, + live: this.live, + private: this.private + }; + } +} \ No newline at end of file diff --git a/node-youtube-dl/YouTube/index.ts b/node-youtube-dl/YouTube/index.ts new file mode 100644 index 0000000..ea8738c --- /dev/null +++ b/node-youtube-dl/YouTube/index.ts @@ -0,0 +1,3 @@ +export { search } from './search' + +export * from './utils' \ No newline at end of file diff --git a/node-youtube-dl/YouTube/search.ts b/node-youtube-dl/YouTube/search.ts new file mode 100644 index 0000000..4df4f6d --- /dev/null +++ b/node-youtube-dl/YouTube/search.ts @@ -0,0 +1,33 @@ +import { url_get } from "./utils/request"; +import { ParseSearchInterface, ParseSearchResult } from "./utils/parser"; +import { Video } from "./classes/Video"; +import { Channel } from "./classes/Channel"; +import { PlayList } from "./classes/Playlist"; + + +enum SearchType { + Video = 'EgIQAQ%253D%253D', + PlayList = 'EgIQAw%253D%253D', + Channel = 'EgIQAg%253D%253D', +} + +export async function search(search :string, options? : ParseSearchInterface): Promise<(Video | Channel | PlayList)[]> { + let url = 'https://www.youtube.com/results?search_query=' + search.replaceAll(' ', '+') + if(!url.match('&sp=')){ + url += '&sp=' + switch(options?.type){ + case 'channel': + url += SearchType.Channel + break + case 'playlist': + url += SearchType.PlayList + break + case 'video': + url += SearchType.Video + break + } + } + let body = await url_get(url) + let data = ParseSearchResult(body, options) + return data +} \ No newline at end of file diff --git a/node-youtube-dl/YouTube/utils/cipher.ts b/node-youtube-dl/YouTube/utils/cipher.ts new file mode 100644 index 0000000..0c2cb98 --- /dev/null +++ b/node-youtube-dl/YouTube/utils/cipher.ts @@ -0,0 +1,175 @@ +import { URL } from 'node:url' +import { url_get } from './request' +import querystring from 'node:querystring' + +interface formatOptions { + url? : string; + sp? : string; + signatureCipher? : string; + cipher?: string; + s? : string; +} +const var_js = '[a-zA-Z_\\$][a-zA-Z_0-9]*'; +const singlequote_js = `'[^'\\\\]*(:?\\\\[\\s\\S][^'\\\\]*)*'`; +const duoblequote_js = `"[^"\\\\]*(:?\\\\[\\s\\S][^"\\\\]*)*"`; +const quote_js = `(?:${singlequote_js}|${duoblequote_js})`; +const key_js = `(?:${var_js}|${quote_js})`; +const prop_js = `(?:\\.${var_js}|\\[${quote_js}\\])`; +const empty_js = `(?:''|"")`; +const reverse_function = ':function\\(a\\)\\{' + +'(?:return )?a\\.reverse\\(\\)' + +'\\}'; +const slice_function = ':function\\(a,b\\)\\{' + +'return a\\.slice\\(b\\)' + +'\\}'; +const splice_function = ':function\\(a,b\\)\\{' + +'a\\.splice\\(0,b\\)' + +'\\}'; +const swap_function = ':function\\(a,b\\)\\{' + +'var c=a\\[0\\];a\\[0\\]=a\\[b(?:%a\\.length)?\\];a\\[b(?:%a\\.length)?\\]=c(?:;return a)?' + +'\\}'; +const obj_regexp = new RegExp( + `var (${var_js})=\\{((?:(?:${ + key_js}${reverse_function}|${ + key_js}${slice_function}|${ + key_js}${splice_function}|${ + key_js}${swap_function + }),?\\r?\\n?)+)\\};`) +const function_regexp = new RegExp(`${`function(?: ${var_js})?\\(a\\)\\{` + +`a=a\\.split\\(${empty_js}\\);\\s*` + +`((?:(?:a=)?${var_js}`}${ +prop_js +}\\(a,\\d+\\);)+)` + +`return a\\.join\\(${empty_js}\\)` + +`\\}`); +const reverse_regexp = new RegExp(`(?:^|,)(${key_js})${reverse_function}`, 'm'); +const slice_regexp = new RegExp(`(?:^|,)(${key_js})${slice_function}`, 'm'); +const splice_regexp = new RegExp(`(?:^|,)(${key_js})${splice_function}`, 'm'); +const swap_regexp = new RegExp(`(?:^|,)(${key_js})${swap_function}`, 'm'); + +export function js_tokens( body:string ) { + let function_action = function_regexp.exec(body) + let object_action = obj_regexp.exec(body) + if(!function_action || !object_action) return null + + let object = object_action[1].replace(/\$/g, '\\$') + let object_body = object_action[2].replace(/\$/g, '\\$') + let function_body = function_action[1].replace(/\$/g, '\\$') + + let result = reverse_regexp.exec(object_body); + const reverseKey = result && result[1] + .replace(/\$/g, '\\$') + .replace(/\$|^'|^"|'$|"$/g, ''); + + result = slice_regexp.exec(object_body) + const sliceKey = result && result[1] + .replace(/\$/g, '\\$') + .replace(/\$|^'|^"|'$|"$/g, ''); + + result = splice_regexp.exec(object_body); + const spliceKey = result && result[1] + .replace(/\$/g, '\\$') + .replace(/\$|^'|^"|'$|"$/g, ''); + + result = swap_regexp.exec(object_body); + const swapKey = result && result[1] + .replace(/\$/g, '\\$') + .replace(/\$|^'|^"|'$|"$/g, ''); + + const keys = `(${[reverseKey, sliceKey, spliceKey, swapKey].join('|')})`; + const myreg = `(?:a=)?${object + }(?:\\.${keys}|\\['${keys}'\\]|\\["${keys}"\\])` + + `\\(a,(\\d+)\\)`; + const tokenizeRegexp = new RegExp(myreg, 'g'); + const tokens = []; + while((result = tokenizeRegexp.exec(function_body)) !== null){ + let key = result[1] || result[2] || result[3]; + switch (key) { + case swapKey: + tokens.push(`sw${result[4]}`); + break; + case reverseKey: + tokens.push('rv'); + break; + case sliceKey: + tokens.push(`sl${result[4]}`); + break; + case spliceKey: + tokens.push(`sp${result[4]}`); + break; + } + } + return tokens +} + +function deciper_signature(tokens : string[], signature :string){ + let sig = signature.split('') + let len = tokens.length + for(let i = 0; i < len; i++ ){ + let token = tokens[i], pos; + switch(token.slice(0,2)){ + case 'sw': + pos = parseInt(token.slice(2)) + sig = swappositions(sig, pos) + break + case 'rv': + sig = sig.reverse() + break + case 'sl': + pos = parseInt(token.slice(2)) + sig = sig.slice(pos) + break + case 'sp': + pos = parseInt(token.slice(2)) + sig.splice(0, pos) + break + } + } + return sig.join('') +} + + +function swappositions(array : string[], position : number){ + let first = array[0] + let pos_args = array[position] + array[0] = array[position] + array[position] = first + return array +} + +function download_url(format: formatOptions, sig : string){ + let decoded_url; + if(!format.url) return; + decoded_url = format.url + + decoded_url = decodeURIComponent(decoded_url) + + let parsed_url = new URL(decoded_url) + parsed_url.searchParams.set('ratebypass', 'yes'); + + if(sig){ + parsed_url.searchParams.set(format.sp || 'signature', sig) + } + format.url = parsed_url.toString(); +} + +export async function format_decipher(formats: formatOptions[], html5player : string){ + let body = await url_get(html5player) + let tokens = js_tokens(body) + formats.forEach((format) => { + let cipher = format.signatureCipher || format.cipher; + if(cipher){ + Object.assign(format, querystring.parse(cipher)) + delete format.signatureCipher; + delete format.cipher; + } + let sig; + if(tokens && format.s){ + sig = deciper_signature(tokens, format.s) + download_url(format, sig) + delete format.s + delete format.sp + } + }); + return formats +} \ No newline at end of file diff --git a/node-youtube-dl/YouTube/utils/extractor.ts b/node-youtube-dl/YouTube/utils/extractor.ts new file mode 100644 index 0000000..25b5bb8 --- /dev/null +++ b/node-youtube-dl/YouTube/utils/extractor.ts @@ -0,0 +1,174 @@ +import { url_get } from './request' +import { format_decipher, js_tokens } from './cipher' +import { Video } from '../classes/Video' +import { RequestInit } from 'node-fetch' +import { PlayList } from '../classes/Playlist' + +const DEFAULT_API_KEY = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"; +const youtube_url = /https:\/\/www.youtube.com\//g +const video_pattern = /^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$/; + +export async function video_basic_info(url : string){ + if(!url.match(youtube_url) || !url.match(video_pattern)) throw new Error('This is not a YouTube URL') + let video_id = url.split('watch?v=')[1].split('&')[0] + let new_url = 'https://www.youtube.com/watch?v=' + video_id + let body = await url_get(new_url) + let player_response = JSON.parse(body.split("var ytInitialPlayerResponse = ")[1].split(";")[0]) + if(player_response.playabilityStatus.status === 'ERROR') throw new Error(`While getting info from url \n ${player_response.playabilityStatus.reason}`) + let html5player = 'https://www.youtube.com' + body.split('"jsUrl":"')[1].split('"')[0] + let format = [] + format.push(player_response.streamingData.formats[0]) + format.push(...player_response.streamingData.adaptiveFormats) + let vid = player_response.videoDetails + let microformat = player_response.microformat.playerMicroformatRenderer + let video_details = { + id : vid.videoId, + url : 'https://www.youtube.com/watch?v=' + vid.videoId, + title : vid.title, + description : vid.shortDescription, + duration : vid.lengthSeconds, + uploadedDate : microformat.publishDate, + thumbnail : { + width : vid.thumbnail.thumbnails[vid.thumbnail.thumbnails.length - 1].width, + height : vid.thumbnail.thumbnails[vid.thumbnail.thumbnails.length - 1].height, + url : `https://i.ytimg.com/vi/${vid.videoId}/maxresdefault.jpg` + }, + channel : { + name : vid.author, + id : vid.channelId, + url : `https://www.youtube.com/channel/${vid.channelId}` + }, + views : vid.viewCount, + tags : vid.keywords, + averageRating : vid.averageRating, + live : vid.isLiveContent, + private : vid.isPrivate + } + return { + html5player, + format, + video_details + } +} + +export async function video_info(url : string) { + let data = await video_basic_info(url) + if(data.format[0].signatureCipher || data.format[0].cipher){ + data.format = await format_decipher(data.format, data.html5player) + return data + } + else { + return data + } +} + +export async function playlist_info(url : string) { + if (!url || typeof url !== "string") throw new Error(`Expected playlist url, received ${typeof url}!`); + if(url.search('(\\?|\\&)list\\=') === -1) throw new Error('This is not a PlayList URL') + + let Playlist_id = url.split('list=')[1].split('&')[0] + let new_url = `https://www.youtube.com/playlist?list=${Playlist_id}` + + let body = await url_get(new_url) + let response = JSON.parse(body.split("var ytInitialData = ")[1].split(";")[0]) + if(response.alerts && response.alerts[0].alertRenderer.type === 'ERROR') throw new Error(`While parsing playlist url\n ${response.alerts[0].alertRenderer.text.runs[0].text}`) + + let rawJSON = `${body.split('{"playlistVideoListRenderer":{"contents":')[1].split('}],"playlistId"')[0]}}]`; + let parsed = JSON.parse(rawJSON); + let playlistDetails = JSON.parse(body.split('{"playlistSidebarRenderer":')[1].split("}};")[0]).items; + + let API_KEY = body.split('INNERTUBE_API_KEY":"')[1]?.split('"')[0] ?? body.split('innertubeApiKey":"')[1]?.split('"')[0] ?? DEFAULT_API_KEY; + let videos = getPlaylistVideos(parsed, 100); + + let data = playlistDetails[0].playlistSidebarPrimaryInfoRenderer; + if (!data.title.runs || !data.title.runs.length) return undefined; + + let author = playlistDetails[1]?.playlistSidebarSecondaryInfoRenderer.videoOwner; + let views = data.stats.length === 3 ? data.stats[1].simpleText.replace(/[^0-9]/g, "") : 0; + let lastUpdate = data.stats.find((x: any) => "runs" in x && x["runs"].find((y: any) => y.text.toLowerCase().includes("last update")))?.runs.pop()?.text ?? null; + let videosCount = data.stats[0].runs[0].text.replace(/[^0-9]/g, "") || 0; + + let res = new PlayList({ + continuation: { + api: API_KEY, + token: getContinuationToken(parsed), + clientVersion: body.split('"INNERTUBE_CONTEXT_CLIENT_VERSION":"')[1]?.split('"')[0] ?? body.split('"innertube_context_client_version":"')[1]?.split('"')[0] ?? "" + }, + id: data.title.runs[0].navigationEndpoint.watchEndpoint.playlistId, + title: data.title.runs[0].text, + videoCount: parseInt(videosCount) || 0, + lastUpdate: lastUpdate, + views: parseInt(views) || 0, + videos: videos, + url: `https://www.youtube.com/playlist?list=${data.title.runs[0].navigationEndpoint.watchEndpoint.playlistId}`, + link: `https://www.youtube.com${data.title.runs[0].navigationEndpoint.commandMetadata.webCommandMetadata.url}`, + author: author + ? { + name: author.videoOwnerRenderer.title.runs[0].text, + id: author.videoOwnerRenderer.title.runs[0].navigationEndpoint.browseEndpoint.browseId, + url: `https://www.youtube.com${author.videoOwnerRenderer.navigationEndpoint.commandMetadata.webCommandMetadata.url || author.videoOwnerRenderer.navigationEndpoint.browseEndpoint.canonicalBaseUrl}`, + icon: author.videoOwnerRenderer.thumbnail.thumbnails.length ? author.videoOwnerRenderer.thumbnail.thumbnails[author.videoOwnerRenderer.thumbnail.thumbnails.length - 1].url : null + } + : {}, + thumbnail: data.thumbnailRenderer.playlistVideoThumbnailRenderer?.thumbnail.thumbnails.length ? data.thumbnailRenderer.playlistVideoThumbnailRenderer.thumbnail.thumbnails[data.thumbnailRenderer.playlistVideoThumbnailRenderer.thumbnail.thumbnails.length - 1].url : null + }); + return res; +} + +export function getPlaylistVideos(data:any, limit : number = Infinity) : Video[] { + const videos = []; + + for (let i = 0; i < data.length; i++) { + if (limit === videos.length) break; + const info = data[i].playlistVideoRenderer; + if (!info || !info.shortBylineText) continue; + + videos.push( + new Video({ + id: info.videoId, + index: parseInt(info.index?.simpleText) || 0, + duration: parseDuration(info.lengthText?.simpleText) || 0, + duration_raw: info.lengthText?.simpleText ?? "0:00", + thumbnail: { + id: info.videoId, + url: info.thumbnail.thumbnails[info.thumbnail.thumbnails.length - 1].url, + height: info.thumbnail.thumbnails[info.thumbnail.thumbnails.length - 1].height, + width: info.thumbnail.thumbnails[info.thumbnail.thumbnails.length - 1].width + }, + title: info.title.runs[0].text, + channel: { + id: info.shortBylineText.runs[0].navigationEndpoint.browseEndpoint.browseId || undefined, + name: info.shortBylineText.runs[0].text || undefined, + url: `https://www.youtube.com${info.shortBylineText.runs[0].navigationEndpoint.browseEndpoint.canonicalBaseUrl || info.shortBylineText.runs[0].navigationEndpoint.commandMetadata.webCommandMetadata.url}`, + icon: undefined + } + }) + ); + } + return videos +} + +function parseDuration(duration: string): number { + duration ??= "0:00"; + const args = duration.split(":"); + let dur = 0; + + switch (args.length) { + case 3: + dur = parseInt(args[0]) * 60 * 60 + parseInt(args[1]) * 60 + parseInt(args[2]); + break; + case 2: + dur = parseInt(args[0]) * 60 + parseInt(args[1]); + break; + default: + dur = parseInt(args[0]); + } + + return dur; +} + + +export function getContinuationToken(data:any): string { + const continuationToken = data.find((x: any) => Object.keys(x)[0] === "continuationItemRenderer")?.continuationItemRenderer.continuationEndpoint?.continuationCommand?.token; + return continuationToken; +} \ No newline at end of file diff --git a/node-youtube-dl/YouTube/utils/index.ts b/node-youtube-dl/YouTube/utils/index.ts new file mode 100644 index 0000000..52ec51e --- /dev/null +++ b/node-youtube-dl/YouTube/utils/index.ts @@ -0,0 +1 @@ +export { video_basic_info, video_info, playlist_info } from './extractor' \ No newline at end of file diff --git a/node-youtube-dl/YouTube/utils/parser.ts b/node-youtube-dl/YouTube/utils/parser.ts new file mode 100644 index 0000000..b0bdfc1 --- /dev/null +++ b/node-youtube-dl/YouTube/utils/parser.ts @@ -0,0 +1,175 @@ +import { Video } from "../classes/Video"; +import { PlayList } from "../classes/Playlist"; +import { Channel } from "../classes/Channel"; + +export interface ParseSearchInterface { + type?: "video" | "playlist" | "channel" | "all"; + limit?: number; +} + +export function ParseSearchResult(html :string, options? : ParseSearchInterface): (Video | PlayList | Channel)[] { + if(!html) throw new Error('Can\'t parse Search result without data') + if (!options) options = { type: "video", limit: 0 }; + if (!options.type) options.type = "video"; + + let results = [] + let details = [] + let fetched = false; + + try { + let data = html.split("ytInitialData = JSON.parse('")[1].split("');")[0]; + html = data.replace(/\\x([0-9A-F]{2})/gi, (...items) => { + return String.fromCharCode(parseInt(items[1], 16)); + }); + } catch { + /* do nothing */ + } + + try { + details = JSON.parse(html.split('{"itemSectionRenderer":{"contents":')[html.split('{"itemSectionRenderer":{"contents":').length - 1].split(',"continuations":[{')[0]); + fetched = true; + } catch { + /* Do nothing*/ + } + + if (!fetched) { + try { + details = JSON.parse(html.split('{"itemSectionRenderer":')[html.split('{"itemSectionRenderer":').length - 1].split('},{"continuationItemRenderer":{')[0]).contents; + fetched = true; + } catch { + /* do nothing */ + } + } + + if (!fetched) throw new Error('Failed to Fetch the data') + + for (let i = 0; i < details.length; i++) { + if (typeof options.limit === "number" && options.limit > 0 && results.length >= options.limit) break; + let data = details[i]; + let res; + if (options.type === "all") { + if (!!data.videoRenderer) options.type = "video"; + else if (!!data.channelRenderer) options.type = "channel"; + else if (!!data.playlistRenderer) options.type = "playlist"; + else continue; + } + + if (options.type === "video") { + const parsed = parseVideo(data); + if (!parsed) continue; + res = parsed; + } else if (options.type === "channel") { + const parsed = parseChannel(data); + if (!parsed) continue; + res = parsed; + } else if (options.type === "playlist") { + const parsed = parsePlaylist(data); + if (!parsed) continue; + res = parsed; + } + + results.push(res); + } + +return results as (Video | Channel | PlayList)[]; +} + +function parseDuration(duration: string): number { + duration ??= "0:00"; + const args = duration.split(":"); + let dur = 0; + + switch (args.length) { + case 3: + dur = parseInt(args[0]) * 60 * 60 + parseInt(args[1]) * 60 + parseInt(args[2]); + break; + case 2: + dur = parseInt(args[0]) * 60 + parseInt(args[1]); + break; + default: + dur = parseInt(args[0]); + } + + return dur; +} + +export function parseChannel(data?: any): Channel | void { + if (!data || !data.channelRenderer) return; + const badge = data.channelRenderer.ownerBadges && data.channelRenderer.ownerBadges[0]; + let url = `https://www.youtube.com${data.channelRenderer.navigationEndpoint.browseEndpoint.canonicalBaseUrl || data.channelRenderer.navigationEndpoint.commandMetadata.webCommandMetadata.url}`; + let res = new Channel({ + id: data.channelRenderer.channelId, + name: data.channelRenderer.title.simpleText, + icon: { + url : data.channelRenderer.thumbnail.thumbnails[data.channelRenderer.thumbnail.thumbnails.length - 1].url.replace('//', 'https://'), + width : data.channelRenderer.thumbnail.thumbnails[data.channelRenderer.thumbnail.thumbnails.length - 1].width, + height: data.channelRenderer.thumbnail.thumbnails[data.channelRenderer.thumbnail.thumbnails.length - 1].height + }, + url: url, + verified: Boolean(badge?.metadataBadgeRenderer?.style?.toLowerCase().includes("verified")), + subscribers: (data.channelRenderer.subscriberCountText?.simpleText) ? data.channelRenderer.subscriberCountText.simpleText : '0 subscribers' + }); + + return res; +} + +export function parseVideo(data?: any): Video | void { + if (!data || !data.videoRenderer) return; + + const badge = data.videoRenderer.ownerBadges && data.videoRenderer.ownerBadges[0]; + let res = new Video({ + id: data.videoRenderer.videoId, + url: `https://www.youtube.com/watch?v=${data.videoRenderer.videoId}`, + title: data.videoRenderer.title.runs[0].text, + description: data.videoRenderer.descriptionSnippet && data.videoRenderer.descriptionSnippet.runs[0] ? data.videoRenderer.descriptionSnippet.runs[0].text : "", + duration: data.videoRenderer.lengthText ? parseDuration(data.videoRenderer.lengthText.simpleText) : 0, + duration_raw: data.videoRenderer.lengthText ? data.videoRenderer.lengthText.simpleText : null, + thumbnail: { + id: data.videoRenderer.videoId, + url: data.videoRenderer.thumbnail.thumbnails[data.videoRenderer.thumbnail.thumbnails.length - 1].url, + height: data.videoRenderer.thumbnail.thumbnails[data.videoRenderer.thumbnail.thumbnails.length - 1].height, + width: data.videoRenderer.thumbnail.thumbnails[data.videoRenderer.thumbnail.thumbnails.length - 1].width + }, + channel: { + id: data.videoRenderer.ownerText.runs[0].navigationEndpoint.browseEndpoint.browseId || null, + name: data.videoRenderer.ownerText.runs[0].text || null, + url: `https://www.youtube.com${data.videoRenderer.ownerText.runs[0].navigationEndpoint.browseEndpoint.canonicalBaseUrl || data.videoRenderer.ownerText.runs[0].navigationEndpoint.commandMetadata.webCommandMetadata.url}`, + icon: { + url: data.videoRenderer.channelThumbnailSupportedRenderers.channelThumbnailWithLinkRenderer.thumbnail.thumbnails[0].url, + width: data.videoRenderer.channelThumbnailSupportedRenderers.channelThumbnailWithLinkRenderer.thumbnail.thumbnails[0].width, + height: data.videoRenderer.channelThumbnailSupportedRenderers.channelThumbnailWithLinkRenderer.thumbnail.thumbnails[0].height + }, + verified: Boolean(badge?.metadataBadgeRenderer?.style?.toLowerCase().includes("verified")) + }, + uploadedAt: data.videoRenderer.publishedTimeText?.simpleText ?? null, + views: data.videoRenderer.viewCountText?.simpleText?.replace(/[^0-9]/g, "") ?? 0 + }); + + return res; +} + +export function parsePlaylist(data?: any): PlayList | void { + if (!data.playlistRenderer) return; + + const res = new PlayList( + { + id: data.playlistRenderer.playlistId, + title: data.playlistRenderer.title.simpleText, + thumbnail: { + id: data.playlistRenderer.playlistId, + url: data.playlistRenderer.thumbnails[0].thumbnails[data.playlistRenderer.thumbnails[0].thumbnails.length - 1].url, + height: data.playlistRenderer.thumbnails[0].thumbnails[data.playlistRenderer.thumbnails[0].thumbnails.length - 1].height, + width: data.playlistRenderer.thumbnails[0].thumbnails[data.playlistRenderer.thumbnails[0].thumbnails.length - 1].width + }, + channel: { + id: data.playlistRenderer.shortBylineText.runs[0].navigationEndpoint.browseEndpoint.browseId, + name: data.playlistRenderer.shortBylineText.runs[0].text, + url: `https://www.youtube.com${data.playlistRenderer.shortBylineText.runs[0].navigationEndpoint.commandMetadata.webCommandMetadata.url}` + }, + videos: parseInt(data.playlistRenderer.videoCount.replace(/[^0-9]/g, "")) + }, + true + ); + + return res; +} \ No newline at end of file diff --git a/node-youtube-dl/YouTube/utils/request.ts b/node-youtube-dl/YouTube/utils/request.ts new file mode 100644 index 0000000..89e22d1 --- /dev/null +++ b/node-youtube-dl/YouTube/utils/request.ts @@ -0,0 +1,12 @@ +import fetch, { RequestInit } from 'node-fetch' + +export async function url_get (url : string, options? : RequestInit) : Promise{ + return new Promise(async(resolve, reject) => { + let response = await fetch(url, options) + + if(response.status === 200) { + resolve(await response.text()) + } + else reject(`Got ${response.status} from ${url}`) + }) +} \ No newline at end of file diff --git a/node-youtube-dl/index.ts b/node-youtube-dl/index.ts new file mode 100644 index 0000000..c2176cf --- /dev/null +++ b/node-youtube-dl/index.ts @@ -0,0 +1,12 @@ +//This File is in testing stage, everything will change in this +import { playlist_info, video_basic_info, video_info, search } from "./YouTube"; + +let main = async() => { + let time_start = Date.now() + let result = await search('Rick Roll') + console.log(result[0].url) + let time_end = Date.now() + console.log(`Time Taken : ${(time_end - time_start)/1000} seconds`) +} + +main() \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..81a5c9b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,175 @@ +{ + "name": "killer", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "killer", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "node-fetch": "^2.6.1" + }, + "devDependencies": { + "@types/node-fetch": "^2.5.12" + } + }, + "node_modules/@types/node": { + "version": "16.4.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.13.tgz", + "integrity": "sha512-bLL69sKtd25w7p1nvg9pigE4gtKVpGTPojBFLMkGHXuUgap2sLqQt2qUnqmVCDfzGUL0DRNZP+1prIZJbMeAXg==", + "dev": true + }, + "node_modules/@types/node-fetch": { + "version": "2.5.12", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.12.tgz", + "integrity": "sha512-MKgC4dlq4kKNa/mYrwpKfzQMB5X3ee5U6fSprkKpToBqBmX4nFZL9cW5jl6sWn+xpRJ7ypWh2yyqqr8UUCstSw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "form-data": "^3.0.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/mime-db": { + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz", + "integrity": "sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.32", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.32.tgz", + "integrity": "sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A==", + "dev": true, + "dependencies": { + "mime-db": "1.49.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", + "engines": { + "node": "4.x || >=6.0.0" + } + } + }, + "dependencies": { + "@types/node": { + "version": "16.4.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.13.tgz", + "integrity": "sha512-bLL69sKtd25w7p1nvg9pigE4gtKVpGTPojBFLMkGHXuUgap2sLqQt2qUnqmVCDfzGUL0DRNZP+1prIZJbMeAXg==", + "dev": true + }, + "@types/node-fetch": { + "version": "2.5.12", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.12.tgz", + "integrity": "sha512-MKgC4dlq4kKNa/mYrwpKfzQMB5X3ee5U6fSprkKpToBqBmX4nFZL9cW5jl6sWn+xpRJ7ypWh2yyqqr8UUCstSw==", + "dev": true, + "requires": { + "@types/node": "*", + "form-data": "^3.0.0" + } + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "mime-db": { + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz", + "integrity": "sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA==", + "dev": true + }, + "mime-types": { + "version": "2.1.32", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.32.tgz", + "integrity": "sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A==", + "dev": true, + "requires": { + "mime-db": "1.49.0" + } + }, + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..27a6b00 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "killer", + "version": "1.0.0", + "description": "", + "main": "dist/index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/killer069/node-youtube-dl.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/killer069/node-youtube-dl/issues" + }, + "homepage": "https://github.com/killer069/node-youtube-dl#readme", + "dependencies": { + "node-fetch": "^2.6.1" + }, + "devDependencies": { + "@types/node-fetch": "^2.5.12" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..9ef05e0 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "strict": true, + "moduleResolution": "node", + "removeComments": false, + "alwaysStrict": true, + "pretty": true, + "target": "es2019", + "lib": ["ESNext"], + "sourceMap": true, + "inlineSources": true, + "module": "commonjs", + "declaration": true, + "declarationMap": true, + "outDir": "dist", + "incremental": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true + }, + "include": ["node-youtube-dl/**/**/*.ts"], + "exclude": ["node-youtube-dl/**/__tests__"] +} \ No newline at end of file