From 4608f3d59b883d4ecbc53ebeaba77580d53d0c7e Mon Sep 17 00:00:00 2001 From: killer069 <65385476+killer069@users.noreply.github.com> Date: Fri, 8 Oct 2021 14:58:06 +0530 Subject: [PATCH] Cookies update --- play-dl/Request/classes.ts | 110 ++++++++++++++++++++++++++ play-dl/Request/index.ts | 53 +++++++++++++ play-dl/YouTube/classes/LiveStream.ts | 5 +- play-dl/YouTube/stream.ts | 5 +- play-dl/YouTube/utils/cookie.ts | 31 ++++++++ play-dl/YouTube/utils/extractor.ts | 9 +-- play-dl/YouTube/utils/request.ts | 41 +++++++++- play-dl/index.ts | 26 +++++- 8 files changed, 257 insertions(+), 23 deletions(-) create mode 100644 play-dl/Request/classes.ts create mode 100644 play-dl/Request/index.ts create mode 100644 play-dl/YouTube/utils/cookie.ts diff --git a/play-dl/Request/classes.ts b/play-dl/Request/classes.ts new file mode 100644 index 0000000..4fc96a2 --- /dev/null +++ b/play-dl/Request/classes.ts @@ -0,0 +1,110 @@ +import tls , { TLSSocket } from "tls"; +import { URL } from "url"; +import { Timer } from "../YouTube/classes/LiveStream"; + +interface ResponseOptions extends tls.ConnectionOptions{ + body?: string; + method: 'GET' | 'POST'; + cookies? : boolean + headers? : Object; + timeout? : number +} + +export class Response { + parsed_url : URL + statusCode : number; + rawHeaders : string; + headers : Object; + body : string; + socket : TLSSocket; + sentHeaders : string; + sentBody : string; + private options : ResponseOptions; + private timer : Timer | null + constructor(req_url : string, options : ResponseOptions){ + this.parsed_url = new URL(req_url) + this.sentHeaders = '' + this.statusCode = 0 + this.sentBody = "" + this.rawHeaders = '' + this.body = '' + this.headers = {} + this.timer = null + this.options = options + this.socket = tls.connect({ + host : this.parsed_url.hostname, + port : Number(this.parsed_url.port) || 443, + socket : options.socket, + rejectUnauthorized : false + }, () => this.onConnect()) + if(options.headers){ + for(const [ key, value ] of Object.entries(options.headers)){ + this.sentHeaders += `${key}: ${value}\r\n` + } + } + if(options.body) this.sentBody = options.body + } + + private onConnect(){ + this.socket.write( + `${this.options.method} ${this.parsed_url.pathname}${this.parsed_url.search} HTTP/1.1\r\n` + + `Host : ${this.parsed_url.hostname}\r\n` + + this.sentHeaders + + `Connection: close\r\n` + + `\r\n` + + this.sentBody + ) + } + + private parseHeaders(){ + const head_arr = this.rawHeaders.split('\r\n') + this.statusCode = Number(head_arr.shift()?.split(' ')[1]) ?? -1 + for(const head of head_arr){ + let [ key, value ] = head.split(': ') + if(!value) break; + key = key.trim().toLowerCase() + value = value.trim() + if(Object.keys(this.headers).includes(key)){ + let val = (this.headers as any)[key] + if(typeof val === 'string') val = [val] + Object.assign(this.headers, { [key]: [...val, value] }) + } + else Object.assign(this.headers, { [key] : value }) + } + } + + stream(): Promise{ + return new Promise((resolve, reject) => { + this.timer = new Timer(() => this.socket.end(), this.options.timeout || 1) + this.socket.once('error', (err) => reject(err)) + this.socket.once('data', (chunk) => { + this.rawHeaders = chunk.toString('utf-8') + this.parseHeaders() + resolve(this.socket) + }) + this.socket.on('data', () => this.timer?.reuse()) + this.socket.once('end', () => this.timer?.destroy()) + }) + } + + fetch(): Promise{ + return new Promise((resolve, reject) => { + this.socket.setEncoding('utf-8'); + this.socket.once('error', (err) => reject(err)) + this.socket.on('data', (chunk: string) => { + if(this.rawHeaders.length === 0){ + this.rawHeaders = chunk + this.parseHeaders() + } + else { + const arr = chunk.split('\r\n') + if(arr.length > 1 && arr[0].length < 5) arr.shift() + this.body += arr.join('') + } + }) + this.socket.on('end', () => { + resolve(this) + }) + }) + } +} \ No newline at end of file diff --git a/play-dl/Request/index.ts b/play-dl/Request/index.ts new file mode 100644 index 0000000..97bec5e --- /dev/null +++ b/play-dl/Request/index.ts @@ -0,0 +1,53 @@ +import { Response } from "./classes"; + + +export type Proxy = ProxyOpts | string; + +interface ProxyOpts { + host: string; + port: number; + authentication?: { + username: string; + password: string; + }; +} + +interface RequestOptions { + body?: string; + method: 'GET' | 'POST'; + proxies?: Proxy[]; + cookies? : boolean + headers? : Object; + timeout? : number +} + +interface StreamGetterOptions{ + method: 'GET' | 'POST'; + cookies? : boolean + headers : Object; +} + +export function request_stream(req_url : string, options : RequestOptions = {method : "GET"}): Promise{ + return new Promise(async(resolve, reject) => { + let res = new Response(req_url, options) + await res.stream() + if (res.statusCode >= 300 && res.statusCode < 400) { + res = await request_stream((res.headers as any).location, options); + await res.stream() + } + resolve(res) + }) +} + +export function request(req_url : string, options : RequestOptions = {method : "GET"}): Promise{ + return new Promise(async(resolve, reject) => { + let res = new Response(req_url, options) + await res.fetch() + if (Number(res.statusCode) >= 300 && Number(res.statusCode) < 400) { + res = await request((res.headers as any).location, options); + } else if (Number(res.statusCode) > 400) { + reject(new Error(`Got ${res.statusCode} from the request`)); + } + resolve(res) + }) +} \ No newline at end of file diff --git a/play-dl/YouTube/classes/LiveStream.ts b/play-dl/YouTube/classes/LiveStream.ts index 9b2d20f..aaf7f3d 100644 --- a/play-dl/YouTube/classes/LiveStream.ts +++ b/play-dl/YouTube/classes/LiveStream.ts @@ -130,7 +130,6 @@ export class Stream { private per_sec_bytes: number; private content_length: number; private video_url: string; - private cookie: string; private timer: Timer; private quality: number; private proxy: Proxy[] | undefined; @@ -141,7 +140,6 @@ export class Stream { duration: number, contentLength: number, video_url: string, - cookie: string, options: StreamOptions ) { this.stream = new PassThrough({ highWaterMark: 10 * 1000 * 1000 }); @@ -151,7 +149,6 @@ export class Stream { this.type = type; this.bytes_count = 0; this.video_url = video_url; - this.cookie = cookie; this.per_sec_bytes = Math.ceil(contentLength / duration); this.content_length = contentLength; this.request = null; @@ -166,7 +163,7 @@ export class Stream { } private async retry() { - const info = await video_info(this.video_url, { cookie: this.cookie, proxy: this.proxy }); + const info = await video_info(this.video_url, { proxy: this.proxy }); const audioFormat = parseAudioFormats(info.format); this.url = audioFormat[this.quality].url; } diff --git a/play-dl/YouTube/stream.ts b/play-dl/YouTube/stream.ts index c87f683..674bc9c 100644 --- a/play-dl/YouTube/stream.ts +++ b/play-dl/YouTube/stream.ts @@ -12,7 +12,6 @@ export enum StreamType { export interface StreamOptions { quality?: number; - cookie?: string; proxy?: Proxy[]; } @@ -54,7 +53,7 @@ export type YouTubeStream = Stream | LiveStreaming; * @returns Stream class with type and stream for playing. */ export async function stream(url: string, options: StreamOptions = {}): Promise { - const info = await video_info(url, { cookie: options.cookie, proxy: options.proxy }); + const info = await video_info(url, { proxy: options.proxy }); const final: any[] = []; if ( info.LiveStreamData.isLive === true && @@ -83,7 +82,6 @@ export async function stream(url: string, options: StreamOptions = {}): Promise< info.video_details.durationInSec, Number(final[0].contentLength), info.video_details.url, - options.cookie as string, options ); } @@ -122,7 +120,6 @@ export async function stream_from_info(info: InfoData, options: StreamOptions = info.video_details.durationInSec, Number(final[0].contentLength), info.video_details.url, - options.cookie as string, options ); } diff --git a/play-dl/YouTube/utils/cookie.ts b/play-dl/YouTube/utils/cookie.ts new file mode 100644 index 0000000..28a19da --- /dev/null +++ b/play-dl/YouTube/utils/cookie.ts @@ -0,0 +1,31 @@ +import fs from 'fs'; + +let youtubeData: youtubeDataOptions; +if (fs.existsSync('.data/youtube.data')) { + youtubeData = JSON.parse(fs.readFileSync('.data/youtube.data').toString()); +} + +interface youtubeDataOptions { + cookie?: Object; +} + +export function getCookies(): undefined | string { + let result = '' + if(!youtubeData?.cookie) return undefined + for (const [ key, value ] of Object.entries(youtubeData.cookie)){ + result+= `${key}=${value};` + } + return result; +} + +export function setCookie(key: string, value: string): boolean { + if (!youtubeData?.cookie) return false; + key = key.trim() + value = value.trim() + Object.assign(youtubeData.cookie, { [key] : value }) + return true +} + +export function uploadCookie() { + if(youtubeData) fs.writeFileSync('.data/youtube.data', JSON.stringify(youtubeData, undefined, 4)); +} diff --git a/play-dl/YouTube/utils/extractor.ts b/play-dl/YouTube/utils/extractor.ts index e30395e..8701d6f 100644 --- a/play-dl/YouTube/utils/extractor.ts +++ b/play-dl/YouTube/utils/extractor.ts @@ -4,7 +4,6 @@ import { YouTubeVideo } from '../classes/Video'; import { YouTubePlayList } from '../classes/Playlist'; interface InfoOptions { - cookie?: string; proxy?: Proxy[]; } @@ -78,12 +77,8 @@ export async function video_basic_info(url: string, options: InfoOptions = {}) { const new_url = `https://www.youtube.com/watch?v=${video_id}&has_verified=1`; const body = await request(new_url, { proxies: options.proxy ?? undefined, - headers: options.cookie - ? { - 'cookie': options.cookie, - 'accept-language': 'en-US,en-IN;q=0.9,en;q=0.8,hi;q=0.7' - } - : { 'accept-language': 'en-US,en-IN;q=0.9,en;q=0.8,hi;q=0.7' } + headers: { 'accept-language': 'en-US,en-IN;q=0.9,en;q=0.8,hi;q=0.7' }, + cookies : true }); const player_response = JSON.parse(body.split('var ytInitialPlayerResponse = ')[1].split('}};')[0] + '}}'); const initial_response = JSON.parse(body.split('var ytInitialData = ')[1].split('}};')[0] + '}}'); diff --git a/play-dl/YouTube/utils/request.ts b/play-dl/YouTube/utils/request.ts index 73a3ce8..29e6a49 100644 --- a/play-dl/YouTube/utils/request.ts +++ b/play-dl/YouTube/utils/request.ts @@ -2,6 +2,7 @@ import https, { RequestOptions } from 'https'; import tls from 'tls'; import http, { ClientRequest, IncomingMessage } from 'http'; import { URL } from 'url'; +import { getCookies, setCookie, uploadCookie } from './cookie'; /** * Types for Proxy */ @@ -18,7 +19,7 @@ interface ProxyOpts { interface ProxyOutput { statusCode: number; - head: string; + head: string[]; body: string; } @@ -26,6 +27,7 @@ interface RequestOpts extends RequestOptions { body?: string; method?: 'GET' | 'POST'; proxies?: Proxy[]; + cookies? : boolean } /** * Main module that play-dl uses for making a https request @@ -135,7 +137,7 @@ async function proxy_getter(req_url: string, req_proxy: Proxy[]): Promise { +export async function request(url: string, options: RequestOpts = {}): Promise { return new Promise(async (resolve, reject) => { if (!options?.proxies || options.proxies.length === 0) { let data = ''; + let cook = getCookies() + if (typeof cook === 'string' && options.headers) { + Object.assign(options.headers, { cookie : cook }); + } let res = await https_getter(url, options).catch((err: Error) => err); if (res instanceof Error) { reject(res); return; } + if(res.headers && res.headers['set-cookie'] && cook){ + res.headers['set-cookie'].forEach((x) => { + x.split(';').forEach((x) => { + const [key, value] = x.split('='); + if (!value) return; + setCookie(key, value); + }); + }) + uploadCookie() + } if (Number(res.statusCode) >= 300 && Number(res.statusCode) < 400) { res = await https_getter(res.headers.location as string, options); } else if (Number(res.statusCode) > 400) { @@ -168,13 +184,30 @@ export async function request(url: string, options?: RequestOpts): Promise (data += c)); res.on('end', () => resolve(data)); } else { + let cook = getCookies() + if (typeof cook === 'string' && options.headers) { + Object.assign(options.headers, { cookie : cook }); + } let res = await proxy_getter(url, options.proxies).catch((e: Error) => e); if (res instanceof Error) { reject(res); return; } + if(res.head && cook){ + let cookies = res.head.filter((x) => x.toLocaleLowerCase().startsWith('set-cookie: ')); + cookies.forEach((x) => { + x.toLocaleLowerCase().split('set-cookie: ')[1].split(';').forEach((y) => { + let [key, value] = y.split('='); + if (!value) + return; + setCookie(key, value); + }); + }); + uploadCookie() + } if (res.statusCode >= 300 && res.statusCode < 400) { - res = await proxy_getter(res.head.split('Location: ')[1].split('\n')[0], options.proxies); + let url = res.head.filter((x) => x.startsWith('Location: ')); + res = await proxy_getter(url[0].split('\n')[0], options.proxies); } else if (res.statusCode > 400) { reject(new Error(`GOT ${res.statusCode} from proxy request`)); } diff --git a/play-dl/index.ts b/play-dl/index.ts index 85d3e47..d63f022 100644 --- a/play-dl/index.ts +++ b/play-dl/index.ts @@ -111,7 +111,7 @@ export function authorization(): void { input: process.stdin, output: process.stdout }); - ask.question('Choose your service - sc (for SoundCloud) / sp (for Spotify) : ', (msg) => { + ask.question('Choose your service - sc (for SoundCloud) / sp (for Spotify) / yo (for YouTube): ', (msg) => { if (msg.toLowerCase().startsWith('sp')) { let client_id: string, client_secret: string, redirect_url: string, market: string; ask.question('Start by entering your Client ID : ', (id) => { @@ -157,9 +157,8 @@ export function authorization(): void { }); }); } else if (msg.toLowerCase().startsWith('sc')) { - let client_id: string; ask.question('Client ID : ', async (id) => { - client_id = id; + let client_id = id; if (!client_id) { console.log("You didn't provide a client ID. Try again..."); ask.close(); @@ -173,6 +172,25 @@ export function authorization(): void { } else console.log("That doesn't look like a valid client ID. Retry with a correct client ID."); ask.close(); }); + } else if (msg.toLowerCase().startsWith('yo')) { + ask.question('Cookies : ', (cook: string) => { + if (!cook || cook.length === 0) { + console.log("You didn't provide a cookie. Try again..."); + ask.close(); + return; + } + if (!fs.existsSync('.data')) fs.mkdirSync('.data'); + console.log('Cookies has been added successfully.'); + let cookie: Object = {}; + cook.split(';').forEach((x) => { + let [ key, value ] = x.split('=') + key = key.trim() + value = value.trim() + Object.assign(cookie, { [key] : value }) + }) + fs.writeFileSync('.data/youtube.data', JSON.stringify({ cookie }, undefined, 4)); + ask.close(); + }); } else { console.log("That option doesn't exist. Try again..."); ask.close(); @@ -189,4 +207,4 @@ export function attachListeners(player: EventEmitter, resource: YouTubeStream | player.removeListener(AudioPlayerStatus.AutoPaused, () => resource.pause()); player.removeListener(AudioPlayerStatus.Playing, () => resource.resume()); }); -} +}; \ No newline at end of file