From 81043ef09648c7cb5862b245bbed0ce7c66ba2cc Mon Sep 17 00:00:00 2001 From: killer069 <65385476+killer069@users.noreply.github.com> Date: Wed, 8 Sep 2021 15:06:07 +0530 Subject: [PATCH 1/6] Spotify v2.0 incoming --- play-dl/Spotify/index.ts | 76 ++++++++++++++++++++++++++++++++++++++++ play-dl/index.ts | 2 +- 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/play-dl/Spotify/index.ts b/play-dl/Spotify/index.ts index 5862436..8c23f08 100644 --- a/play-dl/Spotify/index.ts +++ b/play-dl/Spotify/index.ts @@ -1,6 +1,23 @@ import got from "got/dist/source" import { SpotifyAlbum, SpotifyPlaylist, SpotifyVideo } from "./classes" +import readline from 'readline' +import fs from 'fs' +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; +} + +const ask = readline.createInterface({ + input : process.stdin, + output : process.stdout +}) const pattern = /^((https:)?\/\/)?open.spotify.com\/(track|album|playlist)\// @@ -53,4 +70,63 @@ export function sp_validate(url : string): "track" | "playlist" | "album" | bool return "playlist" } else return false +} + +export function Authorization(){ + let client_id : string, client_secret : string, redirect_url : string; + let code : 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('Now Go to this url in your browser and Paste this url. Answer the next question \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 : ', (url) => { + code = url.split('code=')[1] + if (!fs.existsSync('.data')) fs.mkdirSync('.data') + fs.writeFileSync('.data/spotify.data', JSON.stringify({ + client_id, + client_secret, + redirect_url, + authorization_code : code + })) + ask.close() + }) + }) + }) + }) +} + +export async function StartSpotify(){ + if(!fs.existsSync('.data/spotify.data')) throw new Error('Spotify Data is Missing\nDid you forgot to do authorization ?') + + let data: SpotifyDataOptions = JSON.parse(fs.readFileSync('.data/spotify.data').toString()) + + if(data.authorization_code) data = await SpotifyAuthorize(data) + +} + +async function SpotifyAuthorize(data : SpotifyDataOptions): Promise{ + let response = await got.post(`https://accounts.spotify.com/api/token?grant_type=authorization_code&code=${data.authorization_code}&redirect_uri=${encodeURI(data.redirect_url)}`, { + headers : { + "Authorization" : `Basic ${Buffer.from(`${data.client_id}:${data.client_secret}`).toString('base64')}`, + "Content-Type" : "application/x-www-form-urlencoded" + } + }) + + if(response.statusCode === 200) { + let resp_json = JSON.parse(response.body) + return{ + 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), + token_type : resp_json.token_type + } + } + else throw new Error(`Got ${response.statusCode} while getting spotify access token\n${response.body}`) } \ No newline at end of file diff --git a/play-dl/index.ts b/play-dl/index.ts index 20896e0..b974ce4 100644 --- a/play-dl/index.ts +++ b/play-dl/index.ts @@ -1,6 +1,6 @@ export { playlist_info, video_basic_info, video_info, search, stream, stream_from_info, yt_validate, extractID } from "./YouTube"; -export { spotify, sp_validate } from './Spotify' +export { spotify, sp_validate, Authorization, StartSpotify } from './Spotify' import { sp_validate, yt_validate } from "."; From 7dcf3d09d188cc8107f59a738eba553768fb010c Mon Sep 17 00:00:00 2001 From: killer069 <65385476+killer069@users.noreply.github.com> Date: Wed, 8 Sep 2021 21:19:55 +0530 Subject: [PATCH 2/6] Changes requested by cjh980402 --- play-dl/YouTube/classes/LiveStream.ts | 23 +++++++---------------- play-dl/YouTube/utils/extractor.ts | 2 +- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/play-dl/YouTube/classes/LiveStream.ts b/play-dl/YouTube/classes/LiveStream.ts index 0e1e391..4ffd623 100644 --- a/play-dl/YouTube/classes/LiveStream.ts +++ b/play-dl/YouTube/classes/LiveStream.ts @@ -115,7 +115,7 @@ export class Stream { private url : string private bytes_count : number; private per_sec_bytes : number; - private timer : NodeJS.Timer | null + private content_length : number private request : Request | null constructor(url : string, type : StreamType, duration : number, contentLength : number){ this.url = url @@ -123,7 +123,7 @@ export class Stream { this.stream = new PassThrough({ highWaterMark : 10 * 1000 * 1000 }) this.bytes_count = 0 this.per_sec_bytes = Math.ceil(contentLength / duration) - this.timer = null + this.content_length = contentLength this.request = null this.stream.on('close', () => { this.cleanup() @@ -132,13 +132,12 @@ export class Stream { } private cleanup(){ - clearTimeout(this.timer as NodeJS.Timer) this.request?.unpipe(this.stream) this.request?.destroy() this.request = null - this.timer = null this.url = '' this.bytes_count = 0 + this.per_sec_bytes = 0 } private loop(){ @@ -146,10 +145,10 @@ export class Stream { this.cleanup() return } - let absolute_bytes : number = 0 + let end : number = this.bytes_count + this.per_sec_bytes * 300; let stream = got.stream(this.url, { headers : { - "range" : `bytes=${this.bytes_count}-` + "range" : `bytes=${this.bytes_count}-${end >= this.content_length ? '' : end}` } }) this.request = stream @@ -160,20 +159,12 @@ export class Stream { }) stream.on('data', (chunk: any) => { - absolute_bytes += chunk.length this.bytes_count += chunk.length - if(absolute_bytes > (this.per_sec_bytes * 300)){ - stream.destroy() - } }) stream.on('end', () => { - this.cleanup() + if(end < this.content_length) this.loop() + else this.cleanup() }) - - this.timer = setTimeout(() => { - this.request?.unpipe(this.stream) - this.loop() - }, 280 * 1000) } } diff --git a/play-dl/YouTube/utils/extractor.ts b/play-dl/YouTube/utils/extractor.ts index 90d09fd..19bf374 100644 --- a/play-dl/YouTube/utils/extractor.ts +++ b/play-dl/YouTube/utils/extractor.ts @@ -52,7 +52,7 @@ export async function video_basic_info(url : string, cookie? : string){ 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.reason || player_response.playabilityStatus.messages[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 From b55f0ad3d33f0bdc3a664b82de5c80c6e84e62b9 Mon Sep 17 00:00:00 2001 From: killer069 <65385476+killer069@users.noreply.github.com> Date: Wed, 8 Sep 2021 23:37:24 +0530 Subject: [PATCH 3/6] spotify revamped --- play-dl/Spotify/index.ts | 173 +++++++++++++++++++++++++-------------- play-dl/index.ts | 2 +- 2 files changed, 111 insertions(+), 64 deletions(-) diff --git a/play-dl/Spotify/index.ts b/play-dl/Spotify/index.ts index 8c23f08..613dae1 100644 --- a/play-dl/Spotify/index.ts +++ b/play-dl/Spotify/index.ts @@ -3,6 +3,8 @@ import { SpotifyAlbum, SpotifyPlaylist, SpotifyVideo } from "./classes" import readline from 'readline' import fs from 'fs' +var spotifyData : SpotifyDataOptions; + interface SpotifyDataOptions{ client_id : string; client_secret : string; @@ -12,50 +14,45 @@ interface SpotifyDataOptions{ refresh_token? : string; token_type? : string; expires_in? : number; + expiry? : number; + market? : string; } -const ask = readline.createInterface({ - input : process.stdin, - output : process.stdout -}) - const pattern = /^((https:)?\/\/)?open.spotify.com\/(track|album|playlist)\// export async function spotify(url : string): Promise{ if(!url.match(pattern)) throw new Error('This is not a Spotify URL') - let embed = embed_url(url) - let response = await got(embed) - return parse_json(embed, response.body) -} - -function parse_json(url : string, data : string): SpotifyAlbum | SpotifyPlaylist | SpotifyVideo{ - let json_data = JSON.parse(decodeURIComponent(data.split('')[0])) - if(url.indexOf('track') !== -1){ - return new SpotifyVideo(json_data) - } - else if(url.indexOf('album') !== -1){ - return new SpotifyAlbum(json_data) - } - else if(url.indexOf('playlist') !== -1){ - return new SpotifyPlaylist(json_data) - } - else throw new Error('Failed to parse data') -} - -function embed_url(url : string): string{ if(url.indexOf('track/') !== -1){ - let trackID = url.split('track/')[1].split('?')[0].split('/')[0].split('&')[0] - return `https://open.spotify.com/embed/track/${trackID}` + let trackID = url.split('track/')[1].split('&')[0].split('?')[0] + let response = await got(`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.body)) + 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].split('&')[0] - return `https://open.spotify.com/embed/album/${albumID}` + let albumID = url.split('album/')[1].split('&')[0].split('?')[0] + let response = await got(`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.body)) + 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].split('&')[0] - return `https://open.spotify.com/embed/playlist/${playlistID}` + let playlistID = url.split('playlist/')[1].split('&')[0].split('?')[0] + let response = await got(`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 SpotifyAlbum(JSON.parse(response.body)) + else throw new Error('Failed to get spotify Playlist Data') } - else throw new Error('Unable to generate embed url for given spotify url.') + else throw new Error('URL is out of scope for play-dl.') } export function sp_validate(url : string): "track" | "playlist" | "album" | boolean{ @@ -73,26 +70,39 @@ export function sp_validate(url : string): "track" | "playlist" | "album" | bool } export function Authorization(){ - let client_id : string, client_secret : string, redirect_url : string; - let code : string; + 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('Now Go to this url in your browser and Paste this url. Answer the next question \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 : ', (url) => { - code = url.split('code=')[1] - if (!fs.existsSync('.data')) fs.mkdirSync('.data') - fs.writeFileSync('.data/spotify.data', JSON.stringify({ - client_id, - client_secret, - redirect_url, - authorization_code : code - })) - ask.close() + 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 : ', (url) => { + if (!fs.existsSync('.data')) fs.mkdirSync('.data') + spotifyData = { + client_id, + client_secret, + redirect_url, + authorization_code : url.split('code=')[1], + market + } + fs.writeFileSync('.data/spotify.data', JSON.stringify(spotifyData, undefined, 4)) + ask.close() + }) }) }) }) @@ -101,32 +111,69 @@ export function Authorization(){ export async function StartSpotify(){ if(!fs.existsSync('.data/spotify.data')) throw new Error('Spotify Data is Missing\nDid you forgot to do authorization ?') - - let data: SpotifyDataOptions = JSON.parse(fs.readFileSync('.data/spotify.data').toString()) - - if(data.authorization_code) data = await SpotifyAuthorize(data) + if(!spotifyData) spotifyData = JSON.parse(fs.readFileSync('.data/spotify.data').toString()) + + if(spotifyData.authorization_code) { + let check = await SpotifyAuthorize(spotifyData) + if(check !== false) spotifyData = check + fs.writeFileSync('.data/spotify.data', JSON.stringify(spotifyData, undefined, 4)) + } } -async function SpotifyAuthorize(data : SpotifyDataOptions): Promise{ +async function SpotifyAuthorize(data : SpotifyDataOptions): Promise{ let response = await got.post(`https://accounts.spotify.com/api/token?grant_type=authorization_code&code=${data.authorization_code}&redirect_uri=${encodeURI(data.redirect_url)}`, { headers : { "Authorization" : `Basic ${Buffer.from(`${data.client_id}:${data.client_secret}`).toString('base64')}`, "Content-Type" : "application/x-www-form-urlencoded" } + }).catch(() => { + return 0 }) - if(response.statusCode === 200) { - let resp_json = JSON.parse(response.body) - return{ - 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), - token_type : resp_json.token_type - } + if(typeof response === 'number') return false + let resp_json = JSON.parse(response.body) + return{ + 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() + (Number(resp_json.expires_in) * 1000), + token_type : resp_json.token_type, + market : data.market } - else throw new Error(`Got ${response.statusCode} while getting spotify access token\n${response.body}`) +} + +export function is_expired(){ + if(Date.now() >= (spotifyData.expiry as number)) return true + else return false +} + +export async function RefreshToken(): Promise{ + let response = await got.post(`https://accounts.spotify.com/api/token?grant_type=refresh_token&refresh_token=${spotifyData.refresh_token}`, { + headers : { + "Authorization" : `Basic ${Buffer.from(`${spotifyData.client_id}:${spotifyData.client_secret}`).toString('base64')}`, + "Content-Type" : "application/x-www-form-urlencoded" + } + }).catch(() => { + return 0 + }) + + if(typeof response === 'number') return false + let resp_json = JSON.parse(response.body) + spotifyData = { + client_id : spotifyData.client_id, + client_secret : spotifyData.client_secret, + redirect_url : spotifyData.redirect_url, + access_token : resp_json.access_token, + refresh_token : spotifyData.refresh_token, + expires_in : Number(resp_json.expires_in), + expiry : Date.now() + (Number(resp_json.expires_in) * 1000), + token_type : resp_json.token_type, + market : spotifyData.market + } + fs.writeFileSync('.data/spotify.data', JSON.stringify(spotifyData, undefined, 4)) + return true } \ No newline at end of file diff --git a/play-dl/index.ts b/play-dl/index.ts index b974ce4..0f2f82d 100644 --- a/play-dl/index.ts +++ b/play-dl/index.ts @@ -1,6 +1,6 @@ export { playlist_info, video_basic_info, video_info, search, stream, stream_from_info, yt_validate, extractID } from "./YouTube"; -export { spotify, sp_validate, Authorization, StartSpotify } from './Spotify' +export { spotify, sp_validate, Authorization, StartSpotify, RefreshToken, is_expired } from './Spotify' import { sp_validate, yt_validate } from "."; From 139b540a2cc334ea522630714b0de706749c412f Mon Sep 17 00:00:00 2001 From: killer069 <65385476+killer069@users.noreply.github.com> Date: Thu, 9 Sep 2021 09:25:21 +0530 Subject: [PATCH 4/6] Basic Spotify --- play-dl/Spotify/index.ts | 27 +++++++++++---------------- play-dl/index.ts | 2 +- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/play-dl/Spotify/index.ts b/play-dl/Spotify/index.ts index 613dae1..c25b6c0 100644 --- a/play-dl/Spotify/index.ts +++ b/play-dl/Spotify/index.ts @@ -4,6 +4,9 @@ import readline from 'readline' import fs from 'fs' var spotifyData : SpotifyDataOptions; +if(fs.existsSync('.data/spotify.data')){ + spotifyData = JSON.parse(fs.readFileSync('.data/spotify.data').toString()) +} interface SpotifyDataOptions{ client_id : string; @@ -21,6 +24,7 @@ interface SpotifyDataOptions{ 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] @@ -91,7 +95,7 @@ export function Authorization(){ } 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 : ', (url) => { + ask.question('Redirected URL : ',async (url) => { if (!fs.existsSync('.data')) fs.mkdirSync('.data') spotifyData = { client_id, @@ -100,7 +104,8 @@ export function Authorization(){ authorization_code : url.split('code=')[1], market } - fs.writeFileSync('.data/spotify.data', JSON.stringify(spotifyData, undefined, 4)) + let check = await SpotifyAuthorize(spotifyData) + if(check === false) throw new Error('Failed to get access Token.') ask.close() }) }) @@ -109,19 +114,7 @@ export function Authorization(){ }) } -export async function StartSpotify(){ - if(!fs.existsSync('.data/spotify.data')) throw new Error('Spotify Data is Missing\nDid you forgot to do authorization ?') - - if(!spotifyData) spotifyData = JSON.parse(fs.readFileSync('.data/spotify.data').toString()) - - if(spotifyData.authorization_code) { - let check = await SpotifyAuthorize(spotifyData) - if(check !== false) spotifyData = check - fs.writeFileSync('.data/spotify.data', JSON.stringify(spotifyData, undefined, 4)) - } -} - -async function SpotifyAuthorize(data : SpotifyDataOptions): Promise{ +async function SpotifyAuthorize(data : SpotifyDataOptions): Promise{ let response = await got.post(`https://accounts.spotify.com/api/token?grant_type=authorization_code&code=${data.authorization_code}&redirect_uri=${encodeURI(data.redirect_url)}`, { headers : { "Authorization" : `Basic ${Buffer.from(`${data.client_id}:${data.client_secret}`).toString('base64')}`, @@ -133,7 +126,7 @@ async function SpotifyAuthorize(data : SpotifyDataOptions): Promise Date: Thu, 9 Sep 2021 12:11:11 +0530 Subject: [PATCH 5/6] Spotify Work + Error Language = EN - US --- README.md | 7 ++ docs/Spotify/README.md | 93 ++++++++++++++++++++--- examples/Spotify/authorize.js | 3 + examples/Spotify/play.js | 4 +- play-dl/Spotify/classes.ts | 114 +++++++++++++++++++++++++--- play-dl/Spotify/index.ts | 6 +- play-dl/YouTube/classes/Playlist.ts | 2 +- play-dl/YouTube/search.ts | 4 +- play-dl/YouTube/utils/extractor.ts | 6 +- 9 files changed, 211 insertions(+), 28 deletions(-) create mode 100644 examples/Spotify/authorize.js diff --git a/README.md b/README.md index dac4746..9449a92 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,13 @@ This is a **light-weight** youtube downloader and searcher. npm install play-dl@latest ``` +### Importing +```ts +import * as play from 'play-dl' // ES-6 import or TS import + +const play = require('play-dl') //JS importing +``` + ### Examples - [YouTube](https://github.com/play-dl/play-dl/tree/main/examples/YouTube) - [Spotify](https://github.com/play-dl/play-dl/tree/main/examples/Spotify) diff --git a/docs/Spotify/README.md b/docs/Spotify/README.md index ea98563..5f50780 100644 --- a/docs/Spotify/README.md +++ b/docs/Spotify/README.md @@ -1,5 +1,88 @@ # Spotify +## Main +### spotify(url : `string`) +*This returns data from a track | playlist | album url.* + +```js +let data = await spotify(url) //Gets the data + +console.log(data.type) // Console logs the type of data that you got. +``` + +### Authorization() +*This creates basic spotify data to be stored locally.* + +```js +Authorization() //After then you will be asked client-id, client-secret, redirect url, market, redirected URL. +``` + +### is_expired() +*This tells that whether the access token is expired or not* + +**Returns :** `boolean` + +```js +if(is_expired()){ + await RefreshToken() +} +``` + +### RefreshToken() +*This refreshes the access token.* + +**Returns :** `boolean` for telling whether access token is refreshed or not + +```js +await RefreshToken() +``` + +## Classes [ Returned by spotify() function ] +### SpotifyVideo +*Don't go by the name. This is class for a spotify track.* + +#### type `property` +*This will always return as "track" for this class.* + +#### toJSON() `function` +*converts class into a json format* + +### SpotifyPlaylist +*This is a spotify playlist class.* + +#### fetch() `function` +*This will fetch tracks in a playlist upto 1000 tracks only.* + +```js +let data = await spotify(playlist_url) + +await data.fetch() // Fetches tracks more than 100 tracks in playlist +``` + +#### type `property` +*This will always return as "playlist" for this class.* + +#### toJSON() `function` +*converts class into a json format* + +### SpotifyAlbum +*This is a spotify albun class.* + +#### fetch() `function` +*This will fetch tracks in a album upto 500 tracks only.* + +```js +let data = await spotify(playlist_url) + +await data.fetch() // Fetches tracks more than 50 tracks in album +``` + +#### type `property` +*This will always return as "album" for this class.* + +#### toJSON() `function` +*converts class into a json format* + ## Validate ### sp_validate(url : `string`) *This checks that given url is spotify url or not.* @@ -11,14 +94,4 @@ let check = sp_validate(url) if(!check) // Invalid Spotify URL if(check === 'track') // Spotify Track URL -``` - -## Main -### spotify(url : `string`) -*This returns data from a track | playlist | album url.* - -```js -let data = spotify(url) //Gets the data - -console.log(data.type) // Console logs the type of data that you got. ``` \ No newline at end of file diff --git a/examples/Spotify/authorize.js b/examples/Spotify/authorize.js new file mode 100644 index 0000000..a45b5ed --- /dev/null +++ b/examples/Spotify/authorize.js @@ -0,0 +1,3 @@ +const { Authorization } = require('play-dl'); + +Authorization() \ No newline at end of file diff --git a/examples/Spotify/play.js b/examples/Spotify/play.js index 9fefe09..4634763 100644 --- a/examples/Spotify/play.js +++ b/examples/Spotify/play.js @@ -14,7 +14,9 @@ client.on('messageCreate', async message => { guildId : message.guild.id, adapterCreator: message.guild.voiceAdapterCreator }) - + if(play.is_expired()){ + await play.RefreshToken() // This will check if access token has expired or not. If yes, then refresh the token. + } let args = message.content.split('play ')[1].split(' ')[0] let sp_data = await play.spotify(args) // This will get spotify data from the url [ I used track url, make sure to make a logic for playlist, album ] let searched = await play.search(`${sp_data.name}`, { limit : 1 }) // This will search the found track on youtube. diff --git a/play-dl/Spotify/classes.ts b/play-dl/Spotify/classes.ts index 0186b20..e107490 100644 --- a/play-dl/Spotify/classes.ts +++ b/play-dl/Spotify/classes.ts @@ -1,3 +1,5 @@ +import got, { Response } from "got/dist/source"; +import { SpotifyDataOptions } from "."; interface SpotifyTrackAlbum{ @@ -90,8 +92,10 @@ export class SpotifyPlaylist{ id : string; thumbnail : SpotifyThumbnail; owner : SpotifyArtists; - tracks : SpotifyVideo[] - constructor(data : any){ + 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 @@ -104,11 +108,56 @@ export class SpotifyPlaylist{ 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.tracks = videos + 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 got(`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[] = [] + let res = response as Response + let json_data = JSON.parse(res.body) + json_data.items.forEach((v : any) => { + videos.push(new SpotifyVideo(v.track)) + }) + this.fetched_tracks.set(`${i}`, videos) + resolve('Success') + })) + } + 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}`) + } + + 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 } toJSON(){ @@ -121,7 +170,6 @@ export class SpotifyPlaylist{ id : this.id, thumbnail : this.thumbnail, owner : this.owner, - tracks : this.tracks } } } @@ -130,16 +178,19 @@ 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; - total_tracks : number - tracks : SpotifyTracks[] - constructor(data : any){ + 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[] = [] @@ -154,12 +205,56 @@ export class SpotifyAlbum{ this.copyrights = data.copyrights this.release_date = data.release_date this.release_date_precision = data.release_date_precision - this.total_tracks = data.total_tracks + this.trackCount = data.total_tracks let videos: SpotifyTracks[] = [] data.tracks.items.forEach((v : any) => { videos.push(new SpotifyTracks(v)) }) - this.tracks = videos + 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 got(`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[] = [] + let res = response as Response + let json_data = JSON.parse(res.body) + json_data.items.forEach((v : any) => { + videos.push(new SpotifyTracks(v)) + }) + this.fetched_tracks.set(`${i}`, videos) + resolve('Success') + })) + } + 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}`) + } + + 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 } toJSON(){ @@ -173,7 +268,6 @@ export class SpotifyAlbum{ release_date : this.release_date, release_date_precision : this.release_date_precision, total_tracks : this.total_tracks, - tracks : this.tracks } } } diff --git a/play-dl/Spotify/index.ts b/play-dl/Spotify/index.ts index c25b6c0..955c2f1 100644 --- a/play-dl/Spotify/index.ts +++ b/play-dl/Spotify/index.ts @@ -8,7 +8,7 @@ if(fs.existsSync('.data/spotify.data')){ spotifyData = JSON.parse(fs.readFileSync('.data/spotify.data').toString()) } -interface SpotifyDataOptions{ +export interface SpotifyDataOptions{ client_id : string; client_secret : string; redirect_url : string; @@ -43,7 +43,7 @@ export async function spotify(url : string): Promise {return 0}) - if(typeof response !== 'number') return new SpotifyAlbum(JSON.parse(response.body)) + if(typeof response !== 'number') return new SpotifyAlbum(JSON.parse(response.body), spotifyData) else throw new Error('Failed to get spotify Album Data') } else if(url.indexOf('playlist/') !== -1){ @@ -53,7 +53,7 @@ export async function spotify(url : string): Promise {return 0}) - if(typeof response !== 'number') return new SpotifyAlbum(JSON.parse(response.body)) + if(typeof response !== 'number') return new SpotifyPlaylist(JSON.parse(response.body), spotifyData) else throw new Error('Failed to get spotify Playlist Data') } else throw new Error('URL is out of scope for play-dl.') diff --git a/play-dl/YouTube/classes/Playlist.ts b/play-dl/YouTube/classes/Playlist.ts index 2eb5ec2..dfead2b 100644 --- a/play-dl/YouTube/classes/Playlist.ts +++ b/play-dl/YouTube/classes/Playlist.ts @@ -109,7 +109,7 @@ export class PlayList{ } page(number : number): Video[]{ - if(!number) throw new Error('Given Page number is not provided') + 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[] } diff --git a/play-dl/YouTube/search.ts b/play-dl/YouTube/search.ts index 4df4f6d..913ff48 100644 --- a/play-dl/YouTube/search.ts +++ b/play-dl/YouTube/search.ts @@ -27,7 +27,9 @@ export async function search(search :string, options? : ParseSearchInterface): P break } } - let body = await url_get(url) + let body = await url_get(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 diff --git a/play-dl/YouTube/utils/extractor.ts b/play-dl/YouTube/utils/extractor.ts index 19bf374..b7774c2 100644 --- a/play-dl/YouTube/utils/extractor.ts +++ b/play-dl/YouTube/utils/extractor.ts @@ -47,7 +47,7 @@ export async function video_basic_info(url : string, cookie? : string){ else video_id = url let new_url = `https://www.youtube.com/watch?v=${video_id}` let body = await url_get(new_url, { - headers : (cookie) ? { 'cookie' : cookie } : {} + 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] + '}}') @@ -129,7 +129,9 @@ export async function playlist_info(url : string, parseIncomplete : boolean = fa else Playlist_id = url let new_url = `https://www.youtube.com/playlist?list=${Playlist_id}` - let body = await url_get(new_url) + let body = await url_get(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') { From 68daeed093a39b861434b79ba94456980d0c2cb7 Mon Sep 17 00:00:00 2001 From: killer069 <65385476+killer069@users.noreply.github.com> Date: Thu, 9 Sep 2021 12:44:49 +0530 Subject: [PATCH 6/6] Search command cleaned up --- play-dl/YouTube/search.ts | 1 + play-dl/YouTube/utils/parser.ts | 66 +++++++-------------------------- 2 files changed, 15 insertions(+), 52 deletions(-) diff --git a/play-dl/YouTube/search.ts b/play-dl/YouTube/search.ts index 913ff48..010788f 100644 --- a/play-dl/YouTube/search.ts +++ b/play-dl/YouTube/search.ts @@ -13,6 +13,7 @@ enum SearchType { export async function search(search :string, options? : ParseSearchInterface): Promise<(Video | Channel | PlayList)[]> { let url = 'https://www.youtube.com/results?search_query=' + search.replaceAll(' ', '+') + if(!options || options.type) options = { type : "video" } if(!url.match('&sp=')){ url += '&sp=' switch(options?.type){ diff --git a/play-dl/YouTube/utils/parser.ts b/play-dl/YouTube/utils/parser.ts index 24afcc6..a79f865 100644 --- a/play-dl/YouTube/utils/parser.ts +++ b/play-dl/YouTube/utils/parser.ts @@ -3,7 +3,7 @@ import { PlayList } from "../classes/Playlist"; import { Channel } from "../classes/Channel"; export interface ParseSearchInterface { - type?: "video" | "playlist" | "channel" | "all"; + type?: "video" | "playlist" | "channel" ; limit?: number; } @@ -13,73 +13,35 @@ export interface thumbnail{ url : string } -export function ParseSearchResult(html :string, options? : ParseSearchInterface): (Video | PlayList | Channel)[] { +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 = [] - let fetched = false; - - try { - let data = html.split("ytInitialData = JSON.parse('")[1].split("');")[0]; - html = data.replace(/\\x([0-9A-F]{2})/gi, (...items) => { - return String.fromCharCode(parseInt(items[1], 16)); - }); - } catch { - /* do nothing */ - } - - try { - details = JSON.parse(html.split('{"itemSectionRenderer":{"contents":')[html.split('{"itemSectionRenderer":{"contents":').length - 1].split(',"continuations":[{')[0]); - fetched = true; - } catch { - /* Do nothing*/ - } - - if (!fetched) { - try { - details = JSON.parse(html.split('{"itemSectionRenderer":')[html.split('{"itemSectionRenderer":').length - 1].split('},{"continuationItemRenderer":{')[0]).contents; - fetched = true; - } catch { - /* do nothing */ - } - } - - if (!fetched) throw new Error('Failed to Fetch the data') - - for (let i = 0; i < details.length; i++) { + 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; - let data = details[i]; - let res; - if (options.type === "all") { - if (!!data.videoRenderer) options.type = "video"; - else if (!!data.channelRenderer) options.type = "channel"; - else if (!!data.playlistRenderer) options.type = "playlist"; - else continue; - } - if (options.type === "video") { - const parsed = parseVideo(data); + const parsed = parseVideo(details[i]); if (!parsed) continue; - res = parsed; + results.push(parsed) } else if (options.type === "channel") { - const parsed = parseChannel(data); + const parsed = parseChannel(details[i]); if (!parsed) continue; - res = parsed; + results.push(parsed) } else if (options.type === "playlist") { - const parsed = parsePlaylist(data); + const parsed = parsePlaylist(details[i]); if (!parsed) continue; - res = parsed; + results.push(parsed) } - - results.push(res); } - -return results as (Video | Channel | PlayList)[]; + return results } + function parseDuration(duration: string): number { duration ??= "0:00"; const args = duration.split(":");