Merge pull request #7 from play-dl/Killer

Stream Finished
This commit is contained in:
C++, JS, TS, Python Developer 2021-08-16 15:30:07 +05:30 committed by GitHub
commit 3f303bd956
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 355 additions and 33 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
node_modules/
dist/
play-dl/Stream/stream_final.ts

View File

@ -1,3 +0,0 @@
export { search } from './search'
export * from './utils'

View File

@ -1,12 +0,0 @@
//This File is in testing stage, everything will change in this
import { playlist_info, video_basic_info, video_info, search } from "./YouTube";
let main = async() => {
let time_start = Date.now()
let result = await search('Rick Roll')
console.log(result[0].url)
let time_end = Date.now()
console.log(`Time Taken : ${(time_end - time_start)/1000} seconds`)
}
main()

10
package-lock.json generated
View File

@ -1,13 +1,13 @@
{
"name": "killer",
"version": "1.0.0",
"name": "play-dl",
"version": "0.0.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "killer",
"version": "1.0.0",
"license": "ISC",
"name": "play-dl",
"version": "0.0.2",
"license": "MIT",
"dependencies": {
"node-fetch": "^2.6.1"
},

View File

@ -1,22 +1,36 @@
{
"name": "killer",
"version": "1.0.0",
"description": "",
"name": "play-dl",
"version": "0.0.2",
"description": "YouTube, SoundCloud, Spotify downloader",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"build" : "tsc",
"build:check": "tsc --noEmit --incremental false"
},
"repository": {
"type": "git",
"url": "git+https://github.com/killer069/node-youtube-dl.git"
"url": "git+https://github.com/play-dl/play-dl"
},
"keywords": [],
"author": "",
"license": "ISC",
"keywords": [
"node-youtube-dl",
"youtube-dl",
"yt-dl",
"ytdl",
"youtube",
"spotify-dl",
"spotify",
"soundcloud"
],
"author": "Killer069",
"license": "MIT",
"bugs": {
"url": "https://github.com/killer069/node-youtube-dl/issues"
"url": "https://github.com/play-dl/play-dl/issues"
},
"homepage": "https://github.com/killer069/node-youtube-dl#readme",
"homepage": "https://github.com/play-dl/play-dl#readme",
"files": [
"dist/*"
],
"dependencies": {
"node-fetch": "^2.6.1"
},

225
play-dl/Stream/stream.ts Normal file
View File

@ -0,0 +1,225 @@
import { IncomingMessage, ClientRequest } from 'http';
import { Socket } from 'net'
import { EventEmitter } from 'node:events'
import { RequestOptions, default as https } from 'https'
import { URL } from 'url'
import { Transform, PassThrough } from 'stream'
interface StreamOptions extends RequestOptions {
maxRetries?: number;
maxReconnects?: number;
highWaterMark?: number;
backoff?: { inc: number; max: number };
}
class StreamError extends Error{
public statusCode? : number;
constructor(message:string, statusCode? : number){
super(message)
this.statusCode = statusCode
}
}
export interface Stream extends PassThrough {
abort: (err?: Error) => void;
aborted: boolean;
destroy: (err?: Error) => void;
destroyed: boolean;
text: () => Promise<string>;
on(event: 'reconnect', listener: (attempt: number, err?: Error) => void): this;
on(event: 'retry', listener: (attempt: number, err?: Error) => void): this;
on(event: 'redirect', listener: (streaming_url: string) => void): this;
on(event: string | symbol, listener: (...args: any) => void): this;
}
var default_opts : StreamOptions = {
maxReconnects : 2,
maxRetries : 5,
highWaterMark : 15 * 1000 * 1000,
backoff : {
inc : 100,
max : 1000
}
}
interface RetryOptions {
err?: Error;
retryAfter?: number;
}
class Streaming {
private opts : StreamOptions;
private stream : Stream;
private streaming_url : URL;
private request? : ClientRequest;
private response? : IncomingMessage;
private actual_stream : Transform | null;
private retries : number;
private retryTime? : NodeJS.Timer;
private reconnects : number;
private contentLength : number;
private downloaded : number;
private retryStatusCodes = new Set([429, 503]);
constructor(stream_url : string | URL, options : StreamOptions = {}){
this.streaming_url = (stream_url instanceof URL) ? stream_url : new URL(stream_url)
this.opts = Object.assign({}, default_opts, options)
this.stream = new PassThrough({ highWaterMark: this.opts.highWaterMark }) as Stream
this.stream.destroyed = this.stream.aborted = false
this.retries = 0
this.reconnects = 0
this.contentLength = 0
this.downloaded = 0
this.actual_stream = null
}
creation = () => {
process.nextTick(() => this.download())
return this.stream
}
private downloadStarted = () => { return Boolean(this.actual_stream && this.downloaded > 0) }
private downloadCompleted = () => { return Boolean(this.downloaded === this.contentLength) }
private reconnect = (err? : StreamError) => {
this.actual_stream = null
this.retries = 0
let ms = Math.min(this.opts.backoff?.inc as number, this.opts.backoff?.max as number)
this.retryTime = setTimeout(this.download , ms)
this.stream.emit('reconnect', this.reconnects, err)
}
private Earlyreconnect = (err? : StreamError) => {
if(this.opts.method !== 'HEAD' && !this.downloadCompleted() && this.reconnects++ < (this.opts.maxReconnects as number)){
this.reconnect(err)
return true
}
else return false
}
private retryrequest = (retryOptions : RetryOptions) => {
if(!this.opts.backoff?.inc || this.stream.destroyed) return false
if(this.downloadStarted()){
return this.Earlyreconnect(retryOptions.err)
}
else if((!retryOptions.err || retryOptions.err.message === 'ENOTFOUND') && this.reconnects++ < (this.opts.maxReconnects as number)){
let ms = retryOptions.retryAfter ||
Math.min(this.retries * this.opts.backoff.inc, this.opts.backoff.max);
this.retryTime = setTimeout(this.download, ms)
return true
}
return false
}
private OnError = (err? : StreamError) => {
if (this.stream.destroyed || this.stream.readableEnded) { return; }
this.cleanup();
if (!this.retryrequest({ err })) {
this.stream.emit('error', err);
} else {
this.request?.removeListener('close', this.onRequestClose);
}
}
private onRequestClose = () => {
this.cleanup();
this.retryrequest({})
}
private cleanup = () => {
this.request?.removeListener('close', this.onRequestClose)
this.response?.removeListener('data', this.OnData)
this.stream.removeListener('end', this.OnEnd)
}
private OnData(chunk : Buffer){
this.downloaded += chunk.length
}
private OnEnd = () => {
this.cleanup()
if(!this.Earlyreconnect()){
this.stream.end()
}
}
private forwardEvents = (ee: EventEmitter, events: string[]) => {
for (let event of events) {
ee.on(event, this.stream.emit.bind(this.stream, event));
}
};
private download = () => {
let parsed : RequestOptions = {}
parsed = {
host : this.streaming_url.host,
hostname : this.streaming_url.hostname,
path : this.streaming_url.pathname + this.streaming_url.search + this.streaming_url.hash,
port : this.streaming_url.port,
protocol : this.streaming_url.protocol
}
if(this.streaming_url.username){
parsed.auth = `${this.streaming_url.username}:${this.streaming_url.password}`
}
Object.assign(parsed, this.opts)
this.request = https.request(parsed, (res : IncomingMessage) => this.OnResponse(res))
this.request?.on('error', this.OnError)
this.request.on('close', this.onRequestClose)
this.forwardEvents(this.request, ['connect', 'continue', 'information', 'socket', 'timeout', 'upgrade'])
if (this.stream.destroyed) {
this.Destroy();
}
this.stream.emit('request', this.request);
this.request.end();
}
private Destroy = () => {
this.request?.destroy(new Error('Some Error Occured'))
this.actual_stream?.unpipe()
this.actual_stream?.destroy()
clearTimeout(this.retryTime as NodeJS.Timer)
}
private OnResponse = (res : IncomingMessage) => {
if(this.stream.destroyed) return
if(this.retryStatusCodes.has(res.statusCode as number)){
if(!this.retryrequest({ retryAfter: parseInt(res.headers['retry-after'] || '0', 10) })){
let err = new StreamError(`Status code: ${res.statusCode}`, res.statusCode)
this.stream.emit('error', err)
}
this.cleanup();
return;
}
else if(res.statusCode && (res.statusCode < 200 || res.statusCode >= 400)){
let err = new StreamError(`Status code: ${res.statusCode}`, res.statusCode);
if (res.statusCode >= 500) {
this.OnError(err);
} else {
this.stream.emit('error', err);
}
this.cleanup();
return;
}
if(!this.contentLength){
this.contentLength = parseInt(`${res.headers['content-length']}`, 10)
}
this.actual_stream = res as unknown as Transform
res.on('data', this.OnData)
this.actual_stream.on('end', this.OnEnd)
this.actual_stream.pipe(this.stream)
this.response = res
this.stream.emit('response', res)
res.on('error', this.OnError)
this.forwardEvents(res, ['aborted'])
}
}
export function stream(stream_url : string | URL, options? : StreamOptions){
let new_stream = new Streaming(stream_url, options)
return new_stream.creation()
}

5
play-dl/YouTube/index.ts Normal file
View File

@ -0,0 +1,5 @@
import { stream } from './stream'
export { search } from './search'
export { stream }
export * from './utils'

87
play-dl/YouTube/stream.ts Normal file
View File

@ -0,0 +1,87 @@
import { video_info } from "."
import { Stream, stream as yt_stream } from "../Stream/stream"
interface FilterOptions {
averagebitrate? : number;
videoQuality? : "144p" | "240p" | "360p" | "480p" | "720p" | "1080p";
audioQuality? : "AUDIO_QUALITY_LOW" | "AUDIO_QUALITY_MEDIUM";
audioSampleRate? : number;
audioChannels? : number;
audioCodec? : string;
audioContainer? : string;
hasAudio? : boolean;
hasVideo? : boolean;
isLive? : boolean;
}
interface StreamOptions {
filter : "bestaudio" | "bestvideo"
}
function parseFormats(formats : any[]): { audio: any[], video:any[] } {
let audio: any[] = []
let video: any[] = []
formats.forEach((format) => {
let type = format.mimeType as string
if(type.startsWith('audio')){
format.audioCodec = type.split('codecs="')[1].split('"')[0]
format.audioContainer = type.split('audio/')[1].split(';')[0]
format.hasAudio = true
format.hasVideo = false
audio.push(format)
}
else if(type.startsWith('video')){
format.videoQuality = format.qualityLabel
format.hasAudio = false
format.hasVideo = true
video.push(format)
}
})
return { audio, video }
}
function filter_songs(formats : any[], options : FilterOptions) {
}
export async function stream(url : string, options? : StreamOptions): Promise<Stream>{
let info = await video_info(url)
let final: any[] = [];
if(options?.filter === 'bestaudio'){
info.format.forEach((format) => {
let type = format.mimeType as string
if(type.startsWith('audio/webm')){
return final.push(format)
}
else return
})
if(final.length === 0){
info.format.forEach((format) => {
let type = format.mimeType as string
if(type.startsWith('audio/')){
return final.push(format)
}
else return
})
}
}
else if(options?.filter === 'bestvideo'){
info.format.forEach((format) => {
let type = format.mimeType as string
if(type.startsWith('video/')){
if(parseInt(format.qualityLabel) > 480) final.push(format)
else return
}
else return
})
if(final.length === 0) throw new Error("Video Format > 480p is not found")
}
else{
final.push(info.format[info.format.length - 1])
}
return yt_stream(final[0].url)
}

5
play-dl/index.ts Normal file
View File

@ -0,0 +1,5 @@
//This File is in testing stage, everything will change in this
import { playlist_info, video_basic_info, video_info, search, stream } from "./YouTube";
export let youtube = { playlist_info, video_basic_info, video_info, search, stream }

View File

@ -18,6 +18,6 @@
"skipLibCheck": true,
"resolveJsonModule": true
},
"include": ["node-youtube-dl/**/**/*.ts"],
"exclude": ["node-youtube-dl/**/__tests__"]
"include": ["play-dl/**/**/*.ts"],
"exclude": ["play-dl/**/__tests__"]
}