From 65026abca165346da8f52809195ff97667cde74c Mon Sep 17 00:00:00 2001 From: killer069 <65385476+killer069@users.noreply.github.com> Date: Mon, 20 Sep 2021 17:20:15 +0530 Subject: [PATCH] SoundCloud Work Completed --- package.json | 3 +- play-dl/SoundCloud/classes.ts | 100 +++++++++++++++++++---------- play-dl/SoundCloud/index.ts | 40 +++++++++++- play-dl/Spotify/index.ts | 8 +-- play-dl/YouTube/stream.ts | 2 +- play-dl/YouTube/utils/extractor.ts | 6 +- play-dl/index.ts | 49 ++++++++------ 7 files changed, 143 insertions(+), 65 deletions(-) diff --git a/package.json b/package.json index b41f164..6922d08 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,7 @@ "scripts": { "build": "tsc", "build:check": "tsc --noEmit --incremental false", - "pretty": "prettier --config .prettierrc \"play-dl/*.ts\" \"play-dl/*/*.ts\" \"play-dl/*/*/*.ts\" --write ", - "lint": "eslint . --ext .ts" + "pretty": "prettier --config .prettierrc \"play-dl/*.ts\" \"play-dl/*/*.ts\" \"play-dl/*/*/*.ts\" --write " }, "repository": { "type": "git", diff --git a/play-dl/SoundCloud/classes.ts b/play-dl/SoundCloud/classes.ts index 3a2672d..67aa7d7 100644 --- a/play-dl/SoundCloud/classes.ts +++ b/play-dl/SoundCloud/classes.ts @@ -48,7 +48,7 @@ export class SoundCloudTrack { artist: string; contains_music: boolean; writer_composer: string; - }; + } | null; thumbanil: string; user: SoundCloudUser; constructor(data: any) { @@ -59,13 +59,15 @@ export class SoundCloudTrack { 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 - }; + if (data.publisher_metadata) + 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 + }; + else this.publisher = null; this.formats = data.media.transcodings; this.user = { name: data.user.username, @@ -131,12 +133,12 @@ export class SoundCloudPlaylist { this.tracks = tracks; } - async fetch() { + async fetch(): Promise { const work: any[] = []; for (let i = 0; i < this.tracks.length; i++) { if (!this.tracks[i].fetched) { work.push( - new Promise(async (resolve, reject) => { + new Promise(async (resolve) => { const num = i; const data = await request( `https://api-v2.soundcloud.com/tracks/${this.tracks[i].id}?client_id=${this.client_id}` @@ -158,16 +160,20 @@ export class Stream { private url: string; private playing_count: number; private downloaded_time: number; - private request : IncomingMessage | null + private downloaded_segments: number; + private request: IncomingMessage | null; + private data_ended: boolean; private time: number[]; private segment_urls: string[]; - constructor(url: string, type: StreamType = StreamType.Arbitrary, client_id: string) { + constructor(url: string, type: StreamType = StreamType.Arbitrary) { this.type = type; - this.url = url + client_id; + this.url = url; this.stream = new PassThrough({ highWaterMark: 10 * 1000 * 1000 }); this.playing_count = 0; this.downloaded_time = 0; - this.request = null + this.request = null; + this.downloaded_segments = 0; + this.data_ended = false; this.time = []; this.segment_urls = []; this.stream.on('close', () => { @@ -175,6 +181,13 @@ export class Stream { }); this.stream.on('pause', () => { this.playing_count++; + if (this.data_ended) { + this.cleanup(); + this.stream.removeAllListeners('pause'); + } else if (this.playing_count === 120) { + this.playing_count = 0; + this.start(); + } }); this.start(); } @@ -200,26 +213,47 @@ export class Stream { this.cleanup(); return; } + this.time = []; + this.segment_urls = []; 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); - }); - }); - } + this.downloaded_time = 0; + this.segment_urls.splice(0, this.downloaded_segments); + this.loop(); } - private cleanup() {} + private async loop() { + if (this.stream.destroyed) { + this.cleanup(); + return; + } + if (this.time.length === 0 || this.segment_urls.length === 0) { + this.data_ended = true; + return; + } + this.downloaded_time += this.time.shift() as number; + this.downloaded_segments++; + const stream = await request_stream(this.segment_urls.shift() as string).catch((err: Error) => err); + if (stream instanceof Error) throw stream; + + stream.pipe(this.stream, { end: false }); + stream.on('end', () => { + if (this.downloaded_time >= 300) return; + else this.loop(); + }); + stream.once('error', (err) => { + this.stream.emit('error', err); + }); + } + + private cleanup() { + this.request?.unpipe(this.stream); + this.request?.destroy(); + this.url = ''; + this.playing_count = 0; + this.downloaded_time = 0; + this.downloaded_segments = 0; + this.request = null; + this.time = []; + this.segment_urls = []; + } } diff --git a/play-dl/SoundCloud/index.ts b/play-dl/SoundCloud/index.ts index 891a877..f32a2a9 100644 --- a/play-dl/SoundCloud/index.ts +++ b/play-dl/SoundCloud/index.ts @@ -1,6 +1,7 @@ import fs from 'fs'; +import { StreamType } from '../YouTube/stream'; import { request } from '../YouTube/utils/request'; -import { SoundCloudPlaylist, SoundCloudTrack } from './classes'; +import { SoundCloudPlaylist, SoundCloudTrack, Stream } from './classes'; let soundData: SoundDataOptions; if (fs.existsSync('.data/soundcloud.data')) { @@ -32,7 +33,29 @@ export async function soundcloud(url: string): Promise { + const data = await soundcloud(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 s_data = JSON.parse(await request(req_url)); + const type = data.formats[data.formats.length - 1].format.mime_type.startsWith('audio/ogg') + ? StreamType.OggOpus + : StreamType.Arbitrary; + return new Stream(s_data.url, type); +} + +export async function stream_from_info(data: SoundCloudTrack): Promise { + const req_url = data.formats[data.formats.length - 1].url + '?client_id=' + soundData.client_id; + const s_data = JSON.parse(await request(req_url)); + const type = data.formats[data.formats.length - 1].format.mime_type.startsWith('audio/ogg') + ? StreamType.OggOpus + : StreamType.Arbitrary; + return new Stream(s_data.url, type); +} + +export async function check_id(id: string): Promise { const response = await request(`https://api-v2.soundcloud.com/search?client_id=${id}&q=Rick+Roll&limit=0`).catch( (err: Error) => { return err; @@ -41,3 +64,16 @@ export async function check_id(id: string) { if (response instanceof Error) return false; else return true; } + +export async function so_validate(url: string): Promise { + 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') return 'track'; + else if (json_data.kind === 'playlist') return 'playlist'; + else return false; +} diff --git a/play-dl/Spotify/index.ts b/play-dl/Spotify/index.ts index de4399c..0842462 100644 --- a/play-dl/Spotify/index.ts +++ b/play-dl/Spotify/index.ts @@ -64,7 +64,7 @@ export async function spotify(url: string): Promise= (spotifyData.expiry as number)) return true; else return false; } -export async function refreshToken(): Promise { +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( @@ -123,7 +123,7 @@ export async function refreshToken(): Promise { }).catch((err: Error) => { return err; }); - if (response instanceof Error) throw response; + if (response instanceof Error) return false; const resp_json = JSON.parse(response); spotifyData.access_token = resp_json.access_token; spotifyData.expires_in = Number(resp_json.expires_in); diff --git a/play-dl/YouTube/stream.ts b/play-dl/YouTube/stream.ts index 54f8e30..4b54a8f 100644 --- a/play-dl/YouTube/stream.ts +++ b/play-dl/YouTube/stream.ts @@ -9,7 +9,7 @@ export enum StreamType { Opus = 'opus' } -interface InfoData { +export interface InfoData { LiveStreamData: { isLive: boolean; dashManifestUrl: string; diff --git a/play-dl/YouTube/utils/extractor.ts b/play-dl/YouTube/utils/extractor.ts index bb8f110..f1911ee 100644 --- a/play-dl/YouTube/utils/extractor.ts +++ b/play-dl/YouTube/utils/extractor.ts @@ -8,7 +8,7 @@ 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 { +export function yt_validate(url: string): 'playlist' | 'video' | false { if (url.indexOf('list=') === -1) { if (!url.match(video_pattern)) return false; else return 'video'; @@ -90,8 +90,8 @@ export async function video_basic_info(url: string, cookie?: string) { live: vid.isLiveContent, private: vid.isPrivate }; - format.push(...player_response.streamingData.formats ?? []); - format.push(...player_response.streamingData.adaptiveFormats ?? []); + format.push(...(player_response.streamingData.formats ?? [])); + format.push(...(player_response.streamingData.adaptiveFormats ?? [])); const LiveStreamData = { isLive: video_details.live, dashManifestUrl: player_response.streamingData?.dashManifestUrl ?? null, diff --git a/play-dl/index.ts b/play-dl/index.ts index 436ef0e..34b2d6b 100644 --- a/play-dl/index.ts +++ b/play-dl/index.ts @@ -1,31 +1,40 @@ -import readline from 'readline'; - -export { - playlist_info, - video_basic_info, - video_info, - search, - stream, - stream_from_info, - yt_validate, - extractID -} from './YouTube'; - +export { playlist_info, video_basic_info, video_info, search, yt_validate, extractID } from './YouTube'; export { spotify, sp_validate, refreshToken, is_expired } from './Spotify'; +export { soundcloud, so_validate } from './SoundCloud'; -export { soundcloud } from './SoundCloud'; - -import { sp_validate, yt_validate } from '.'; -import { SpotifyAuthorize } from './Spotify'; +import readline from 'readline'; import fs from 'fs'; -import { check_id } from './SoundCloud'; +import { sp_validate, yt_validate, so_validate } from '.'; +import { SpotifyAuthorize } from './Spotify'; +import { check_id, 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 { SoundCloudTrack, Stream as SoStream } from './SoundCloud/classes'; +import { LiveStreaming, Stream } from './YouTube/classes/LiveStream'; -export function validate(url: string): string | boolean { +export async function stream(url: string, cookie?: string): Promise { + if (url.indexOf('soundcloud') !== -1) return await so_stream(url); + else return await yt_stream(url, cookie); +} + +export async function stream_from_info( + info: InfoData | SoundCloudTrack, + cookie?: string +): Promise { + if (info instanceof SoundCloudTrack) return await so_stream_info(info); + else return await yt_stream_info(info, cookie); +} + +export async function validate(url: string): Promise { if (url.indexOf('spotify') !== -1) { const check = sp_validate(url); if (check) { return 'sp_' + check; } else return check; + } else if (url.indexOf('soundcloud') !== -1) { + const check = await so_validate(url); + if (check) { + return 'so_' + check; + } else return check; } else { const check = yt_validate(url); if (check) { @@ -34,7 +43,7 @@ export function validate(url: string): string | boolean { } } -export function authorization() { +export function authorization(): void { const ask = readline.createInterface({ input: process.stdin, output: process.stdout