## Change Log

- [x] Added Global search feature.
- [x] Changed related video ID to video URL.
- [x] Added quality support in Stream function
This commit is contained in:
Killer069 2021-09-24 15:23:24 +05:30 committed by GitHub
commit 6242e0238f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 201 additions and 113 deletions

View File

@ -40,14 +40,46 @@ _This creates basic spotify / soundcloud data to be stored locally._
authorization() //After then you will be asked about type of data you want to create and then follow the steps properly. authorization() //After then you will be asked about type of data you want to create and then follow the steps properly.
``` ```
### Search
#### SearchOptions :
- limit : `number` :- Sets total amount of results you want.
- source : {
youtube: `video` | `playlist` | `channel` ;
spotify: `album` | `playlist` | `track` ;
soundcloud: `tracks` | `playlists` | `albums` ;
}
#### search(query : `string`, options? : [`SearchOptions`]())
_This is basic to search with any source._
**NOTE :-** If no options.source is not specified, then it will default to youtube video search.
```js
let data = await search('Rick Roll', { limit : 1, source { youtube : "video" } }) // Searches for youtube video
let data = await search('Rick Roll', { limit: 1, source { soundcloud : "track" } }) // Searches for spotify track.
let data = await search('Rick Roll', { limit: 1, source { spotify : "tracks" } }) // Searches for soundcloud track.
```
### Stream ### Stream
#### stream(url : `string`, cookie? : `string`) #### StreamOptions :
- quality : `number` :- Sets quality of stream [ 0 = Lowest, 1 = Medium ]. Leave this empty to get highest audio quality.
- cookie : `string` :- **[Cookies](https://github.com/play-dl/play-dl/discussions/34)** are optional and are required for playing age restricted videos.
#### stream(url : `string`, options? : [`StreamOptions`]())
_This is basic to create a stream from a youtube or soundcloud url._ _This is basic to create a stream from a youtube or soundcloud url._
**[Cookies](https://github.com/play-dl/play-dl/discussions/34) are optional and are required for playing age restricted videos.**
```js ```js
let source = await stream("url") // This will create a stream Class. let source = await stream("url") // This will create a stream Class.
@ -56,20 +88,18 @@ let resource = createAudioResource(source.stream, {
}) // This creates resource for playing }) // This creates resource for playing
``` ```
### stream_from_info(info : `infoData`, cookie? : `string`) #### stream_from_info(info : `infoData`, options? : [`StreamOptions`]())
_This is basic to create a stream from a info [ from [video_info](https://github.com/play-dl/play-dl#video_infourl--string) function or [soundcloud]() function [**Only SoundCloudTrack class is allowed**] ]._ _This is basic to create a stream from a info [ from [video_info](https://github.com/play-dl/play-dl#video_infourl--string) function or [soundcloud]() function [**Only SoundCloudTrack class is allowed**] ]._
**[Cookies](https://github.com/play-dl/play-dl/discussions/34) are optional and are required for playing age restricted videos.**
**Note :** Here, cookies are required only for retrying purposes. **Note :** Here, cookies are required only for retrying purposes.
```js ```js
let source = await stream_from_info(info) // This will create a stream Class from video_info or SoundCoudTrack Class. let source = await stream_from_info(info) // This will create a stream Class from video_info or SoundCoudTrack Class.
/* OR
let source = await stream_from_info(info, cookie) This will create a stream Class and also give cookies if retrying. let source = await stream_from_info(info, { cookie }) //This will create a stream Class and also give cookies if retrying.
*/
let resource = createAudioResource(source.stream, { let resource = createAudioResource(source.stream, {
inputType : source.type inputType : source.type

View File

@ -41,24 +41,6 @@ _This will return videoID or playlistID from a url_
let id = extractID(url) let id = extractID(url)
``` ```
## Search
### search(url : `string`, options? : [SearchOptions](https://github.com/play-dl/play-dl/tree/main/play-dl/YouTube#searchoptions))
_This enables all searching mechanism (video, channel, playlist)_
```js
const options = {
limit : 1
}
const results = await youtube.search('never gonna give you up', options);
console.log(results[0].url);
```
- #### SearchOptions
- _type_ : `video` | `channel` | `playlist`
- _limit_ : `integer`
## Video ## Video
### video_basic_info(url : `string`, cookie? : `string`) ### video_basic_info(url : `string`, cookie? : `string`)

View File

@ -17,13 +17,13 @@ client.on('messageCreate', async message => {
}) })
let args = message.content.split('play ')[1].split(' ')[0] let args = message.content.split('play ')[1].split(' ')[0]
let stream = await play.stream(args, COOKIE) let stream = await play.stream(args, { cookie : COOKIE })
/* /*
OR if you want to get info about youtube link and then stream it OR if you want to get info about youtube link and then stream it
let yt_info = await play.video_info(args, COOKIE) let yt_info = await play.video_info(args, { cookie : COOKIE })
console.log(yt_info.video_details.title) console.log(yt_info.video_details.title)
let stream = await play.stream_from_info(yt_info, COOKIE) let stream = await play.stream_from_info(yt_info, { cookie : COOKIE })
*/ */
let resource = createAudioResource(stream.stream, { let resource = createAudioResource(stream.stream, {

View File

@ -22,7 +22,7 @@ interface SoundCloudTrackDeprecated {
type: 'track'; type: 'track';
} }
interface SoundCloudTrackFormat { export interface SoundCloudTrackFormat {
url: string; url: string;
preset: string; preset: string;
duration: number; duration: number;
@ -90,13 +90,13 @@ export class SoundCloudTrack {
id: this.id, id: this.id,
type: this.type, type: this.type,
url: this.url, url: this.url,
fetched : this.fetched, fetched: this.fetched,
durationInMs: this.durationInMs, durationInMs: this.durationInMs,
durationInSec: this.durationInSec, durationInSec: this.durationInSec,
publisher: this.publisher, publisher: this.publisher,
formats: this.formats, formats: this.formats,
thumbnail: this.thumbnail, thumbnail: this.thumbnail,
user : this.user user: this.user
}; };
} }
} }
@ -169,13 +169,13 @@ export class SoundCloudPlaylist {
await Promise.allSettled(work); await Promise.allSettled(work);
} }
get total_tracks(){ get total_tracks() {
let count = 0 let count = 0;
this.tracks.forEach((track) => { this.tracks.forEach((track) => {
if(track instanceof SoundCloudTrack) count++ if (track instanceof SoundCloudTrack) count++;
else return else return;
}) });
return count return count;
} }
toJSON() { toJSON() {
@ -183,19 +183,19 @@ export class SoundCloudPlaylist {
name: this.name, name: this.name,
id: this.id, id: this.id,
type: this.type, type: this.type,
sub_type : this.sub_type, sub_type: this.sub_type,
url: this.url, url: this.url,
durationInMs: this.durationInMs, durationInMs: this.durationInMs,
durationInSec: this.durationInSec, durationInSec: this.durationInSec,
tracksCount : this.tracksCount, tracksCount: this.tracksCount,
user : this.user, user: this.user,
tracks : this.tracks tracks: this.tracks
}; };
} }
} }
export class Stream { export class Stream {
stream : PassThrough; stream: PassThrough;
type: StreamType; type: StreamType;
private url: string; private url: string;
private playing_count: number; private playing_count: number;

View File

@ -1,7 +1,7 @@
import fs from 'fs'; import fs from 'fs';
import { StreamType } from '../YouTube/stream'; import { StreamType } from '../YouTube/stream';
import { request } from '../YouTube/utils/request'; import { request } from '../YouTube/utils/request';
import { SoundCloudPlaylist, SoundCloudTrack, Stream } from './classes'; import { SoundCloudPlaylist, SoundCloudTrack, SoundCloudTrackFormat, Stream } from './classes';
let soundData: SoundDataOptions; let soundData: SoundDataOptions;
if (fs.existsSync('.data/soundcloud.data')) { if (fs.existsSync('.data/soundcloud.data')) {
@ -33,23 +33,48 @@ export async function soundcloud(url: string): Promise<SoundCloudTrack | SoundCl
else return new SoundCloudPlaylist(json_data, soundData.client_id); else return new SoundCloudPlaylist(json_data, soundData.client_id);
} }
export async function stream(url: string): Promise<Stream> { export async function so_search(
query: string,
type: 'tracks' | 'playlists' | 'albums',
limit: number = 10
): Promise<(SoundCloudPlaylist | SoundCloudTrack)[]> {
const response = await request(
`https://api-v2.soundcloud.com/search/${type}?q=${query}&client_id=${soundData.client_id}&limit=${limit}`
);
const results: (SoundCloudPlaylist | SoundCloudTrack)[] = [];
const json_data = JSON.parse(response);
json_data.collection.forEach((x: any) => {
if (type === 'tracks') results.push(new SoundCloudTrack(x));
else results.push(new SoundCloudPlaylist(x, soundData.client_id));
});
return results;
}
export async function stream(url: string, quality?: number): Promise<Stream> {
const data = await soundcloud(url); const data = await soundcloud(url);
if (data instanceof SoundCloudPlaylist) throw new Error("Streams can't be created from Playlist url"); if (data instanceof SoundCloudPlaylist) throw new Error("Streams can't be created from Playlist url");
const req_url = data.formats[data.formats.length - 1].url + '?client_id=' + soundData.client_id; const HLSformats = parseHlsFormats(data.formats);
if (typeof quality !== 'number') quality = HLSformats.length - 1;
else if (quality <= 0) quality = 0;
else if (quality >= HLSformats.length) quality = HLSformats.length - 1;
const req_url = HLSformats[quality].url + '?client_id=' + soundData.client_id;
const s_data = JSON.parse(await request(req_url)); const s_data = JSON.parse(await request(req_url));
const type = data.formats[data.formats.length - 1].format.mime_type.startsWith('audio/ogg') const type = HLSformats[quality].format.mime_type.startsWith('audio/ogg')
? StreamType.OggOpus ? StreamType.OggOpus
: StreamType.Arbitrary; : StreamType.Arbitrary;
return new Stream(s_data.url, type); return new Stream(s_data.url, type);
} }
export async function stream_from_info(data: SoundCloudTrack): Promise<Stream> { export async function stream_from_info(data: SoundCloudTrack, quality?: number): Promise<Stream> {
const req_url = data.formats[data.formats.length - 1].url + '?client_id=' + soundData.client_id; const HLSformats = parseHlsFormats(data.formats);
if (typeof quality !== 'number') quality = HLSformats.length - 1;
else if (quality <= 0) quality = 0;
else if (quality >= HLSformats.length) quality = HLSformats.length - 1;
const req_url = HLSformats[quality].url + '?client_id=' + soundData.client_id;
const s_data = JSON.parse(await request(req_url)); const s_data = JSON.parse(await request(req_url));
const type = data.formats[data.formats.length - 1].format.mime_type.startsWith('audio/ogg') const type = HLSformats[quality].format.mime_type.startsWith('audio/ogg')
? StreamType.OggOpus ? StreamType.OggOpus
: StreamType.Arbitrary; : StreamType.Arbitrary;
return new Stream(s_data.url, type); return new Stream(s_data.url, type);
@ -77,3 +102,11 @@ export async function so_validate(url: string): Promise<false | 'track' | 'playl
else if (json_data.kind === 'playlist') return 'playlist'; else if (json_data.kind === 'playlist') return 'playlist';
else return false; else return false;
} }
function parseHlsFormats(data: SoundCloudTrackFormat[]) {
const result: SoundCloudTrackFormat[] = [];
data.forEach((format) => {
if (format.format.protocol === 'hls') result.push(format);
});
return result;
}

View File

@ -110,6 +110,45 @@ export function is_expired(): boolean {
else return false; else return false;
} }
export async function sp_search(
query: string,
type: 'album' | 'playlist' | 'track',
limit: number = 10
): Promise<(SpotifyAlbum | SpotifyPlaylist | SpotifyVideo)[]> {
const results: (SpotifyAlbum | SpotifyPlaylist | SpotifyVideo)[] = [];
if (!spotifyData) throw new Error('Spotify Data is missing\nDid you forgot to do authorization ?');
if (query.length === 0) throw new Error('Pass some query to search.');
if (limit > 50 || limit < 0) throw new Error(`You crossed limit range of Spotify [ 0 - 50 ]`);
const response = await request(
`https://api.spotify.com/v1/search?type=${type}&q=${query.replaceAll(' ', '+')}&limit=${limit}&market=${
spotifyData.market
}`,
{
headers: {
Authorization: `${spotifyData.token_type} ${spotifyData.access_token}`
}
}
).catch((err: Error) => {
return err;
});
if (response instanceof Error) throw response;
const json_data = JSON.parse(response);
if (type === 'track') {
json_data.tracks.items.forEach((track: any) => {
results.push(new SpotifyVideo(track));
});
} else if (type === 'album') {
json_data.albums.items.forEach((album: any) => {
results.push(new SpotifyAlbum(album, spotifyData));
});
} else if (type === 'playlist') {
json_data.playlists.items.forEach((playlist: any) => {
results.push(new SpotifyPlaylist(playlist, spotifyData));
});
}
return results;
}
export async function refreshToken(): Promise<boolean> { export async function refreshToken(): Promise<boolean> {
const response = await request(`https://accounts.spotify.com/api/token`, { const response = await request(`https://accounts.spotify.com/api/token`, {
headers: { headers: {

View File

@ -11,7 +11,7 @@ export interface FormatInterface {
} }
export class LiveStreaming { export class LiveStreaming {
stream : PassThrough; stream: PassThrough;
type: StreamType; type: StreamType;
private base_url: string; private base_url: string;
private url: string; private url: string;
@ -23,7 +23,7 @@ export class LiveStreaming {
private segments_urls: string[]; private segments_urls: string[];
private request: IncomingMessage | null; private request: IncomingMessage | null;
constructor(dash_url: string, target_interval: number, video_url: string) { constructor(dash_url: string, target_interval: number, video_url: string) {
this.stream = new PassThrough({ highWaterMark: 10 * 1000 * 1000 }) this.stream = new PassThrough({ highWaterMark: 10 * 1000 * 1000 });
this.type = StreamType.Arbitrary; this.type = StreamType.Arbitrary;
this.url = dash_url; this.url = dash_url;
this.base_url = ''; this.base_url = '';
@ -121,7 +121,7 @@ export class LiveStreaming {
} }
export class Stream { export class Stream {
stream : PassThrough; stream: PassThrough;
type: StreamType; type: StreamType;
private url: string; private url: string;
private bytes_count: number; private bytes_count: number;

View File

@ -1,3 +1,2 @@
export { search } from './search';
export { stream, stream_from_info } from './stream'; export { stream, stream_from_info } from './stream';
export * from './utils'; export * from './utils';

View File

@ -10,7 +10,7 @@ enum SearchType {
Channel = 'EgIQAg%253D%253D' Channel = 'EgIQAg%253D%253D'
} }
export async function search( export async function yt_search(
search: string, search: string,
options: ParseSearchInterface = {} options: ParseSearchInterface = {}
): Promise<(Video | Channel | PlayList)[]> { ): Promise<(Video | Channel | PlayList)[]> {

View File

@ -9,6 +9,11 @@ export enum StreamType {
Opus = 'opus' Opus = 'opus'
} }
export interface StreamOptions {
quality?: number;
cookie?: string;
}
export interface InfoData { export interface InfoData {
LiveStreamData: { LiveStreamData: {
isLive: boolean; isLive: boolean;
@ -33,10 +38,9 @@ function parseAudioFormats(formats: any[]) {
return result; return result;
} }
export async function stream(url: string, cookie?: string): Promise<Stream | LiveStreaming> { export async function stream(url: string, options: StreamOptions = {}): Promise<Stream | LiveStreaming> {
const info = await video_info(url, cookie); const info = await video_info(url, options.cookie);
const final: any[] = []; const final: any[] = [];
let type: StreamType;
if ( if (
info.LiveStreamData.isLive === true && info.LiveStreamData.isLive === true &&
info.LiveStreamData.hlsManifestUrl !== null && info.LiveStreamData.hlsManifestUrl !== null &&
@ -50,33 +54,26 @@ export async function stream(url: string, cookie?: string): Promise<Stream | Liv
} }
const audioFormat = parseAudioFormats(info.format); const audioFormat = parseAudioFormats(info.format);
const opusFormats = filterFormat(audioFormat, 'opus'); if (typeof options.quality !== 'number') options.quality = audioFormat.length - 1;
else if (options.quality <= 0) options.quality = 0;
if (opusFormats.length === 0) { else if (options.quality >= audioFormat.length) options.quality = audioFormat.length - 1;
type = StreamType.Arbitrary; final.push(audioFormat[options.quality]);
if (audioFormat.length === 0) { let type: StreamType =
final.push(info.format[info.format.length - 1]); audioFormat[options.quality].codec === 'opus' && audioFormat[options.quality].container === 'webm'
} else { ? StreamType.WebmOpus
final.push(audioFormat[audioFormat.length - 1]); : StreamType.Arbitrary;
}
} else {
type = StreamType.WebmOpus;
final.push(opusFormats[opusFormats.length - 1]);
}
return new Stream( return new Stream(
final[0].url, final[0].url,
type, type,
info.video_details.durationInSec, info.video_details.durationInSec,
Number(final[0].contentLength), Number(final[0].contentLength),
info.video_details.url, info.video_details.url,
cookie as string options.cookie as string
); );
} }
export async function stream_from_info(info: InfoData, cookie?: string): Promise<Stream | LiveStreaming> { export async function stream_from_info(info: InfoData, options: StreamOptions = {}): Promise<Stream | LiveStreaming> {
const final: any[] = []; const final: any[] = [];
let type: StreamType;
if ( if (
info.LiveStreamData.isLive === true && info.LiveStreamData.isLive === true &&
info.LiveStreamData.hlsManifestUrl !== null && info.LiveStreamData.hlsManifestUrl !== null &&
@ -90,34 +87,20 @@ export async function stream_from_info(info: InfoData, cookie?: string): Promise
} }
const audioFormat = parseAudioFormats(info.format); const audioFormat = parseAudioFormats(info.format);
const opusFormats = filterFormat(audioFormat, 'opus'); if (typeof options.quality !== 'number') options.quality = audioFormat.length - 1;
else if (options.quality <= 0) options.quality = 0;
if (opusFormats.length === 0) { else if (options.quality >= audioFormat.length) options.quality = audioFormat.length - 1;
type = StreamType.Arbitrary; final.push(audioFormat[options.quality]);
if (audioFormat.length === 0) { let type: StreamType =
final.push(info.format[info.format.length - 1]); audioFormat[options.quality].codec === 'opus' && audioFormat[options.quality].container === 'webm'
} else { ? StreamType.WebmOpus
final.push(audioFormat[audioFormat.length - 1]); : StreamType.Arbitrary;
}
} else {
type = StreamType.WebmOpus;
final.push(opusFormats[opusFormats.length - 1]);
}
return new Stream( return new Stream(
final[0].url, final[0].url,
type, type,
info.video_details.durationInSec, info.video_details.durationInSec,
Number(final[0].contentLength), Number(final[0].contentLength),
info.video_details.url, info.video_details.url,
cookie as string options.cookie as string
); );
} }
function filterFormat(formats: any[], codec: string) {
const result: any[] = [];
formats.forEach((format) => {
if (format.codec === codec) result.push(format);
});
return result;
}

View File

@ -66,10 +66,13 @@ export async function video_basic_info(url: string, cookie?: string) {
initial_response.contents.twoColumnWatchNextResults.results.results.contents[1]?.videoSecondaryInfoRenderer initial_response.contents.twoColumnWatchNextResults.results.results.contents[1]?.videoSecondaryInfoRenderer
?.owner?.videoOwnerRenderer?.badges[0]; ?.owner?.videoOwnerRenderer?.badges[0];
const html5player = `https://www.youtube.com${body.split('"jsUrl":"')[1].split('"')[0]}`; const html5player = `https://www.youtube.com${body.split('"jsUrl":"')[1].split('"')[0]}`;
const related: any[] = [] const related: any[] = [];
initial_response.contents.twoColumnWatchNextResults.secondaryResults.secondaryResults.results.forEach((res: any) => { initial_response.contents.twoColumnWatchNextResults.secondaryResults.secondaryResults.results.forEach(
if(res.compactVideoRenderer) related.push(res.compactVideoRenderer.videoId) (res: any) => {
}) if (res.compactVideoRenderer)
related.push(`https://www.youtube.com/watch?v=${res.compactVideoRenderer.videoId}`);
}
);
const format = []; const format = [];
const vid = player_response.videoDetails; const vid = player_response.videoDetails;
const microformat = player_response.microformat.playerMicroformatRenderer; const microformat = player_response.microformat.playerMicroformatRenderer;
@ -106,7 +109,7 @@ export async function video_basic_info(url: string, cookie?: string) {
html5player, html5player,
format, format,
video_details, video_details,
related_videos : related related_videos: related
}; };
} }

View File

@ -1,27 +1,46 @@
export { playlist_info, video_basic_info, video_info, search, yt_validate, extractID } from './YouTube'; export { playlist_info, video_basic_info, video_info, yt_validate, extractID } from './YouTube';
export { spotify, sp_validate, refreshToken, is_expired } from './Spotify'; export { spotify, sp_validate, refreshToken, is_expired } from './Spotify';
export { soundcloud, so_validate } from './SoundCloud'; export { soundcloud, so_validate } from './SoundCloud';
interface SearchOptions {
limit?: number;
source?: {
youtube?: 'video' | 'playlist' | 'channel';
spotify?: 'album' | 'playlist' | 'track';
soundcloud?: 'tracks' | 'playlists' | 'albums';
};
}
import readline from 'readline'; import readline from 'readline';
import fs from 'fs'; import fs from 'fs';
import { sp_validate, yt_validate, so_validate } from '.'; import { sp_validate, yt_validate, so_validate } from '.';
import { SpotifyAuthorize } from './Spotify'; import { SpotifyAuthorize, sp_search } from './Spotify';
import { check_id, stream as so_stream, stream_from_info as so_stream_info } from './SoundCloud'; import { check_id, so_search, stream as so_stream, stream_from_info as so_stream_info } from './SoundCloud';
import { InfoData, stream as yt_stream, stream_from_info as yt_stream_info } from './YouTube/stream'; import { InfoData, stream as yt_stream, StreamOptions, stream_from_info as yt_stream_info } from './YouTube/stream';
import { SoundCloudTrack, Stream as SoStream } from './SoundCloud/classes'; import { SoundCloudTrack, Stream as SoStream } from './SoundCloud/classes';
import { LiveStreaming, Stream as YTStream } from './YouTube/classes/LiveStream'; import { LiveStreaming, Stream as YTStream } from './YouTube/classes/LiveStream';
import { yt_search } from './YouTube/search';
export async function stream(url: string, cookie?: string): Promise<YTStream | LiveStreaming | SoStream> { export async function stream(url: string, options: StreamOptions = {}): Promise<YTStream | LiveStreaming | SoStream> {
if (url.indexOf('soundcloud') !== -1) return await so_stream(url); if (url.length === 0) throw new Error('Stream URL has a length of 0. Check your url again.');
else return await yt_stream(url, cookie); if (url.indexOf('soundcloud') !== -1) return await so_stream(url, options.quality);
else return await yt_stream(url, { cookie: options.cookie });
}
export async function search(query: string, options: SearchOptions = {}) {
if (!options.source) options.source = { youtube: 'video' };
if (options.source.youtube) return await yt_search(query, { limit: options.limit, type: options.source.youtube });
else if (options.source.spotify) return await sp_search(query, options.source.spotify, options.limit);
else if (options.source.soundcloud) return await so_search(query, options.source.soundcloud, options.limit);
} }
export async function stream_from_info( export async function stream_from_info(
info: InfoData | SoundCloudTrack, info: InfoData | SoundCloudTrack,
cookie?: string options: StreamOptions = {}
): Promise<YTStream | LiveStreaming | SoStream> { ): Promise<YTStream | LiveStreaming | SoStream> {
if (info instanceof SoundCloudTrack) return await so_stream_info(info); if (info instanceof SoundCloudTrack) return await so_stream_info(info);
else return await yt_stream_info(info, cookie); else return await yt_stream_info(info, { cookie: options.cookie });
} }
export async function validate(url: string): Promise<string | boolean> { export async function validate(url: string): Promise<string | boolean> {