import { MultiProgressBars , UpdateOptions } from 'multi-progress-bars' ; import Video from './lib/Video' ; import { settings , args } from './lib/helpers' ; type promiseFunction = ( f : Promise < void > ) => void ; const reset = '\u001b[0m' ; const cy = ( str ) => `\u001b[36;1m${ str }\u001b[0m` ; const gr = ( str ) => `\u001b[32;1m${ str }\u001b[0m` ; const ye = ( str ) => `\u001b[33;1m${ str }\u001b[0m` ; const bl = ( str ) => `\u001b[34;1m${ str }\u001b[0m` ; export default class Downloader { private mpb ? : MultiProgressBars ; private videoQueue : Array < { video : Video ; res : promiseFunction ; } > = [ ] ; private videosProcessing : number = 0 ; private videosProcessed : number = 0 ; private summaryStats : { [ key ] : { totalMB : number ; downloadedMB : number ; downloadSpeed : number ; } ; } = { } ; private runQueue : boolean = false ; start ( ) { if ( this . runQueue === false ) { this . runQueue = true ; this . tickQueue ( ) ; } } private tickQueue ( ) { if ( this . runQueue === false ) return ; while ( this . videoQueue . length > 0 && ( settings . downloadThreads === - 1 || this . videosProcessing < settings . downloadThreads ) ) { this . videosProcessing ++ ; const task = this . videoQueue . pop ( ) ; if ( task !== undefined ) { const processingVideoPromise = this . processVideo ( task . video ) ; task . res ( processingVideoPromise ) ; processingVideoPromise . then ( ( ) => this . videosProcessing -- && this . videosProcessed ++ ) ; } } setTimeout ( ( ) => this . tickQueue ( ) , 50 ) ; } stop ( ) { this . runQueue = false ; } processVideos ( videos ) { if ( videos . length !== 0 ) { console . log ( `> Processing ${ videos . length } videos...` ) ; if ( args . headless !== true ) this . mpb = new MultiProgressBars ( { initMessage : '' , anchor : 'top' } ) ; this . summaryStats = { } ; this . videosProcessed = 0 ; } const processingPromises = videos . reverse ( ) . map ( ( video ) => new Promise < void > ( ( res ) => this . videoQueue . push ( { video , res } ) ) ) ; if ( videos . length !== 0 ) Promise . all ( processingPromises ) . then ( ( ) => this . updateSummaryBar ( ) ) ; return processingPromises ; } private updateSummaryBar ( ) { if ( this . summaryStats === undefined ) return ; const { totalMB , downloadedMB , downloadSpeed } = Object . values ( this . summaryStats ) . reduce ( ( summary , stats ) => { for ( const key in stats ) { summary [ key as keyof typeof stats ] += stats [ key as keyof typeof stats ] ; } return summary ; } , { totalMB : 0 , downloadedMB : 0 , downloadSpeed : 0 } ) ; const totalVideos = this . videoQueue . length + this . videosProcessed + this . videosProcessing ; const whitespace = '                        ' ; const processed = `Processed:        ${ ye ( this . videosProcessed ) }/${ ye ( totalVideos ) }${ whitespace }` ; const downloaded = `Total Downloaded: ${ cy ( downloadedMB . toFixed ( 2 ) ) }/${ cy ( totalMB . toFixed ( 2 ) + 'MB' ) }${ whitespace }` ; const speed = `Download Speed:   ${ gr ( ( ( downloadSpeed / 1024000 ) * 8 ) . toFixed ( 2 ) + 'Mb/s' ) }${ whitespace }` ; process . stdout . write ( '                                                         ' ) ; process . stdout . write ( `\n${ processed }\n${ downloaded }\n${ speed }\n\n\n` ) ; } param formattedTitle param barUpdate param displayNow private updateBar ( formattedTitle , barUpdate , displayNow = false ) { if ( args . headless === true ) { if ( displayNow === true && barUpdate . message !== undefined ) console . log ( `${ formattedTitle } - ${ barUpdate . message }` ) ; } else if ( this . mpb !== undefined ) this . mpb . updateTask ( formattedTitle , barUpdate ) ; } private async processVideo ( video , retries = 0 , allowRangeQuery = true ) { let formattedTitle ; if ( args . headless === true ) formattedTitle = `${ video . channel . title } - ${ video . title }` ; else if ( video . channel . consoleColor !== undefined ) { formattedTitle = `${ video . channel . consoleColor }${ video . channel . title }${ reset } - ${ video . title }` . slice ( 0 , 32 + video . channel . consoleColor . length + reset . length ) ; } else formattedTitle = `${ video . channel . title } - ${ video . title }` . slice ( 0 , 32 ) ; if ( this . summaryStats !== undefined ) while ( formattedTitle in this . summaryStats ) formattedTitle = `.${ formattedTitle }` . slice ( 0 , 32 ) ; if ( args . headless === true ) console . log ( `${ formattedTitle } - Downloading...` ) ; else { if ( this . mpb === undefined ) throw new Error ( 'Progressbar failed to initialize! Cannot continue download' ) ; this . mpb . addTask ( formattedTitle , { type : 'percentage' , barTransformFn : ( str ) => `${ video . channel . consoleColor || '' }${ str }` , } ) ; } try { if ( ! ( await video . isMuxed ( ) ) && ! ( await video . isDownloaded ( ) ) ) { const startTime = Date . now ( ) ; const downloadRequests = await video . download ( settings . floatplane . videoResolution as string , allowRangeQuery ) ; const totalBytes = [ ] ; const downloadedBytes = [ ] ; const percentage = [ ] ; await Promise . all ( downloadRequests . map ( async ( request , i ) => { request . on ( 'downloadProgress' , ( downloadProgress ) => { const timeElapsed = ( Date . now ( ) - startTime ) / 1000 ; totalBytes [ i ] = downloadProgress . total ; downloadedBytes [ i ] = downloadProgress . transferred ; percentage [ i ] = downloadProgress . percent ; const total = totalBytes . reduce ( ( sum , b ) => sum + b , 0 ) ; const transferred = downloadedBytes . reduce ( ( sum , b ) => sum + b , 0 ) ; const totalMB = total / 1024000 ; const downloadedMB = transferred / 1024000 ; const downloadSpeed = transferred / timeElapsed ; const downloadETA = total / downloadSpeed - timeElapsed ; this . updateBar ( formattedTitle , { percentage : percentage . reduce ( ( sum , b ) => sum + b , 0 ) / percentage . length , message : `${ reset }${ cy ( downloadedMB . toFixed ( 2 ) ) }/${ cy ( totalMB . toFixed ( 2 ) + 'MB' ) } ${ gr ( ( ( downloadSpeed / 1024000 ) * 8 ) . toFixed ( 2 ) + 'Mb/s' ) } ETA: ${ bl ( Math . floor ( downloadETA / 60 ) + 'm ' + ( Math . floor ( downloadETA ) % 60 ) + 's' ) }` , } ) ; this . summaryStats [ formattedTitle ] = { totalMB , downloadedMB , downloadSpeed } ; if ( args . headless !== true ) this . updateSummaryBar ( ) ; } ) ; await new Promise ( ( res , rej ) => { request . on ( 'end' , res ) ; request . on ( 'error' , rej ) ; } ) ; } ) ) ; this . summaryStats [ formattedTitle ] . downloadSpeed = 0 ; } if ( ! ( await video . isMuxed ( ) ) ) { this . updateBar ( formattedTitle , { percentage : 0.99 , message : 'Muxing ffmpeg metadata...' , } ) ; await video . muxffmpegMetadata ( ) ; } if ( settings . postProcessingCommand !== '' ) { this . updateBar ( formattedTitle , { message : `Running post download command "${ settings . postProcessingCommand }"...` } ) ; await video . postProcessingCommand ( ) . catch ( ( err ) => console . log ( `An error occurred while executing the postProcessingCommand!\n${ err . message }\n` ) ) ; } if ( args . headless === true ) { this . updateBar ( formattedTitle , { message : `Downloaded!` } ) ; this . updateSummaryBar ( ) ; } else if ( this . mpb !== undefined ) this . mpb . done ( formattedTitle ) ; } catch ( error ) { let info ; if ( ! ( error instanceof Error ) ) info = new Error ( `Something weird happened, whatever was thrown was not a error! ${ error }` ) ; else info = error ; if ( retries < settings . floatplane . retries ) { this . updateBar ( formattedTitle , { message : `\u001b[31m\u001b[1mERR\u001b[0m: ${ info . message } - Retrying ${ retries }/${ settings . floatplane . retries }` } , true ) ; if ( info . message . indexOf ( 'Range Not Satisfiable' ) ) await this . processVideo ( video , ++ retries , false ) ; else await this . processVideo ( video , ++ retries ) ; } else this . updateBar ( formattedTitle , { message : `\u001b[31m\u001b[1mERR\u001b[0m: ${ info . message } Max Retries! ${ retries }/${ settings . floatplane . retries }` } , true ) ; } } }