diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..9a4dab9 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "printWidth": 120, + "tabWidth": 4, + "singleQuote": true, + "quoteProps": "consistent", + "semi": true, + "embeddedLanguageFormatting": "off", + "trailingComma": "none" +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 261d638..6c4fda0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "0.9.9", "license": "MIT", "devDependencies": { - "@types/node": "^16.9.1" + "@types/node": "^16.9.1", + "prettier": "^2.3.1" }, "engines": { "node": ">=16.0.0" @@ -20,6 +21,18 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz", "integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==", "dev": true + }, + "node_modules/prettier": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.4.1.tgz", + "integrity": "sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + } } }, "dependencies": { @@ -28,6 +41,12 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz", "integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==", "dev": true + }, + "prettier": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.4.1.tgz", + "integrity": "sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA==", + "dev": true } } } diff --git a/package.json b/package.json index fbb1236..b41f164 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,9 @@ }, "scripts": { "build": "tsc", - "build:check": "tsc --noEmit --incremental false" + "build:check": "tsc --noEmit --incremental false", + "pretty": "prettier --config .prettierrc \"play-dl/*.ts\" \"play-dl/*/*.ts\" \"play-dl/*/*/*.ts\" --write ", + "lint": "eslint . --ext .ts" }, "repository": { "type": "git", @@ -39,6 +41,7 @@ "dist/*" ], "devDependencies": { - "@types/node": "^16.9.1" + "@types/node": "^16.9.1", + "prettier": "^2.3.1" } } diff --git a/play-dl/SoundCloud/classes.ts b/play-dl/SoundCloud/classes.ts new file mode 100644 index 0000000..3a2672d --- /dev/null +++ b/play-dl/SoundCloud/classes.ts @@ -0,0 +1,225 @@ +import { request, request_stream } from '../YouTube/utils/request'; +import { PassThrough } from 'stream'; +import { IncomingMessage } from 'http'; +import { StreamType } from '../YouTube/stream'; + +interface SoundCloudUser { + name: string; + id: string; + url: string; + type: 'track' | 'playlist' | 'user'; + verified: boolean; + description: string; + first_name: string; + full_name: string; + last_name: string; + thumbnail: string; +} + +interface SoundCloudTrackDeprecated { + fetched: boolean; + id: number; + type: 'track'; +} + +interface SoundCloudTrackFormat { + url: string; + preset: string; + duration: number; + format: { + protocol: string; + mime_type: string; + }; + quality: string; +} + +export class SoundCloudTrack { + name: string; + id: number; + url: string; + fetched: boolean; + type: 'track' | 'playlist' | 'user'; + durationInSec: number; + durationInMs: number; + formats: SoundCloudTrackFormat[]; + publisher: { + name: string; + id: number; + artist: string; + contains_music: boolean; + writer_composer: string; + }; + thumbanil: string; + user: SoundCloudUser; + constructor(data: any) { + this.name = data.title; + this.id = data.id; + this.url = data.uri; + this.fetched = true; + this.type = 'track'; + this.durationInSec = Number(data.duration) / 1000; + this.durationInMs = Number(data.duration); + this.publisher = { + name: data.publisher_metadata.publisher, + id: data.publisher_metadata.id, + artist: data.publisher_metadata.artist, + contains_music: Boolean(data.publisher_metadata.contains_music) || false, + writer_composer: data.publisher_metadata.writer_composer + }; + this.formats = data.media.transcodings; + this.user = { + name: data.user.username, + id: data.user.id, + type: 'user', + url: data.user.permalink_url, + verified: Boolean(data.user.verified) || false, + description: data.user.description, + first_name: data.user.first_name, + full_name: data.user.full_name, + last_name: data.user.last_name, + thumbnail: data.user.avatar_url + }; + this.thumbanil = data.artwork_url; + } +} + +export class SoundCloudPlaylist { + name: string; + id: number; + url: string; + type: 'track' | 'playlist' | 'user'; + sub_type: string; + durationInSec: number; + durationInMs: number; + client_id: string; + user: SoundCloudUser; + tracks: SoundCloudTrack[] | SoundCloudTrackDeprecated[]; + tracksCount: number; + constructor(data: any, client_id: string) { + this.name = data.title; + this.id = data.id; + this.url = data.uri; + this.client_id = client_id; + this.type = 'playlist'; + this.sub_type = data.set_type; + this.durationInSec = Number(data.duration) / 1000; + this.durationInMs = Number(data.duration); + this.user = { + name: data.user.username, + id: data.user.id, + type: 'user', + url: data.user.permalink_url, + verified: Boolean(data.user.verified) || false, + description: data.user.description, + first_name: data.user.first_name, + full_name: data.user.full_name, + last_name: data.user.last_name, + thumbnail: data.user.avatar_url + }; + this.tracksCount = data.track_count; + const tracks: any[] = []; + data.tracks.forEach((track: any) => { + if (track.title) { + tracks.push(new SoundCloudTrack(track)); + } else + tracks.push({ + id: track.id, + fetched: false, + type: 'track' + }); + }); + this.tracks = tracks; + } + + async fetch() { + const work: any[] = []; + for (let i = 0; i < this.tracks.length; i++) { + if (!this.tracks[i].fetched) { + work.push( + new Promise(async (resolve, reject) => { + const num = i; + const data = await request( + `https://api-v2.soundcloud.com/tracks/${this.tracks[i].id}?client_id=${this.client_id}` + ); + + this.tracks[num] = new SoundCloudTrack(JSON.parse(data)); + resolve(''); + }) + ); + } + } + await Promise.allSettled(work); + } +} + +export class Stream { + type: StreamType; + stream: PassThrough; + private url: string; + private playing_count: number; + private downloaded_time: number; + private request : IncomingMessage | null + private time: number[]; + private segment_urls: string[]; + constructor(url: string, type: StreamType = StreamType.Arbitrary, client_id: string) { + this.type = type; + this.url = url + client_id; + this.stream = new PassThrough({ highWaterMark: 10 * 1000 * 1000 }); + this.playing_count = 0; + this.downloaded_time = 0; + this.request = null + this.time = []; + this.segment_urls = []; + this.stream.on('close', () => { + this.cleanup(); + }); + this.stream.on('pause', () => { + this.playing_count++; + }); + this.start(); + } + + private async parser() { + const response = await request(this.url).catch((err: Error) => { + return err; + }); + if (response instanceof Error) throw response; + const array = response.split('\n'); + array.forEach((val) => { + if (val.startsWith('#EXTINF:')) { + this.time.push(parseFloat(val.replace('#EXTINF:', ''))); + } else if (val.startsWith('https')) { + this.segment_urls.push(val); + } + }); + return; + } + + private async start() { + if (this.stream.destroyed) { + this.cleanup(); + return; + } + await this.parser(); + for await (const segment of this.segment_urls) { + await new Promise(async (resolve, reject) => { + const stream = await request_stream(segment).catch((err: Error) => err); + if (stream instanceof Error) { + this.stream.emit('error', stream); + reject(stream) + return; + } + this.request = stream + stream.pipe(this.stream, { end : false }) + stream.on('end', () => { + resolve(''); + }); + stream.once('error', (err) => { + this.stream.emit('error', err); + }); + }); + } + } + + private cleanup() {} +} diff --git a/play-dl/SoundCloud/index.ts b/play-dl/SoundCloud/index.ts new file mode 100644 index 0000000..891a877 --- /dev/null +++ b/play-dl/SoundCloud/index.ts @@ -0,0 +1,43 @@ +import fs from 'fs'; +import { request } from '../YouTube/utils/request'; +import { SoundCloudPlaylist, SoundCloudTrack } from './classes'; + +let soundData: SoundDataOptions; +if (fs.existsSync('.data/soundcloud.data')) { + soundData = JSON.parse(fs.readFileSync('.data/soundcloud.data').toString()); +} + +interface SoundDataOptions { + client_id: string; +} + +const pattern = /^(?:(https?):\/\/)?(?:(?:www|m)\.)?(soundcloud\.com|snd\.sc)\/(.*)$/; + +export async function soundcloud(url: string): Promise { + if (!soundData) throw new Error('SoundCloud Data is missing\nDid you forgot to do authorization ?'); + if (!url.match(pattern)) throw new Error('This is not a SoundCloud URL'); + + const data = await request( + `https://api-v2.soundcloud.com/resolve?url=${url}&client_id=${soundData.client_id}` + ).catch((err: Error) => err); + + if (data instanceof Error) throw data; + + const json_data = JSON.parse(data); + + if (json_data.kind !== 'track' && json_data.kind !== 'playlist') + throw new Error('This url is out of scope for play-dl.'); + + if (json_data.kind === 'track') return new SoundCloudTrack(json_data); + else return new SoundCloudPlaylist(json_data, soundData.client_id); +} + +export async function check_id(id: string) { + const response = await request(`https://api-v2.soundcloud.com/search?client_id=${id}&q=Rick+Roll&limit=0`).catch( + (err: Error) => { + return err; + } + ); + if (response instanceof Error) return false; + else return true; +} diff --git a/play-dl/Spotify/classes.ts b/play-dl/Spotify/classes.ts index b939756..daf49d5 100644 --- a/play-dl/Spotify/classes.ts +++ b/play-dl/Spotify/classes.ts @@ -1,315 +1,328 @@ -import { request } from "../YouTube/utils/request"; -import { SpotifyDataOptions } from "."; +import { request } from '../YouTube/utils/request'; +import { SpotifyDataOptions } from '.'; - -interface SpotifyTrackAlbum{ - name : string; - url : string; - id : string; - release_date : string; - release_date_precision : string; - total_tracks : number; +interface SpotifyTrackAlbum { + name: string; + url: string; + id: string; + release_date: string; + release_date_precision: string; + total_tracks: number; } -interface SpotifyArtists{ - name : string; - url : string; - id : string; +interface SpotifyArtists { + name: string; + url: string; + id: string; } -interface SpotifyThumbnail{ - height : number; - width : number - url : string +interface SpotifyThumbnail { + height: number; + width: number; + url: string; } -interface SpotifyCopyright{ - text : string; - type : string; +interface SpotifyCopyright { + text: string; + type: string; } -export class SpotifyVideo{ - name : string; - type : "track" | "playlist" | "album" - id : string; - url : string; - explicit : boolean; - durationInSec : number; - durationInMs : number; - artists : SpotifyArtists[] - album : SpotifyTrackAlbum - thumbnail : SpotifyThumbnail - constructor(data : any){ - this.name = data.name - this.id = data.id - this.type = "track" - this.url = data.external_urls.spotify - this.explicit = data.explicit - this.durationInMs = data.duration_ms - this.durationInSec = Math.round(this.durationInMs/1000) - let artists : SpotifyArtists[] = [] - data.artists.forEach((v : any) => { +export class SpotifyVideo { + name: string; + type: 'track' | 'playlist' | 'album'; + id: string; + url: string; + explicit: boolean; + durationInSec: number; + durationInMs: number; + artists: SpotifyArtists[]; + album: SpotifyTrackAlbum; + thumbnail: SpotifyThumbnail; + constructor(data: any) { + this.name = data.name; + this.id = data.id; + this.type = 'track'; + this.url = data.external_urls.spotify; + this.explicit = data.explicit; + this.durationInMs = data.duration_ms; + this.durationInSec = Math.round(this.durationInMs / 1000); + const artists: SpotifyArtists[] = []; + data.artists.forEach((v: any) => { artists.push({ - name : v.name, - id : v.id, - url : v.external_urls.spotify - }) - }) - this.artists = artists + name: v.name, + id: v.id, + url: v.external_urls.spotify + }); + }); + this.artists = artists; this.album = { - name : data.album.name, - url : data.external_urls.spotify, - id : data.album.id, - release_date : data.album.release_date, - release_date_precision : data.album.release_date_precision, - total_tracks : data.album.total_tracks - } - this.thumbnail = data.album.images[0] + name: data.album.name, + url: data.external_urls.spotify, + id: data.album.id, + release_date: data.album.release_date, + release_date_precision: data.album.release_date_precision, + total_tracks: data.album.total_tracks + }; + this.thumbnail = data.album.images[0]; } - toJSON(){ + toJSON() { return { - name : this.name, - id : this.id, - type : this.type, - url : this.url, - explicit : this.explicit, - durationInMs : this.durationInMs, - durationInSec : this.durationInSec, - artists : this.artists, - album : this.album, - thumbnail : this.thumbnail - } + name: this.name, + id: this.id, + type: this.type, + url: this.url, + explicit: this.explicit, + durationInMs: this.durationInMs, + durationInSec: this.durationInSec, + artists: this.artists, + album: this.album, + thumbnail: this.thumbnail + }; } } -export class SpotifyPlaylist{ - name : string; - type : "track" | "playlist" | "album" - collaborative : boolean; - description : string; - url : string; - id : string; - thumbnail : SpotifyThumbnail; - owner : SpotifyArtists; - tracksCount : number; - private spotifyData : SpotifyDataOptions; - private fetched_tracks : Map - constructor(data : any, spotifyData : SpotifyDataOptions){ - this.name = data.name - this.type = "playlist" - this.collaborative = data.collaborative - this.description = data.description - this.url = data.external_urls.spotify - this.id = data.id - this.thumbnail = data.images[0] +export class SpotifyPlaylist { + name: string; + type: 'track' | 'playlist' | 'album'; + collaborative: boolean; + description: string; + url: string; + id: string; + thumbnail: SpotifyThumbnail; + owner: SpotifyArtists; + tracksCount: number; + private spotifyData: SpotifyDataOptions; + private fetched_tracks: Map; + constructor(data: any, spotifyData: SpotifyDataOptions) { + this.name = data.name; + this.type = 'playlist'; + this.collaborative = data.collaborative; + this.description = data.description; + this.url = data.external_urls.spotify; + this.id = data.id; + this.thumbnail = data.images[0]; this.owner = { - name : data.owner.display_name, - url : data.owner.external_urls.spotify, - id : data.owner.id - } - this.tracksCount = Number(data.tracks.total) - let videos: SpotifyVideo[] = [] - data.tracks.items.forEach((v : any) => { - videos.push(new SpotifyVideo(v.track)) - }) - this.fetched_tracks = new Map() - this.fetched_tracks.set('1', videos) - this.spotifyData = spotifyData + name: data.owner.display_name, + url: data.owner.external_urls.spotify, + id: data.owner.id + }; + this.tracksCount = Number(data.tracks.total); + const videos: SpotifyVideo[] = []; + data.tracks.items.forEach((v: any) => { + videos.push(new SpotifyVideo(v.track)); + }); + this.fetched_tracks = new Map(); + this.fetched_tracks.set('1', videos); + this.spotifyData = spotifyData; } - async fetch(){ - let fetching : number; - if(this.tracksCount > 1000) fetching = 1000 - else fetching = this.tracksCount - if(fetching <= 100) return - let work = [] - for(let i = 2; i <= Math.ceil(fetching/100); i++){ - work.push(new Promise(async (resolve, reject) => { - let response = await request(`https://api.spotify.com/v1/playlists/${this.id}/tracks?offset=${(i-1)*100}&limit=100&market=${this.spotifyData.market}`, { - headers : { - "Authorization" : `${this.spotifyData.token_type} ${this.spotifyData.access_token}` - } - }).catch((err) => reject(`Response Error : \n${err}`)) - let videos: SpotifyVideo[] = [] - if(typeof response !== 'string') return - let json_data = JSON.parse(response) - json_data.items.forEach((v : any) => { - videos.push(new SpotifyVideo(v.track)) + async fetch() { + let fetching: number; + if (this.tracksCount > 1000) fetching = 1000; + else fetching = this.tracksCount; + if (fetching <= 100) return; + const work = []; + for (let i = 2; i <= Math.ceil(fetching / 100); i++) { + work.push( + new Promise(async (resolve, reject) => { + const response = await request( + `https://api.spotify.com/v1/playlists/${this.id}/tracks?offset=${ + (i - 1) * 100 + }&limit=100&market=${this.spotifyData.market}`, + { + headers: { + Authorization: `${this.spotifyData.token_type} ${this.spotifyData.access_token}` + } + } + ).catch((err) => reject(`Response Error : \n${err}`)); + const videos: SpotifyVideo[] = []; + if (typeof response !== 'string') return; + const json_data = JSON.parse(response); + json_data.items.forEach((v: any) => { + videos.push(new SpotifyVideo(v.track)); + }); + this.fetched_tracks.set(`${i}`, videos); + resolve('Success'); }) - this.fetched_tracks.set(`${i}`, videos) - resolve('Success') - })) + ); } - await Promise.allSettled(work) - return this + await Promise.allSettled(work); + return this; } - page(num : number){ - if(!num) throw new Error('Page number is not provided') - if(!this.fetched_tracks.has(`${num}`)) throw new Error('Given Page number is invalid') - return this.fetched_tracks.get(`${num}`) + page(num: number) { + if (!num) throw new Error('Page number is not provided'); + if (!this.fetched_tracks.has(`${num}`)) throw new Error('Given Page number is invalid'); + return this.fetched_tracks.get(`${num}`); } - get total_pages(){ - return this.fetched_tracks.size + get total_pages() { + return this.fetched_tracks.size; } - get total_tracks(){ - let page_number: number = this.total_pages - return (page_number - 1) * 100 + (this.fetched_tracks.get(`page${page_number}`) as SpotifyVideo[]).length + get total_tracks() { + const page_number: number = this.total_pages; + return (page_number - 1) * 100 + (this.fetched_tracks.get(`page${page_number}`) as SpotifyVideo[]).length; } - toJSON(){ + toJSON() { return { - name : this.name, - type : this.type, - collaborative : this.collaborative, - description : this.description, - url : this.url, - id : this.id, - thumbnail : this.thumbnail, - owner : this.owner, - } + name: this.name, + type: this.type, + collaborative: this.collaborative, + description: this.description, + url: this.url, + id: this.id, + thumbnail: this.thumbnail, + owner: this.owner + }; } } -export class SpotifyAlbum{ - name : string - type : "track" | "playlist" | "album" - url : string - id : string; - thumbnail : SpotifyThumbnail - artists : SpotifyArtists[] - copyrights : SpotifyCopyright[] - release_date : string; - release_date_precision : string; - trackCount : number - private spotifyData : SpotifyDataOptions; - private fetched_tracks : Map - constructor(data : any, spotifyData : SpotifyDataOptions){ - this.name = data.name - this.type = "album" - this.id = data.id - this.url = data.external_urls.spotify - this.thumbnail = data.images[0] - let artists : SpotifyArtists[] = [] - data.artists.forEach((v : any) => { +export class SpotifyAlbum { + name: string; + type: 'track' | 'playlist' | 'album'; + url: string; + id: string; + thumbnail: SpotifyThumbnail; + artists: SpotifyArtists[]; + copyrights: SpotifyCopyright[]; + release_date: string; + release_date_precision: string; + trackCount: number; + private spotifyData: SpotifyDataOptions; + private fetched_tracks: Map; + constructor(data: any, spotifyData: SpotifyDataOptions) { + this.name = data.name; + this.type = 'album'; + this.id = data.id; + this.url = data.external_urls.spotify; + this.thumbnail = data.images[0]; + const artists: SpotifyArtists[] = []; + data.artists.forEach((v: any) => { artists.push({ - name : v.name, - id : v.id, - url : v.external_urls.spotify - }) - }) - this.artists = artists - this.copyrights = data.copyrights - this.release_date = data.release_date - this.release_date_precision = data.release_date_precision - this.trackCount = data.total_tracks - let videos: SpotifyTracks[] = [] - data.tracks.items.forEach((v : any) => { - videos.push(new SpotifyTracks(v)) - }) - this.fetched_tracks = new Map() - this.fetched_tracks.set('1', videos) - this.spotifyData = spotifyData + name: v.name, + id: v.id, + url: v.external_urls.spotify + }); + }); + this.artists = artists; + this.copyrights = data.copyrights; + this.release_date = data.release_date; + this.release_date_precision = data.release_date_precision; + this.trackCount = data.total_tracks; + const videos: SpotifyTracks[] = []; + data.tracks.items.forEach((v: any) => { + videos.push(new SpotifyTracks(v)); + }); + this.fetched_tracks = new Map(); + this.fetched_tracks.set('1', videos); + this.spotifyData = spotifyData; } - async fetch(){ - let fetching : number; - if(this.trackCount > 500) fetching = 500 - else fetching = this.trackCount - if(fetching <= 50) return - let work = [] - for(let i = 2; i <= Math.ceil(fetching/50); i++){ - work.push(new Promise(async (resolve, reject) => { - let response = await request(`https://api.spotify.com/v1/albums/${this.id}/tracks?offset=${(i-1)*50}&limit=50&market=${this.spotifyData.market}`, { - headers : { - "Authorization" : `${this.spotifyData.token_type} ${this.spotifyData.access_token}` - } - }).catch((err) => reject(`Response Error : \n${err}`)) - let videos: SpotifyTracks[] = [] - if(typeof response !== 'string') return - let json_data = JSON.parse(response) - json_data.items.forEach((v : any) => { - videos.push(new SpotifyTracks(v)) + async fetch() { + let fetching: number; + if (this.trackCount > 500) fetching = 500; + else fetching = this.trackCount; + if (fetching <= 50) return; + const work = []; + for (let i = 2; i <= Math.ceil(fetching / 50); i++) { + work.push( + new Promise(async (resolve, reject) => { + const response = await request( + `https://api.spotify.com/v1/albums/${this.id}/tracks?offset=${(i - 1) * 50}&limit=50&market=${ + this.spotifyData.market + }`, + { + headers: { + Authorization: `${this.spotifyData.token_type} ${this.spotifyData.access_token}` + } + } + ).catch((err) => reject(`Response Error : \n${err}`)); + const videos: SpotifyTracks[] = []; + if (typeof response !== 'string') return; + const json_data = JSON.parse(response); + json_data.items.forEach((v: any) => { + videos.push(new SpotifyTracks(v)); + }); + this.fetched_tracks.set(`${i}`, videos); + resolve('Success'); }) - this.fetched_tracks.set(`${i}`, videos) - resolve('Success') - })) + ); } - await Promise.allSettled(work) - return this + await Promise.allSettled(work); + return this; } - page(num : number){ - if(!num) throw new Error('Page number is not provided') - if(!this.fetched_tracks.has(`${num}`)) throw new Error('Given Page number is invalid') - return this.fetched_tracks.get(`${num}`) + page(num: number) { + if (!num) throw new Error('Page number is not provided'); + if (!this.fetched_tracks.has(`${num}`)) throw new Error('Given Page number is invalid'); + return this.fetched_tracks.get(`${num}`); } - get total_pages(){ - return this.fetched_tracks.size + get total_pages() { + return this.fetched_tracks.size; } - get total_tracks(){ - let page_number: number = this.total_pages - return (page_number - 1) * 100 + (this.fetched_tracks.get(`page${page_number}`) as SpotifyVideo[]).length + get total_tracks() { + const page_number: number = this.total_pages; + return (page_number - 1) * 100 + (this.fetched_tracks.get(`page${page_number}`) as SpotifyVideo[]).length; } - toJSON(){ + toJSON() { return { - name : this.name, - type : this.type, - url : this.url, - thumbnail : this.thumbnail, - artists : this.artists, - copyrights : this.copyrights, - release_date : this.release_date, - release_date_precision : this.release_date_precision, - total_tracks : this.total_tracks, - } + name: this.name, + type: this.type, + url: this.url, + thumbnail: this.thumbnail, + artists: this.artists, + copyrights: this.copyrights, + release_date: this.release_date, + release_date_precision: this.release_date_precision, + total_tracks: this.total_tracks + }; } } -class SpotifyTracks{ - name : string; - type : "track" | "playlist" | "album" - id : string; - url : string; - explicit : boolean; - durationInSec : number; - durationInMs : number; - artists : SpotifyArtists[] - constructor(data : any){ - this.name = data.name - this.id = data.id - this.type = "track" - this.url = data.external_urls.spotify - this.explicit = data.explicit - this.durationInMs = data.duration_ms - this.durationInSec = Math.round(this.durationInMs/1000) - let artists : SpotifyArtists[] = [] - data.artists.forEach((v : any) => { +class SpotifyTracks { + name: string; + type: 'track' | 'playlist' | 'album'; + id: string; + url: string; + explicit: boolean; + durationInSec: number; + durationInMs: number; + artists: SpotifyArtists[]; + constructor(data: any) { + this.name = data.name; + this.id = data.id; + this.type = 'track'; + this.url = data.external_urls.spotify; + this.explicit = data.explicit; + this.durationInMs = data.duration_ms; + this.durationInSec = Math.round(this.durationInMs / 1000); + const artists: SpotifyArtists[] = []; + data.artists.forEach((v: any) => { artists.push({ - name : v.name, - id : v.id, - url : v.external_urls.spotify - }) - }) - this.artists = artists + name: v.name, + id: v.id, + url: v.external_urls.spotify + }); + }); + this.artists = artists; } - toJSON(){ + toJSON() { return { - name : this.name, - id : this.id, - type : this.type, - url : this.url, - explicit : this.explicit, - durationInMs : this.durationInMs, - durationInSec : this.durationInSec, - artists : this.artists, - } + name: this.name, + id: this.id, + type: this.type, + url: this.url, + explicit: this.explicit, + durationInMs: this.durationInMs, + durationInSec: this.durationInSec, + artists: this.artists + }; } -} \ No newline at end of file +} diff --git a/play-dl/Spotify/index.ts b/play-dl/Spotify/index.ts index aa11869..de4399c 100644 --- a/play-dl/Spotify/index.ts +++ b/play-dl/Spotify/index.ts @@ -1,171 +1,134 @@ -import { request } from "../YouTube/utils/request"; -import { SpotifyAlbum, SpotifyPlaylist, SpotifyVideo } from "./classes" -import readline from 'readline' -import fs from 'fs' +import { request } from '../YouTube/utils/request'; +import { SpotifyAlbum, SpotifyPlaylist, SpotifyVideo } from './classes'; +import fs from 'fs'; -var spotifyData : SpotifyDataOptions; -if(fs.existsSync('.data/spotify.data')){ - spotifyData = JSON.parse(fs.readFileSync('.data/spotify.data').toString()) +let spotifyData: SpotifyDataOptions; +if (fs.existsSync('.data/spotify.data')) { + spotifyData = JSON.parse(fs.readFileSync('.data/spotify.data').toString()); } -export interface SpotifyDataOptions{ - client_id : string; - client_secret : string; - redirect_url : string; - authorization_code? :string; - access_token? : string; - refresh_token? : string; - token_type? : string; - expires_in? : number; - expiry? : number; - market? : string; +export interface SpotifyDataOptions { + client_id: string; + client_secret: string; + redirect_url: string; + authorization_code?: string; + access_token?: string; + refresh_token?: string; + token_type?: string; + expires_in?: number; + expiry?: number; + market?: string; } -const pattern = /^((https:)?\/\/)?open.spotify.com\/(track|album|playlist)\// +const pattern = /^((https:)?\/\/)?open.spotify.com\/(track|album|playlist)\//; -export async function spotify(url : string): Promise{ - if(!spotifyData) throw new Error('Spotify Data is missing\nDid you forgot to do authorization ?') - if(!url.match(pattern)) throw new Error('This is not a Spotify URL') - if(url.indexOf('track/') !== -1){ - let trackID = url.split('track/')[1].split('&')[0].split('?')[0] - let response = await request(`https://api.spotify.com/v1/tracks/${trackID}?market=${spotifyData.market}`, { - headers : { - "Authorization" : `${spotifyData.token_type} ${spotifyData.access_token}` +export async function spotify(url: string): Promise { + if (!spotifyData) throw new Error('Spotify Data is missing\nDid you forgot to do authorization ?'); + if (!url.match(pattern)) throw new Error('This is not a Spotify URL'); + if (url.indexOf('track/') !== -1) { + const trackID = url.split('track/')[1].split('&')[0].split('?')[0]; + const response = await request(`https://api.spotify.com/v1/tracks/${trackID}?market=${spotifyData.market}`, { + headers: { + Authorization: `${spotifyData.token_type} ${spotifyData.access_token}` } - }).catch((err) => {return 0}) - if(typeof response !== 'number') return new SpotifyVideo(JSON.parse(response)) - else throw new Error('Failed to get spotify Track Data') - } - else if(url.indexOf('album/') !== -1){ - let albumID = url.split('album/')[1].split('&')[0].split('?')[0] - let response = await request(`https://api.spotify.com/v1/albums/${albumID}?market=${spotifyData.market}`, { - headers : { - "Authorization" : `${spotifyData.token_type} ${spotifyData.access_token}` + }).catch((err: Error) => { + return err; + }); + if (response instanceof Error) throw response; + return new SpotifyVideo(JSON.parse(response)); + } else if (url.indexOf('album/') !== -1) { + const albumID = url.split('album/')[1].split('&')[0].split('?')[0]; + const response = await request(`https://api.spotify.com/v1/albums/${albumID}?market=${spotifyData.market}`, { + headers: { + Authorization: `${spotifyData.token_type} ${spotifyData.access_token}` } - }).catch((err) => {return 0}) - if(typeof response !== 'number') return new SpotifyAlbum(JSON.parse(response), spotifyData) - else throw new Error('Failed to get spotify Album Data') - } - else if(url.indexOf('playlist/') !== -1){ - let playlistID = url.split('playlist/')[1].split('&')[0].split('?')[0] - let response = await request(`https://api.spotify.com/v1/playlists/${playlistID}?market=${spotifyData.market}`, { - headers : { - "Authorization" : `${spotifyData.token_type} ${spotifyData.access_token}` + }).catch((err: Error) => { + return err; + }); + if (response instanceof Error) throw response; + return new SpotifyAlbum(JSON.parse(response), spotifyData); + } else if (url.indexOf('playlist/') !== -1) { + const playlistID = url.split('playlist/')[1].split('&')[0].split('?')[0]; + const response = await request( + `https://api.spotify.com/v1/playlists/${playlistID}?market=${spotifyData.market}`, + { + headers: { + Authorization: `${spotifyData.token_type} ${spotifyData.access_token}` + } } - }).catch((err) => {return 0}) - if(typeof response !== 'number') return new SpotifyPlaylist(JSON.parse(response), spotifyData) - else throw new Error('Failed to get spotify Playlist Data') - } - else throw new Error('URL is out of scope for play-dl.') + ).catch((err: Error) => { + return err; + }); + if (response instanceof Error) throw response; + return new SpotifyPlaylist(JSON.parse(response), spotifyData); + } else throw new Error('URL is out of scope for play-dl.'); } -export function sp_validate(url : string): "track" | "playlist" | "album" | boolean{ - if(!url.match(pattern)) return false - if(url.indexOf('track/') !== -1){ - return "track" - } - else if(url.indexOf('album/') !== -1){ - return "album" - } - else if(url.indexOf('playlist/') !== -1){ - return "playlist" - } - else return false +export function sp_validate(url: string): 'track' | 'playlist' | 'album' | boolean { + if (!url.match(pattern)) return false; + if (url.indexOf('track/') !== -1) { + return 'track'; + } else if (url.indexOf('album/') !== -1) { + return 'album'; + } else if (url.indexOf('playlist/') !== -1) { + return 'playlist'; + } else return false; } -export function Authorization(){ - let ask = readline.createInterface({ - input : process.stdin, - output : process.stdout - }) - - let client_id : string, client_secret : string, redirect_url : string, market : string; - ask.question('Client ID : ', (id) => { - client_id = id - ask.question('Client Secret : ', (secret) => { - client_secret = secret - ask.question('Redirect URL : ', (url) => { - redirect_url = url - console.log('\nMarket Selection URL : \nhttps://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements \n') - ask.question('Market : ', (mar) => { - if(mar.length === 2) market = mar - else { - console.log('Invalid Market, Selecting IN as market') - market = 'IN' - } - console.log('\nNow Go to your browser and Paste this url. Authroize it and paste the redirected url here. \n') - console.log(`https://accounts.spotify.com/authorize?client_id=${client_id}&response_type=code&redirect_uri=${encodeURI(redirect_url)} \n`) - ask.question('Redirected URL : ',async (url) => { - if (!fs.existsSync('.data')) fs.mkdirSync('.data') - spotifyData = { - client_id, - client_secret, - redirect_url, - authorization_code : url.split('code=')[1], - market - } - let check = await SpotifyAuthorize(spotifyData) - if(check === false) throw new Error('Failed to get access Token.') - ask.close() - }) - }) - }) - }) - }) -} - -async function SpotifyAuthorize(data : SpotifyDataOptions): Promise{ - let response = await request(`https://accounts.spotify.com/api/token`, { - headers : { - "Authorization" : `Basic ${Buffer.from(`${data.client_id}:${data.client_secret}`).toString('base64')}`, - "Content-Type" : "application/x-www-form-urlencoded" +export async function SpotifyAuthorize(data: SpotifyDataOptions): Promise { + const response = await request(`https://accounts.spotify.com/api/token`, { + headers: { + 'Authorization': `Basic ${Buffer.from(`${data.client_id}:${data.client_secret}`).toString('base64')}`, + 'Content-Type': 'application/x-www-form-urlencoded' }, - body : `grant_type=authorization_code&code=${data.authorization_code}&redirect_uri=${encodeURI(data.redirect_url)}`, - method : "POST" - }).catch(() => { - return 0 - }) - - if(typeof response === 'number') return false - let resp_json = JSON.parse(response) + body: `grant_type=authorization_code&code=${data.authorization_code}&redirect_uri=${encodeURI( + data.redirect_url + )}`, + method: 'POST' + }).catch((err: Error) => { + return err; + }); + if (response instanceof Error) throw response; + const resp_json = JSON.parse(response); spotifyData = { - client_id : data.client_id, - client_secret : data.client_secret, - redirect_url : data.redirect_url, - access_token : resp_json.access_token, - refresh_token : resp_json.refresh_token, - expires_in : Number(resp_json.expires_in), - expiry : Date.now() + ((resp_json.expires_in - 1) * 1000), - token_type : resp_json.token_type, - market : data.market - } - fs.writeFileSync('.data/spotify.data', JSON.stringify(spotifyData, undefined, 4)) - return true + client_id: data.client_id, + client_secret: data.client_secret, + redirect_url: data.redirect_url, + access_token: resp_json.access_token, + refresh_token: resp_json.refresh_token, + expires_in: Number(resp_json.expires_in), + expiry: Date.now() + (resp_json.expires_in - 1) * 1000, + token_type: resp_json.token_type, + market: data.market + }; + fs.writeFileSync('.data/spotify.data', JSON.stringify(spotifyData, undefined, 4)); + return true; } -export function is_expired(){ - if(Date.now() >= (spotifyData.expiry as number)) return true - else return false +export function is_expired() { + if (Date.now() >= (spotifyData.expiry as number)) return true; + else return false; } -export async function RefreshToken(): Promise{ - let response = await request(`https://accounts.spotify.com/api/token`, { - headers : { - "Authorization" : `Basic ${Buffer.from(`${spotifyData.client_id}:${spotifyData.client_secret}`).toString('base64')}`, - "Content-Type" : "application/x-www-form-urlencoded" +export async function refreshToken(): Promise { + const response = await request(`https://accounts.spotify.com/api/token`, { + headers: { + 'Authorization': `Basic ${Buffer.from(`${spotifyData.client_id}:${spotifyData.client_secret}`).toString( + 'base64' + )}`, + 'Content-Type': 'application/x-www-form-urlencoded' }, - body : `grant_type=refresh_token&refresh_token=${spotifyData.refresh_token}`, - method : "POST" - }).catch(() => { - return 0 - }) - - if(typeof response === 'number') return false - let resp_json = JSON.parse(response) - spotifyData.access_token = resp_json.access_token - spotifyData.expires_in = Number(resp_json.expires_in) - spotifyData.expiry = Date.now() + ((resp_json.expires_in - 1) * 1000) - spotifyData.token_type = resp_json.token_type - fs.writeFileSync('.data/spotify.data', JSON.stringify(spotifyData, undefined, 4)) - return true -} \ No newline at end of file + body: `grant_type=refresh_token&refresh_token=${spotifyData.refresh_token}`, + method: 'POST' + }).catch((err: Error) => { + return err; + }); + if (response instanceof Error) throw response; + const resp_json = JSON.parse(response); + spotifyData.access_token = resp_json.access_token; + spotifyData.expires_in = Number(resp_json.expires_in); + spotifyData.expiry = Date.now() + (resp_json.expires_in - 1) * 1000; + spotifyData.token_type = resp_json.token_type; + fs.writeFileSync('.data/spotify.data', JSON.stringify(spotifyData, undefined, 4)); + return true; +} diff --git a/play-dl/YouTube/classes/Channel.ts b/play-dl/YouTube/classes/Channel.ts index d1bafa3..f1d4653 100644 --- a/play-dl/YouTube/classes/Channel.ts +++ b/play-dl/YouTube/classes/Channel.ts @@ -34,19 +34,19 @@ export class Channel { * @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"); + 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]; + 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"; + get type(): 'channel' { + return 'channel'; } toString(): string { - return this.name || ""; + return this.name || ''; } toJSON() { @@ -60,4 +60,4 @@ export class Channel { subscribers: this.subscribers }; } -} \ No newline at end of file +} diff --git a/play-dl/YouTube/classes/LiveStream.ts b/play-dl/YouTube/classes/LiveStream.ts index ea7ecc8..806be47 100644 --- a/play-dl/YouTube/classes/LiveStream.ts +++ b/play-dl/YouTube/classes/LiveStream.ts @@ -1,222 +1,235 @@ -import { PassThrough } from 'stream' +import { PassThrough } from 'stream'; import { IncomingMessage } from 'http'; import { StreamType } from '../stream'; import { request, request_stream } from '../utils/request'; import { video_info } from '..'; -export interface FormatInterface{ - url : string; - targetDurationSec : number; - maxDvrDurationSec : number +export interface FormatInterface { + url: string; + targetDurationSec: number; + maxDvrDurationSec: number; } -export class LiveStreaming{ - type : StreamType - stream : PassThrough - private base_url : string - private url : string - private interval : number - private packet_count : number - private timer : NodeJS.Timer | null - private video_url : string - private dash_timer : NodeJS.Timer | null - private segments_urls : string[] - private request : IncomingMessage | null - constructor(dash_url : string, target_interval : number, video_url : string){ - this.type = StreamType.Arbitrary - this.url = dash_url - this.base_url = '' - this.stream = new PassThrough({ highWaterMark : 10 * 1000 * 1000 }) - this.segments_urls = [] - this.packet_count = 0 - this.request = null - this.timer = null - this.video_url = video_url - this.interval = target_interval * 1000 || 0 +export class LiveStreaming { + type: StreamType; + stream: PassThrough; + private base_url: string; + private url: string; + private interval: number; + private packet_count: number; + private timer: NodeJS.Timer | null; + private video_url: string; + private dash_timer: NodeJS.Timer | null; + private segments_urls: string[]; + private request: IncomingMessage | null; + constructor(dash_url: string, target_interval: number, video_url: string) { + this.type = StreamType.Arbitrary; + this.url = dash_url; + this.base_url = ''; + this.stream = new PassThrough({ highWaterMark: 10 * 1000 * 1000 }); + this.segments_urls = []; + this.packet_count = 0; + this.request = null; + this.timer = null; + this.video_url = video_url; + this.interval = target_interval * 1000 || 0; this.dash_timer = setTimeout(() => { - this.dash_updater() - }, 1800000) + this.dash_updater(); + }, 1800000); this.stream.on('close', () => { - this.cleanup() + this.cleanup(); }); - this.start() + this.start(); } - - private async dash_updater(){ - let info = await video_info(this.video_url) - if(info.LiveStreamData.isLive === true && info.LiveStreamData.hlsManifestUrl !== null && info.video_details.durationInSec === '0'){ - this.url = info.LiveStreamData.dashManifestUrl + + private async dash_updater() { + const info = await video_info(this.video_url); + if ( + info.LiveStreamData.isLive === true && + info.LiveStreamData.hlsManifestUrl !== null && + info.video_details.durationInSec === '0' + ) { + this.url = info.LiveStreamData.dashManifestUrl; } this.dash_timer = setTimeout(() => { - this.dash_updater() - }, 1800000) + this.dash_updater(); + }, 1800000); } - private async dash_getter(){ - let response = await request(this.url) - let audioFormat = response.split('')[0].split('') - if(audioFormat[audioFormat.length - 1] === '') audioFormat.pop() - this.base_url = audioFormat[audioFormat.length - 1].split('')[1].split('')[0] - let list = audioFormat[audioFormat.length - 1].split('')[1].split('')[0] - this.segments_urls = list.replace(new RegExp('') - if(this.segments_urls[this.segments_urls.length - 1] === '') this.segments_urls.pop() + private async dash_getter() { + const response = await request(this.url); + const audioFormat = response + .split('')[0] + .split(''); + if (audioFormat[audioFormat.length - 1] === '') audioFormat.pop(); + this.base_url = audioFormat[audioFormat.length - 1].split('')[1].split('')[0]; + const list = audioFormat[audioFormat.length - 1].split('')[1].split('')[0]; + this.segments_urls = list.replace(new RegExp(''); + if (this.segments_urls[this.segments_urls.length - 1] === '') this.segments_urls.pop(); } - private cleanup(){ - clearTimeout(this.timer as NodeJS.Timer) - clearTimeout(this.dash_timer as NodeJS.Timer) - this.request?.unpipe(this.stream) - this.request?.destroy() - this.dash_timer = null - this.video_url = '' - this.request = null - this.timer = null - this.url = '' - this.base_url = '' - this.segments_urls = [] - this.packet_count = 0 - this.interval = 0 + private cleanup() { + clearTimeout(this.timer as NodeJS.Timer); + clearTimeout(this.dash_timer as NodeJS.Timer); + this.request?.unpipe(this.stream); + this.request?.destroy(); + this.dash_timer = null; + this.video_url = ''; + this.request = null; + this.timer = null; + this.url = ''; + this.base_url = ''; + this.segments_urls = []; + this.packet_count = 0; + this.interval = 0; } - private async start(){ - if(this.stream.destroyed){ - this.cleanup() - return + private async start() { + if (this.stream.destroyed) { + this.cleanup(); + return; } - await this.dash_getter() - if(this.segments_urls.length > 3) this.segments_urls.splice(0, this.segments_urls.length - 3) - if(this.packet_count === 0) this.packet_count = Number(this.segments_urls[0].split('sq/')[1].split('/')[0]) - for await (let segment of this.segments_urls){ - if(Number(segment.split('sq/')[1].split('/')[0]) !== this.packet_count){ - continue + await this.dash_getter(); + if (this.segments_urls.length > 3) this.segments_urls.splice(0, this.segments_urls.length - 3); + if (this.packet_count === 0) this.packet_count = Number(this.segments_urls[0].split('sq/')[1].split('/')[0]); + for await (const segment of this.segments_urls) { + if (Number(segment.split('sq/')[1].split('/')[0]) !== this.packet_count) { + continue; } - await new Promise(async(resolve, reject) => { - let stream = await request_stream(this.base_url + segment).catch((err: Error) => err) - if(stream instanceof Error){ - this.stream.emit('error', stream) - return + await new Promise(async (resolve, reject) => { + const stream = await request_stream(this.base_url + segment).catch((err: Error) => err); + if (stream instanceof Error) { + this.stream.emit('error', stream); + return; } - this.request = stream - stream.pipe(this.stream, { end : false }) + this.request = stream; + stream.pipe(this.stream, { end: false }); stream.on('end', () => { - this.packet_count++ - resolve('') - }) + this.packet_count++; + resolve(''); + }); stream.once('error', (err) => { - this.stream.emit('error', err) - }) - }) + this.stream.emit('error', err); + }); + }); } this.timer = setTimeout(() => { - this.start() - }, this.interval) + this.start(); + }, this.interval); } } export class Stream { - type : StreamType - stream : PassThrough - private url : string - private bytes_count : number; - private per_sec_bytes : number; - private content_length : number; - private video_url : string; - private timer : NodeJS.Timer | null; - private cookie : string; - private data_ended : boolean; - private playing_count : number; - private request : IncomingMessage | null - constructor(url : string, type : StreamType, duration : number, contentLength : number, video_url : string, cookie : string){ - this.url = url - this.type = type - this.stream = new PassThrough({ highWaterMark : 10 * 1000 * 1000 }) - this.bytes_count = 0 - this.video_url = video_url - this.cookie = cookie + type: StreamType; + stream: PassThrough; + private url: string; + private bytes_count: number; + private per_sec_bytes: number; + private content_length: number; + private video_url: string; + private timer: NodeJS.Timer | null; + private cookie: string; + private data_ended: boolean; + private playing_count: number; + private request: IncomingMessage | null; + constructor( + url: string, + type: StreamType, + duration: number, + contentLength: number, + video_url: string, + cookie: string + ) { + this.url = url; + this.type = type; + this.stream = new PassThrough({ highWaterMark: 10 * 1000 * 1000 }); + this.bytes_count = 0; + this.video_url = video_url; + this.cookie = cookie; this.timer = setInterval(() => { - this.retry() - }, 7200 * 1000) - this.per_sec_bytes = Math.ceil(contentLength / duration) - this.content_length = contentLength - this.request = null - this.data_ended = false - this.playing_count = 0 + this.retry(); + }, 7200 * 1000); + this.per_sec_bytes = Math.ceil(contentLength / duration); + this.content_length = contentLength; + this.request = null; + this.data_ended = false; + this.playing_count = 0; this.stream.on('close', () => { - this.cleanup() - }) + this.cleanup(); + }); this.stream.on('pause', () => { this.playing_count++; - if(this.data_ended){ - this.bytes_count = 0 - this.per_sec_bytes = 0 - this.cleanup() - this.stream.removeAllListeners('pause') + if (this.data_ended) { + this.bytes_count = 0; + this.per_sec_bytes = 0; + this.cleanup(); + this.stream.removeAllListeners('pause'); + } else if (this.playing_count === 280) { + this.playing_count = 0; + this.loop(); } - else if(this.playing_count === 280){ - this.playing_count = 0 - this.loop() - } - }) - this.loop() + }); + this.loop(); } - private async retry(){ - let info = await video_info(this.video_url, this.cookie) - this.url = info.format[info.format.length - 1].url + private async retry() { + const info = await video_info(this.video_url, this.cookie); + this.url = info.format[info.format.length - 1].url; } - private cleanup(){ - clearInterval(this.timer as NodeJS.Timer) - this.request?.unpipe(this.stream) - this.request?.destroy() - this.timer = null - this.request = null - this.url = '' + private cleanup() { + clearInterval(this.timer as NodeJS.Timer); + this.request?.unpipe(this.stream); + this.request?.destroy(); + this.timer = null; + this.request = null; + this.url = ''; } - private async loop(){ - if(this.stream.destroyed){ - this.cleanup() - return + private async loop() { + if (this.stream.destroyed) { + this.cleanup(); + return; } - let end : number = this.bytes_count + this.per_sec_bytes * 300; - let stream = await request_stream(this.url, { - headers : { - "range" : `bytes=${this.bytes_count}-${end >= this.content_length ? '' : end}` + const end: number = this.bytes_count + this.per_sec_bytes * 300; + const stream = await request_stream(this.url, { + headers: { + range: `bytes=${this.bytes_count}-${end >= this.content_length ? '' : end}` } - }).catch((err: Error) => err) - if(stream instanceof Error){ - this.stream.emit('error', stream) - this.data_ended = true - this.bytes_count = 0 - this.per_sec_bytes = 0 - this.cleanup() - return + }).catch((err: Error) => err); + if (stream instanceof Error) { + this.stream.emit('error', stream); + this.data_ended = true; + this.bytes_count = 0; + this.per_sec_bytes = 0; + this.cleanup(); + return; } - if(Number(stream.statusCode) >= 400){ - this.cleanup() - await this.retry() - this.loop() - if(!this.timer){ + if (Number(stream.statusCode) >= 400) { + this.cleanup(); + await this.retry(); + this.loop(); + if (!this.timer) { this.timer = setInterval(() => { - this.retry() - }, 7200 * 1000) + this.retry(); + }, 7200 * 1000); } - return + return; } - this.request = stream - stream.pipe(this.stream, { end : false }) + this.request = stream; + stream.pipe(this.stream, { end: false }); stream.once('error', (err) => { - this.stream.emit('error', err) - }) + this.stream.emit('error', err); + }); stream.on('data', (chunk: any) => { - this.bytes_count += chunk.length - }) + this.bytes_count += chunk.length; + }); stream.on('end', () => { - if(end >= this.content_length) this.data_ended = true - }) + if (end >= this.content_length) this.data_ended = true; + }); } } diff --git a/play-dl/YouTube/classes/Playlist.ts b/play-dl/YouTube/classes/Playlist.ts index becd02e..f79c95d 100644 --- a/play-dl/YouTube/classes/Playlist.ts +++ b/play-dl/YouTube/classes/Playlist.ts @@ -1,11 +1,11 @@ -import { getPlaylistVideos, getContinuationToken } from "../utils/extractor"; -import { request } 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="; +import { getPlaylistVideos, getContinuationToken } from '../utils/extractor'; +import { request } 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{ +export class PlayList { id?: string; title?: string; videoCount?: number; @@ -16,19 +16,23 @@ export class PlayList{ channel?: Channel; thumbnail?: Thumbnail; private videos?: []; - private fetched_videos : Map - private _continuation: { api?: string; token?: string; clientVersion?: string } = {}; - private __count : number + private fetched_videos: Map; + private _continuation: { + api?: string; + token?: string; + clientVersion?: string; + } = {}; + private __count: number; - constructor(data : any, searchResult : Boolean = false){ + constructor(data: any, searchResult = 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) + this.__count = 0; + this.fetched_videos = new Map(); + if (searchResult) this.__patchSearch(data); + else this.__patch(data); } - private __patch(data:any){ + private __patch(data: any) { this.id = data.id || undefined; this.url = data.url || undefined; this.title = data.title || undefined; @@ -39,14 +43,14 @@ export class PlayList{ 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.__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 ?? ""; + this._continuation.clientVersion = data.continuation?.clientVersion ?? ''; } - private __patchSearch(data: any){ + 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; @@ -59,19 +63,19 @@ export class PlayList{ this.views = 0; } - async next(limit: number = Infinity): Promise { + async next(limit = Infinity): Promise { if (!this._continuation || !this._continuation.token) return []; - let nextPage = await request(`${BASE_API}${this._continuation.api}`, { - method: "POST", + const nextPage = await request(`${BASE_API}${this._continuation.api}`, { + method: 'POST', body: JSON.stringify({ continuation: this._continuation.token, context: { client: { utcOffsetMinutes: 0, - gl: "US", - hl: "en", - clientName: "WEB", + gl: 'US', + hl: 'en', + clientName: 'WEB', clientVersion: this._continuation.clientVersion }, user: {}, @@ -79,24 +83,25 @@ export class PlayList{ } }) }); - - 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 + const contents = + JSON.parse(nextPage)?.onResponseReceivedActions[0]?.appendContinuationItemsAction?.continuationItems; + if (!contents) return []; + + const 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; + async fetch(max = Infinity) { + const 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++ + 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; } @@ -104,23 +109,23 @@ export class PlayList{ return this; } - get type(): "playlist" { - return "playlist"; + get type(): 'playlist' { + return 'playlist'; } - page(number : number): Video[]{ - if(!number) throw new Error('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[] + page(number: number): Video[] { + if (!number) throw new Error('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_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 + get total_videos() { + const page_number: number = this.total_pages; + return (page_number - 1) * 100 + (this.fetched_videos.get(`page${page_number}`) as Video[]).length; } toJSON() { @@ -129,12 +134,12 @@ export class PlayList{ title: this.title, thumbnail: this.thumbnail, channel: { - name : this.channel?.name, - id : this.channel?.id, - icon : this.channel?.iconURL() + 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/play-dl/YouTube/classes/Thumbnail.ts b/play-dl/YouTube/classes/Thumbnail.ts index 4ea8db5..fce1644 100644 --- a/play-dl/YouTube/classes/Thumbnail.ts +++ b/play-dl/YouTube/classes/Thumbnail.ts @@ -1,4 +1,4 @@ -type ThumbnailType = "default" | "hqdefault" | "mqdefault" | "sddefault" | "maxresdefault" | "ultrares"; +type ThumbnailType = 'default' | 'hqdefault' | 'mqdefault' | 'sddefault' | 'maxresdefault' | 'ultrares'; export class Thumbnail { id?: string; @@ -21,20 +21,21 @@ export class Thumbnail { 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; + 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}"!`); + 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}` : ""; + return this.url ? `${this.url}` : ''; } toJSON() { diff --git a/play-dl/YouTube/classes/Video.ts b/play-dl/YouTube/classes/Video.ts index 40ce821..02bcb47 100644 --- a/play-dl/YouTube/classes/Video.ts +++ b/play-dl/YouTube/classes/Video.ts @@ -1,9 +1,9 @@ -import { Channel } from "./Channel"; -import { Thumbnail } from "./Thumbnail"; +import { Channel } from './Channel'; +import { Thumbnail } from './Thumbnail'; interface VideoOptions { id?: string; - url? : string; + url?: string; title?: string; description?: string; durationRaw: string; @@ -12,21 +12,21 @@ interface VideoOptions { views: number; thumbnail?: { id: string | undefined; - width: number | undefined ; + width: number | undefined; height: number | undefined; url: string | undefined; }; channel?: { - name : string, - id : string, - icon : string + name: string; + id: string; + icon: string; }; videos?: Video[]; - type : string; - ratings : { + type: string; + ratings: { likes: number; dislikes: number; - } + }; live: boolean; private: boolean; tags: string[]; @@ -34,7 +34,7 @@ interface VideoOptions { export class Video { id?: string; - url? : string; + url?: string; title?: string; description?: string; durationRaw: string; @@ -50,35 +50,35 @@ export class Video { private: boolean; tags: string[]; - constructor(data : any){ - if(!data) throw new Error(`Can not initiate ${this.constructor.name} without data`) - + 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.url = `https://www.youtube.com/watch?v=${this.id}`; this.title = data.title || undefined; this.description = data.description || undefined; - this.durationRaw = data.duration_raw || "0:00"; + this.durationRaw = data.duration_raw || '0:00'; this.durationInSec = (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.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 type(): 'video' { + return 'video'; } get toString(): string { - return this.url || ""; + return this.url || ''; } - get toJSON(): VideoOptions{ + get toJSON(): VideoOptions { return { id: this.id, url: this.url, @@ -104,4 +104,4 @@ export class Video { private: this.private }; } -} \ No newline at end of file +} diff --git a/play-dl/YouTube/index.ts b/play-dl/YouTube/index.ts index f758a1b..a268c62 100644 --- a/play-dl/YouTube/index.ts +++ b/play-dl/YouTube/index.ts @@ -1,3 +1,3 @@ -export { search } from './search' -export { stream, stream_from_info } from './stream' -export * from './utils' \ No newline at end of file +export { search } from './search'; +export { stream, stream_from_info } from './stream'; +export * from './utils'; diff --git a/play-dl/YouTube/search.ts b/play-dl/YouTube/search.ts index 100e547..f83cdc9 100644 --- a/play-dl/YouTube/search.ts +++ b/play-dl/YouTube/search.ts @@ -1,36 +1,38 @@ -import { request } from "./utils/request"; -import { ParseSearchInterface, ParseSearchResult } from "./utils/parser"; -import { Video } from "./classes/Video"; -import { Channel } from "./classes/Channel"; -import { PlayList } from "./classes/Playlist"; - +import { request } 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', + 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(' ', '+') - options.type ??= "video" - if(!url.match('&sp=')){ - url += '&sp=' - switch(options?.type){ +export async function search( + search: string, + options: ParseSearchInterface = {} +): Promise<(Video | Channel | PlayList)[]> { + let url = 'https://www.youtube.com/results?search_query=' + search.replaceAll(' ', '+'); + options.type ??= 'video'; + if (!url.match('&sp=')) { + url += '&sp='; + switch (options?.type) { case 'channel': - url += SearchType.Channel - break + url += SearchType.Channel; + break; case 'playlist': - url += SearchType.PlayList - break + url += SearchType.PlayList; + break; case 'video': - url += SearchType.Video - break + url += SearchType.Video; + break; } } - let body = await request(url, { - headers : {'accept-language' : 'en-US,en-IN;q=0.9,en;q=0.8,hi;q=0.7'} - }) - let data = ParseSearchResult(body, options) - return data -} \ No newline at end of file + const body = await request(url, { + headers: { 'accept-language': 'en-US,en-IN;q=0.9,en;q=0.8,hi;q=0.7' } + }); + const data = ParseSearchResult(body, options); + return data; +} diff --git a/play-dl/YouTube/stream.ts b/play-dl/YouTube/stream.ts index 8f50b5c..54f8e30 100644 --- a/play-dl/YouTube/stream.ts +++ b/play-dl/YouTube/stream.ts @@ -1,97 +1,123 @@ -import { video_info } from "." -import { LiveStreaming, Stream } from "./classes/LiveStream" +import { video_info } from '.'; +import { LiveStreaming, Stream } from './classes/LiveStream'; -export enum StreamType{ +export enum StreamType { Arbitrary = 'arbitrary', Raw = 'raw', OggOpus = 'ogg/opus', WebmOpus = 'webm/opus', - Opus = 'opus', + Opus = 'opus' } -interface InfoData{ - LiveStreamData : { - isLive : boolean - dashManifestUrl : string - hlsManifestUrl : string - } - html5player : string - format : any[] - video_details : any +interface InfoData { + LiveStreamData: { + isLive: boolean; + dashManifestUrl: string; + hlsManifestUrl: string; + }; + html5player: string; + format: any[]; + video_details: any; } -function parseAudioFormats(formats : any[]){ - let result: any[] = [] +function parseAudioFormats(formats: any[]) { + const result: any[] = []; formats.forEach((format) => { - let type = format.mimeType as string - if(type.startsWith('audio')){ - format.codec = type.split('codecs="')[1].split('"')[0] - format.container = type.split('audio/')[1].split(';')[0] - result.push(format) + const type = format.mimeType as string; + if (type.startsWith('audio')) { + format.codec = type.split('codecs="')[1].split('"')[0]; + format.container = type.split('audio/')[1].split(';')[0]; + result.push(format); } - }) - return result + }); + return result; } -export async function stream(url : string, cookie? : string): Promise{ - let info = await video_info(url, cookie) - let final: any[] = []; - let type : StreamType; - if(info.LiveStreamData.isLive === true && info.LiveStreamData.hlsManifestUrl !== null && info.video_details.durationInSec === '0') { - return new LiveStreaming(info.LiveStreamData.dashManifestUrl, info.format[info.format.length - 1].targetDurationSec, info.video_details.url) +export async function stream(url: string, cookie?: string): Promise { + const info = await video_info(url, cookie); + const final: any[] = []; + let type: StreamType; + if ( + info.LiveStreamData.isLive === true && + info.LiveStreamData.hlsManifestUrl !== null && + info.video_details.durationInSec === '0' + ) { + return new LiveStreaming( + info.LiveStreamData.dashManifestUrl, + info.format[info.format.length - 1].targetDurationSec, + info.video_details.url + ); } - let audioFormat = parseAudioFormats(info.format) - let opusFormats = filterFormat(audioFormat, "opus") + const audioFormat = parseAudioFormats(info.format); + const opusFormats = filterFormat(audioFormat, 'opus'); - if(opusFormats.length === 0){ - type = StreamType.Arbitrary - if(audioFormat.length === 0){ - final.push(info.format[info.format.length - 1]) - } - else{ - final.push(audioFormat[audioFormat.length - 1]) - } + if (opusFormats.length === 0) { + type = StreamType.Arbitrary; + if (audioFormat.length === 0) { + final.push(info.format[info.format.length - 1]); + } else { + final.push(audioFormat[audioFormat.length - 1]); + } + } else { + type = StreamType.WebmOpus; + final.push(opusFormats[opusFormats.length - 1]); } - else{ - type = StreamType.WebmOpus - final.push(opusFormats[opusFormats.length - 1]) - } - - return new Stream(final[0].url, type, info.video_details.durationInSec, Number(final[0].contentLength), info.video_details.url, cookie as string) + + return new Stream( + final[0].url, + type, + info.video_details.durationInSec, + Number(final[0].contentLength), + info.video_details.url, + cookie as string + ); } -export async function stream_from_info(info : InfoData, cookie? : string): Promise{ - let final: any[] = []; - let type : StreamType; - if(info.LiveStreamData.isLive === true && info.LiveStreamData.hlsManifestUrl !== null && info.video_details.durationInSec === '0') { - return new LiveStreaming(info.LiveStreamData.dashManifestUrl, info.format[info.format.length - 1].targetDurationSec, info.video_details.url) +export async function stream_from_info(info: InfoData, cookie?: string): Promise { + const final: any[] = []; + let type: StreamType; + if ( + info.LiveStreamData.isLive === true && + info.LiveStreamData.hlsManifestUrl !== null && + info.video_details.durationInSec === '0' + ) { + return new LiveStreaming( + info.LiveStreamData.dashManifestUrl, + info.format[info.format.length - 1].targetDurationSec, + info.video_details.url + ); } - let audioFormat = parseAudioFormats(info.format) - let opusFormats = filterFormat(audioFormat, "opus") + const audioFormat = parseAudioFormats(info.format); + const opusFormats = filterFormat(audioFormat, 'opus'); - if(opusFormats.length === 0){ - type = StreamType.Arbitrary - if(audioFormat.length === 0){ - final.push(info.format[info.format.length - 1]) - } - else{ - final.push(audioFormat[audioFormat.length - 1]) - } + if (opusFormats.length === 0) { + type = StreamType.Arbitrary; + if (audioFormat.length === 0) { + final.push(info.format[info.format.length - 1]); + } else { + final.push(audioFormat[audioFormat.length - 1]); + } + } else { + type = StreamType.WebmOpus; + final.push(opusFormats[opusFormats.length - 1]); } - else{ - type = StreamType.WebmOpus - final.push(opusFormats[opusFormats.length - 1]) - } - - return new Stream(final[0].url, type, info.video_details.durationInSec, Number(final[0].contentLength), info.video_details.url, cookie as string) + + return new Stream( + final[0].url, + type, + info.video_details.durationInSec, + Number(final[0].contentLength), + info.video_details.url, + cookie as string + ); } -function filterFormat(formats : any[], codec : string){ - let result: any[] = [] +function filterFormat(formats: any[], codec: string) { + const result: any[] = []; formats.forEach((format) => { - if(format.codec === codec) result.push(format) - }) - return result + if (format.codec === codec) result.push(format); + }); + return result; } diff --git a/play-dl/YouTube/utils/cipher.ts b/play-dl/YouTube/utils/cipher.ts index 964b61f..b9f63cb 100644 --- a/play-dl/YouTube/utils/cipher.ts +++ b/play-dl/YouTube/utils/cipher.ts @@ -1,13 +1,13 @@ -import { URL } from 'url' -import { request } from './request' -import querystring from 'querystring' +import { URL } from 'url'; +import { request } from './request'; +import querystring from 'querystring'; interface formatOptions { - url? : string; - sp? : string; - signatureCipher? : string; - cipher?: string; - s? : string; + 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][^'\\\\]*)*'`; @@ -16,159 +16,140 @@ 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 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}\\)` + -`\\}`); + `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 +export function js_tokens(body: string) { + const function_action = function_regexp.exec(body); + const 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, '\\$') + const object = object_action[1].replace(/\$/g, '\\$'); + const object_body = object_action[2].replace(/\$/g, '\\$'); + const 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, ''); + let result = reverse_regexp.exec(object_body); + const reverseKey = result && result[1].replace(/\$/g, '\\$').replace(/\$|^'|^"|'$|"$/g, ''); - result = splice_regexp.exec(object_body); - const spliceKey = result && result[1] - .replace(/\$/g, '\\$') - .replace(/\$|^'|^"|'$|"$/g, ''); + result = slice_regexp.exec(object_body); + const sliceKey = result && result[1].replace(/\$/g, '\\$').replace(/\$|^'|^"|'$|"$/g, ''); - result = swap_regexp.exec(object_body); - const swapKey = result && result[1] - .replace(/\$/g, '\\$') - .replace(/\$|^'|^"|'$|"$/g, ''); + result = splice_regexp.exec(object_body); + const spliceKey = 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; + 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) { + const 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 + 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 +function deciper_signature(tokens: string[], signature: string) { + let sig = signature.split(''); + const 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('') + return sig.join(''); } - -function swappositions(array : string[], position : number){ - let first = array[0] - array[0] = array[position] - array[position] = first - return array +function swappositions(array: string[], position: number) { + const first = array[0]; + 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 +function download_url(format: formatOptions, sig: string) { + let decoded_url; + if (!format.url) return; + decoded_url = format.url; - decoded_url = decodeURIComponent(decoded_url) + decoded_url = decodeURIComponent(decoded_url); - let parsed_url = new URL(decoded_url) - parsed_url.searchParams.set('ratebypass', 'yes'); + const 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(); + 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 request(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 +export async function format_decipher(formats: formatOptions[], html5player: string) { + const body = await request(html5player); + const tokens = js_tokens(body); + formats.forEach((format) => { + const 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; +} diff --git a/play-dl/YouTube/utils/extractor.ts b/play-dl/YouTube/utils/extractor.ts index f613794..22d1023 100644 --- a/play-dl/YouTube/utils/extractor.ts +++ b/play-dl/YouTube/utils/extractor.ts @@ -1,166 +1,187 @@ -import { request } from './request' -import { format_decipher, js_tokens } from './cipher' -import { Video } from '../classes/Video' -import { PlayList } from '../classes/Playlist' +import { request } from './request'; +import { format_decipher, js_tokens } from './cipher'; +import { Video } from '../classes/Video'; +import { PlayList } from '../classes/Playlist'; -const DEFAULT_API_KEY = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"; -const video_pattern = /^((?:https?:)?\/\/)?(?:(?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$/; -const playlist_pattern = /^((?:https?:)?\/\/)?(?:(?:www|m)\.)?(youtube\.com)\/(?:(playlist|watch))(.*)?((\?|\&)list=)/ +const DEFAULT_API_KEY = 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8'; +const video_pattern = + /^((?:https?:)?\/\/)?(?:(?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$/; +const playlist_pattern = /^((?:https?:)?\/\/)?(?:(?:www|m)\.)?(youtube\.com)\/(?:(playlist|watch))(.*)?((\?|\&)list=)/; -export function yt_validate(url : string): "playlist" | "video" | boolean { - if(url.indexOf('list=') === -1){ - if(!url.match(video_pattern)) return false - else return "video" - } - else { - if(!url.match(playlist_pattern)) return false - let Playlist_id = url.split('list=')[1].split('&')[0] - if(Playlist_id.length !== 34 || !Playlist_id.startsWith('PL')){ - return false +export function yt_validate(url: string): 'playlist' | 'video' | boolean { + if (url.indexOf('list=') === -1) { + if (!url.match(video_pattern)) return false; + else return 'video'; + } else { + if (!url.match(playlist_pattern)) return false; + const Playlist_id = url.split('list=')[1].split('&')[0]; + if (Playlist_id.length !== 34 || !Playlist_id.startsWith('PL')) { + return false; } - return "playlist" + return 'playlist'; } } -export function extractID(url : string): string{ - if(url.startsWith('https')){ - if(url.indexOf('list=') === -1){ - let video_id : string; - if(url.includes('youtu.be/')) video_id = url.split('youtu.be/')[1].split('/')[0] - else if(url.includes('youtube.com/embed/')) video_id = url.split('youtube.com/embed/')[1].split('/')[0] +export function extractID(url: string): string { + if (url.startsWith('https')) { + if (url.indexOf('list=') === -1) { + let video_id: string; + if (url.includes('youtu.be/')) video_id = url.split('youtu.be/')[1].split('/')[0]; + else if (url.includes('youtube.com/embed/')) video_id = url.split('youtube.com/embed/')[1].split('/')[0]; else video_id = url.split('watch?v=')[1].split('&')[0]; - return video_id + return video_id; + } else { + return url.split('list=')[1].split('&')[0]; } - else{ - return url.split('list=')[1].split('&')[0] - } - } - else return url + } else return url; } -export async function video_basic_info(url : string, cookie? : string){ - let video_id : string; - if(url.startsWith('https')) { - if(yt_validate(url) !== 'video') throw new Error('This is not a YouTube Watch URL') - video_id = extractID(url) - } - else video_id = url - let new_url = `https://www.youtube.com/watch?v=${video_id}` - let body = await request(new_url, { - headers : (cookie) ? { 'cookie' : 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'} - }) - let player_response = JSON.parse(body.split("var ytInitialPlayerResponse = ")[1].split("}};")[0] + '}}') - let initial_response = JSON.parse(body.split("var ytInitialData = ")[1].split("}};")[0] + '}}') - let badge = initial_response.contents.twoColumnWatchNextResults.results.results.contents[1]?.videoSecondaryInfoRenderer?.owner?.videoOwnerRenderer?.badges && initial_response.contents.twoColumnWatchNextResults.results.results.contents[1]?.videoSecondaryInfoRenderer?.owner?.videoOwnerRenderer?.badges[0] - if(player_response.playabilityStatus.status !== 'OK') throw new Error(`While getting info from url\n${player_response.playabilityStatus.errorScreen.playerErrorMessageRenderer?.reason.simpleText ?? player_response.playabilityStatus.errorScreen.playerKavRenderer?.reason.simpleText}`) - let html5player = `https://www.youtube.com${body.split('"jsUrl":"')[1].split('"')[0]}` - let format = [] - 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, - durationInSec : vid.lengthSeconds, - durationRaw : parseSeconds(vid.lengthSeconds), - uploadedDate : microformat.publishDate, - thumbnail : vid.thumbnail.thumbnails[vid.thumbnail.thumbnails.length - 1], - channel : { - name : vid.author, - id : vid.channelId, - url : `https://www.youtube.com/channel/${vid.channelId}`, - verified : Boolean(badge?.metadataBadgeRenderer?.style?.toLowerCase().includes('verified')) - }, - views : vid.viewCount, - tags : vid.keywords, - averageRating : vid.averageRating, - live : vid.isLiveContent, - private : vid.isPrivate - } - if(!video_details.live) format.push(player_response.streamingData.formats[0]) - format.push(...player_response.streamingData.adaptiveFormats) - let LiveStreamData = { - isLive : video_details.live, - dashManifestUrl : player_response.streamingData?.dashManifestUrl ?? null, - hlsManifestUrl : player_response.streamingData?.hlsManifestUrl ?? null - } - return { - LiveStreamData, - html5player, - format, - video_details - } +export async function video_basic_info(url: string, cookie?: string) { + let video_id: string; + if (url.startsWith('https')) { + if (yt_validate(url) !== 'video') throw new Error('This is not a YouTube Watch URL'); + video_id = extractID(url); + } else video_id = url; + const new_url = `https://www.youtube.com/watch?v=${video_id}`; + const body = await request(new_url, { + headers: cookie + ? { + 'cookie': 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' } + }); + const player_response = JSON.parse(body.split('var ytInitialPlayerResponse = ')[1].split('}};')[0] + '}}'); + const initial_response = JSON.parse(body.split('var ytInitialData = ')[1].split('}};')[0] + '}}'); + const badge = + initial_response.contents.twoColumnWatchNextResults.results.results.contents[1]?.videoSecondaryInfoRenderer + ?.owner?.videoOwnerRenderer?.badges && + initial_response.contents.twoColumnWatchNextResults.results.results.contents[1]?.videoSecondaryInfoRenderer + ?.owner?.videoOwnerRenderer?.badges[0]; + if (player_response.playabilityStatus.status !== 'OK') + throw new Error( + `While getting info from url\n${ + player_response.playabilityStatus.errorScreen.playerErrorMessageRenderer?.reason.simpleText ?? + player_response.playabilityStatus.errorScreen.playerKavRenderer?.reason.simpleText + }` + ); + const html5player = `https://www.youtube.com${body.split('"jsUrl":"')[1].split('"')[0]}`; + const format = []; + const vid = player_response.videoDetails; + const microformat = player_response.microformat.playerMicroformatRenderer; + const video_details = { + id: vid.videoId, + url: `https://www.youtube.com/watch?v=${vid.videoId}`, + title: vid.title, + description: vid.shortDescription, + durationInSec: vid.lengthSeconds, + durationRaw: parseSeconds(vid.lengthSeconds), + uploadedDate: microformat.publishDate, + thumbnail: vid.thumbnail.thumbnails[vid.thumbnail.thumbnails.length - 1], + channel: { + name: vid.author, + id: vid.channelId, + url: `https://www.youtube.com/channel/${vid.channelId}`, + verified: Boolean(badge?.metadataBadgeRenderer?.style?.toLowerCase().includes('verified')) + }, + views: vid.viewCount, + tags: vid.keywords, + averageRating: vid.averageRating, + live: vid.isLiveContent, + private: vid.isPrivate + }; + if (!video_details.live) format.push(player_response.streamingData.formats[0]); + format.push(...player_response.streamingData.adaptiveFormats); + const LiveStreamData = { + isLive: video_details.live, + dashManifestUrl: player_response.streamingData?.dashManifestUrl ?? null, + hlsManifestUrl: player_response.streamingData?.hlsManifestUrl ?? null + }; + return { + LiveStreamData, + html5player, + format, + video_details + }; } -function parseSeconds(seconds : number): string { - let d = Number(seconds); - var h = Math.floor(d / 3600); - var m = Math.floor(d % 3600 / 60); - var s = Math.floor(d % 3600 % 60); +function parseSeconds(seconds: number): string { + const d = Number(seconds); + const h = Math.floor(d / 3600); + const m = Math.floor((d % 3600) / 60); + const s = Math.floor((d % 3600) % 60); - var hDisplay = h > 0 ? (h < 10 ? `0${h}` : h) + ':' : ""; - var mDisplay = m > 0 ? (m < 10 ? `0${m}` : m) + ':' : "00:"; - var sDisplay = s > 0 ? (s < 10 ? `0${s}` : s) : "00"; - return hDisplay + mDisplay + sDisplay; + const hDisplay = h > 0 ? (h < 10 ? `0${h}` : h) + ':' : ''; + const mDisplay = m > 0 ? (m < 10 ? `0${m}` : m) + ':' : '00:'; + const sDisplay = s > 0 ? (s < 10 ? `0${s}` : s) : '00'; + return hDisplay + mDisplay + sDisplay; } -export async function video_info(url : string, cookie? : string) { - let data = await video_basic_info(url, cookie) - if(data.LiveStreamData.isLive === true && data.LiveStreamData.hlsManifestUrl !== null){ - return data - } - else 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 video_info(url: string, cookie?: string) { + const data = await video_basic_info(url, cookie); + if (data.LiveStreamData.isLive === true && data.LiveStreamData.hlsManifestUrl !== null) { + return data; + } else 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, parseIncomplete : boolean = false) { - if (!url || typeof url !== "string") throw new Error(`Expected playlist url, received ${typeof url}!`); - let Playlist_id : string - if(url.startsWith('https')){ - if(yt_validate(url) !== 'playlist') throw new Error('This is not a Playlist URL') - Playlist_id = extractID(url) - } - else Playlist_id = url - let new_url = `https://www.youtube.com/playlist?list=${Playlist_id}` - - let body = await request(new_url, { - headers : {'accept-language' : 'en-US,en-IN;q=0.9,en;q=0.8,hi;q=0.7'} - }) - let response = JSON.parse(body.split("var ytInitialData = ")[1].split(";")[0]) - if(response.alerts){ - if(response.alerts[0].alertWithButtonRenderer?.type === 'INFO') { - if(!parseIncomplete) throw new Error(`While parsing playlist url\n${response.alerts[0].alertWithButtonRenderer.text.simpleText}`) - } - else if(response.alerts[0].alertRenderer?.type === 'ERROR') throw new Error(`While parsing playlist url\n${response.alerts[0].alertRenderer.text.runs[0].text}`) - else throw new Error('While parsing playlist url\nUnknown Playlist Error') +export async function playlist_info(url: string, parseIncomplete = false) { + if (!url || typeof url !== 'string') throw new Error(`Expected playlist url, received ${typeof url}!`); + let Playlist_id: string; + if (url.startsWith('https')) { + if (yt_validate(url) !== 'playlist') throw new Error('This is not a Playlist URL'); + Playlist_id = extractID(url); + } else Playlist_id = url; + const new_url = `https://www.youtube.com/playlist?list=${Playlist_id}`; + + const body = await request(new_url, { + headers: { 'accept-language': 'en-US,en-IN;q=0.9,en;q=0.8,hi;q=0.7' } + }); + const response = JSON.parse(body.split('var ytInitialData = ')[1].split(';')[0]); + if (response.alerts) { + if (response.alerts[0].alertWithButtonRenderer?.type === 'INFO') { + if (!parseIncomplete) + throw new Error( + `While parsing playlist url\n${response.alerts[0].alertWithButtonRenderer.text.simpleText}` + ); + } else if (response.alerts[0].alertRenderer?.type === 'ERROR') + throw new Error(`While parsing playlist url\n${response.alerts[0].alertRenderer.text.runs[0].text}`); + else throw new Error('While parsing playlist url\nUnknown Playlist Error'); } - 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; + const rawJSON = `${body.split('{"playlistVideoListRenderer":{"contents":')[1].split('}],"playlistId"')[0]}}]`; + const parsed = JSON.parse(rawJSON); + const 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); + const API_KEY = + body.split('INNERTUBE_API_KEY":"')[1]?.split('"')[0] ?? + body.split('innertubeApiKey":"')[1]?.split('"')[0] ?? + DEFAULT_API_KEY; + const videos = getPlaylistVideos(parsed, 100); - let data = playlistDetails[0].playlistSidebarPrimaryInfoRenderer; + const 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; + const author = playlistDetails[1]?.playlistSidebarSecondaryInfoRenderer.videoOwner; + const views = data.stats.length === 3 ? data.stats[1].simpleText.replace(/[^0-9]/g, '') : 0; + const lastUpdate = + data.stats + .find((x: any) => 'runs' in x && x['runs'].find((y: any) => y.text.toLowerCase().includes('last update'))) + ?.runs.pop()?.text ?? null; + const videosCount = data.stats[0].runs[0].text.replace(/[^0-9]/g, '') || 0; - let res = new PlayList({ + const 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] ?? "" + 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, @@ -174,16 +195,27 @@ export async function playlist_info(url : string, parseIncomplete : boolean = fa ? { 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 + 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 + 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[] { +export function getPlaylistVideos(data: any, limit = Infinity): Video[] { const videos = []; for (let i = 0; i < data.length; i++) { @@ -196,7 +228,7 @@ export function getPlaylistVideos(data:any, limit : number = Infinity) : Video[] id: info.videoId, index: parseInt(info.index?.simpleText) || 0, duration: parseDuration(info.lengthText?.simpleText) || 0, - duration_raw: info.lengthText?.simpleText ?? "0:00", + duration_raw: info.lengthText?.simpleText ?? '0:00', thumbnail: { id: info.videoId, url: info.thumbnail.thumbnails[info.thumbnail.thumbnails.length - 1].url, @@ -207,18 +239,21 @@ export function getPlaylistVideos(data:any, limit : number = Infinity) : Video[] 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}`, + url: `https://www.youtube.com${ + info.shortBylineText.runs[0].navigationEndpoint.browseEndpoint.canonicalBaseUrl || + info.shortBylineText.runs[0].navigationEndpoint.commandMetadata.webCommandMetadata.url + }`, icon: undefined } }) ); } - return videos + return videos; } function parseDuration(duration: string): number { - duration ??= "0:00"; - const args = duration.split(":"); + duration ??= '0:00'; + const args = duration.split(':'); let dur = 0; switch (args.length) { @@ -235,8 +270,8 @@ function parseDuration(duration: string): number { return dur; } - -export function getContinuationToken(data:any): string { - const continuationToken = data.find((x: any) => Object.keys(x)[0] === "continuationItemRenderer")?.continuationItemRenderer.continuationEndpoint?.continuationCommand?.token; +export function getContinuationToken(data: any): string { + const continuationToken = data.find((x: any) => Object.keys(x)[0] === 'continuationItemRenderer') + ?.continuationItemRenderer.continuationEndpoint?.continuationCommand?.token; return continuationToken; } diff --git a/play-dl/YouTube/utils/index.ts b/play-dl/YouTube/utils/index.ts index 121c72a..086c0d2 100644 --- a/play-dl/YouTube/utils/index.ts +++ b/play-dl/YouTube/utils/index.ts @@ -1 +1 @@ -export { video_basic_info, video_info, playlist_info, yt_validate, extractID } from './extractor' \ No newline at end of file +export { video_basic_info, video_info, playlist_info, yt_validate, extractID } from './extractor'; diff --git a/play-dl/YouTube/utils/parser.ts b/play-dl/YouTube/utils/parser.ts index a79f865..bd8d619 100644 --- a/play-dl/YouTube/utils/parser.ts +++ b/play-dl/YouTube/utils/parser.ts @@ -1,50 +1,51 @@ -import { Video } from "../classes/Video"; -import { PlayList } from "../classes/Playlist"; -import { Channel } from "../classes/Channel"; +import { Video } from '../classes/Video'; +import { PlayList } from '../classes/Playlist'; +import { Channel } from '../classes/Channel'; export interface ParseSearchInterface { - type?: "video" | "playlist" | "channel" ; + type?: 'video' | 'playlist' | 'channel'; limit?: number; } -export interface thumbnail{ +export interface thumbnail { width: string; - height : string; - url : string + height: string; + url: string; } -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"; +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 data = html.split("var ytInitialData = ")[1].split("}};")[0] + '}}'; - let json_data = JSON.parse(data) - let results = [] - let details = json_data.contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents[0].itemSectionRenderer.contents - for(let i = 0; i < details.length; i++){ - if (typeof options.limit === "number" && options.limit > 0 && results.length >= options.limit) break; - if (options.type === "video") { + const data = html.split('var ytInitialData = ')[1].split('}};')[0] + '}}'; + const json_data = JSON.parse(data); + const results = []; + const details = + json_data.contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents[0] + .itemSectionRenderer.contents; + for (let i = 0; i < details.length; i++) { + if (typeof options.limit === 'number' && options.limit > 0 && results.length >= options.limit) break; + if (options.type === 'video') { const parsed = parseVideo(details[i]); if (!parsed) continue; - results.push(parsed) - } else if (options.type === "channel") { + results.push(parsed); + } else if (options.type === 'channel') { const parsed = parseChannel(details[i]); if (!parsed) continue; - results.push(parsed) - } else if (options.type === "playlist") { + results.push(parsed); + } else if (options.type === 'playlist') { const parsed = parsePlaylist(details[i]); if (!parsed) continue; - results.push(parsed) + results.push(parsed); } } - return results + return results; } - function parseDuration(duration: string): number { - duration ??= "0:00"; - const args = duration.split(":"); + duration ??= '0:00'; + const args = duration.split(':'); let dur = 0; switch (args.length) { @@ -64,18 +65,27 @@ function parseDuration(duration: string): number { 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({ + const url = `https://www.youtube.com${ + data.channelRenderer.navigationEndpoint.browseEndpoint.canonicalBaseUrl || + data.channelRenderer.navigationEndpoint.commandMetadata.webCommandMetadata.url + }`; + const 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: 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' + verified: Boolean(badge?.metadataBadgeRenderer?.style?.toLowerCase().includes('verified')), + subscribers: data.channelRenderer.subscriberCountText?.simpleText + ? data.channelRenderer.subscriberCountText.simpleText + : '0 subscribers' }); return res; @@ -85,34 +95,42 @@ 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({ + const 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 : "", + 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: data.videoRenderer.thumbnail.thumbnails[data.videoRenderer.thumbnail.thumbnails.length - 1], 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}`, + 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 + 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")) + verified: Boolean(badge?.metadataBadgeRenderer?.style?.toLowerCase().includes('verified')) }, uploadedAt: data.videoRenderer.publishedTimeText?.simpleText ?? null, - views: data.videoRenderer.viewCountText?.simpleText?.replace(/[^0-9]/g, "") ?? 0, - live : data.videoRenderer.lengthText ? false : true, + views: data.videoRenderer.viewCountText?.simpleText?.replace(/[^0-9]/g, '') ?? 0, + live: data.videoRenderer.lengthText ? false : true }); return res; } - export function parsePlaylist(data?: any): PlayList | void { if (!data.playlistRenderer) return; @@ -122,19 +140,25 @@ export function parsePlaylist(data?: any): PlayList | void { 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 + 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, "")) + videos: parseInt(data.playlistRenderer.videoCount.replace(/[^0-9]/g, '')) }, true ); return res; -} \ No newline at end of file +} diff --git a/play-dl/YouTube/utils/request.ts b/play-dl/YouTube/utils/request.ts index 68c9756..c502b73 100644 --- a/play-dl/YouTube/utils/request.ts +++ b/play-dl/YouTube/utils/request.ts @@ -1,56 +1,61 @@ -import https, { RequestOptions } from 'https' -import { IncomingMessage } from 'http' -import { URL } from 'url' +import https, { RequestOptions } from 'https'; +import { IncomingMessage } from 'http'; +import { URL } from 'url'; -interface RequestOpts extends RequestOptions{ - body? : string; - method? : "GET" | "POST" +interface RequestOpts extends RequestOptions { + body?: string; + method?: 'GET' | 'POST'; } -function https_getter(req_url : string, options : RequestOpts = {}): Promise{ +function https_getter(req_url: string, options: RequestOpts = {}): Promise { return new Promise((resolve, reject) => { - let s = new URL(req_url) - options.method ??= "GET" - let req_options : RequestOptions = { - host : s.hostname, - path : s.pathname + s.search, - headers : options.headers ?? {}, - method : options.method - } + const s = new URL(req_url); + options.method ??= 'GET'; + const req_options: RequestOptions = { + host: s.hostname, + path: s.pathname + s.search, + headers: options.headers ?? {}, + method: options.method + }; - let req = https.request(req_options, resolve) + const req = https.request(req_options, resolve); req.on('error', (err) => { - reject(err) - }) - if(options.method === "POST") req.write(options.body) - req.end() - }) + reject(err); + }); + if (options.method === 'POST') req.write(options.body); + req.end(); + }); } -export async function request(url : string, options? : RequestOpts): Promise{ +export async function request(url: string, options?: RequestOpts): Promise { return new Promise(async (resolve, reject) => { - let data = '' - let res = await https_getter(url, options).catch((err: Error) => err) - if(res instanceof Error) {reject(res); return} - if(Number(res.statusCode) >= 300 && Number(res.statusCode) < 400){ - res = await https_getter(res.headers.location as string , options) + let data = ''; + let res = await https_getter(url, options).catch((err: Error) => err); + if (res instanceof Error) { + reject(res); + return; } - else if(Number(res.statusCode) > 400){ - reject(new Error(`Got ${res.statusCode} from the request`)) + 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) { + reject(new Error(`Got ${res.statusCode} from the request`)); } - res.setEncoding('utf-8') - res.on('data', (c) => data+=c) - res.on('end', () => resolve(data)) - }) + res.setEncoding('utf-8'); + res.on('data', (c) => (data += c)); + res.on('end', () => resolve(data)); + }); } -export async function request_stream(url : string, options? : RequestOpts): Promise{ +export async function request_stream(url: string, options?: RequestOpts): Promise { return new Promise(async (resolve, reject) => { - let res = await https_getter(url, options).catch((err: Error) => err) - if(res instanceof Error) {reject(res); return} - if(Number(res.statusCode) >= 300 && Number(res.statusCode) < 400){ - res = await https_getter(res.headers.location as string, options) + let res = await https_getter(url, options).catch((err: Error) => err); + if (res instanceof Error) { + reject(res); + return; } - resolve(res) - }) + if (Number(res.statusCode) >= 300 && Number(res.statusCode) < 400) { + res = await https_getter(res.headers.location as string, options); + } + resolve(res); + }); } diff --git a/play-dl/index.ts b/play-dl/index.ts index cf2fc41..436ef0e 100644 --- a/play-dl/index.ts +++ b/play-dl/index.ts @@ -1,22 +1,108 @@ -export { playlist_info, video_basic_info, video_info, search, stream, stream_from_info, yt_validate, extractID } from "./YouTube"; +import readline from 'readline'; -export { spotify, sp_validate, Authorization, RefreshToken, is_expired } from './Spotify' +export { + playlist_info, + video_basic_info, + video_info, + search, + stream, + stream_from_info, + yt_validate, + extractID +} from './YouTube'; -import { sp_validate, yt_validate } from "."; +export { spotify, sp_validate, refreshToken, is_expired } from './Spotify'; -export function validate(url : string): string | boolean{ - if(url.indexOf('spotify') !== -1){ - let check = sp_validate(url) - if(check){ - return "sp_" + check - } - else return check +export { soundcloud } from './SoundCloud'; + +import { sp_validate, yt_validate } from '.'; +import { SpotifyAuthorize } from './Spotify'; +import fs from 'fs'; +import { check_id } from './SoundCloud'; + +export function validate(url: string): string | boolean { + if (url.indexOf('spotify') !== -1) { + const check = sp_validate(url); + if (check) { + return 'sp_' + check; + } else return check; + } else { + const check = yt_validate(url); + if (check) { + return 'yt_' + check; + } else return check; } - else{ - let check = yt_validate(url) - if(check){ - return "yt_" + check +} + +export function authorization() { + const ask = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + ask.question('SoundCloud/ Spotify (so/sp) : ', (msg) => { + if (msg.toLowerCase().startsWith('sp')) { + let client_id: string, client_secret: string, redirect_url: string, market: string; + ask.question('Client ID : ', (id) => { + client_id = id; + ask.question('Client Secret : ', (secret) => { + client_secret = secret; + ask.question('Redirect URL : ', (url) => { + redirect_url = url; + console.log( + '\nMarket Selection URL : \nhttps://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements \n' + ); + ask.question('Market : ', (mar) => { + if (mar.length === 2) market = mar; + else { + console.log('Invalid Market, Selecting IN as market'); + market = 'IN'; + } + console.log( + '\nNow Go to your browser and Paste this url. Authroize it and paste the redirected url here. \n' + ); + console.log( + `https://accounts.spotify.com/authorize?client_id=${client_id}&response_type=code&redirect_uri=${encodeURI( + redirect_url + )} \n` + ); + ask.question('Redirected URL : ', async (url) => { + if (!fs.existsSync('.data')) fs.mkdirSync('.data'); + const spotifyData = { + client_id, + client_secret, + redirect_url, + authorization_code: url.split('code=')[1], + market + }; + const check = await SpotifyAuthorize(spotifyData); + if (check === false) throw new Error('Failed to get access Token.'); + ask.close(); + }); + }); + }); + }); + }); + } else if (msg.toLowerCase().startsWith('so')) { + let client_id: string; + ask.question('Client ID : ', async (id) => { + client_id = id; + if (!client_id) { + console.log("You didn't provided Client ID. Try again"); + ask.close(); + return; + } + if (!fs.existsSync('.data')) fs.mkdirSync('.data'); + console.log('Checking Client ID...................'); + if (await check_id(client_id)) { + console.log('Congratulations! Client ID is correct'); + fs.writeFileSync('.data/soundcloud.data', JSON.stringify({ client_id }, undefined, 4)); + } else console.log('Client ID is incorrect. Try to run this again with correct client ID.'); + + ask.close(); + }); + } else { + console.log('Invalid Option, Please Try again'); + ask.close(); } - else return check - } -} \ No newline at end of file + }); +}