import { isArray } from "jquery" ; class NZBUnity { public _debugMessages : string [ ] = [ ] ; public _debugMessagesMax : number = 1000 ; public optionsTab : chrome . tabs . Tab ; public nzbHost : NZBHost ; private refreshTimer : any ; private interceptExclude : string ; private newznabDetect : boolean ; private newznabProviders : string ; constructor ( ) { this . initOptions ( ) . then ( ( ) => { this . debug ( '[NZBUnity.constructor] Options Ok' ) ; return this . setActiveProfile ( ) ; } ) . then ( ( ) => { this . debug ( '[NZBUnity.constructor] Active profile connected' ) ; chrome . runtime . onMessage . addListener ( this . handleMessage . bind ( this ) ) ; this . debug ( '[NZBUnity.constructor] Message handler initialized' ) ; chrome . commands . onCommand . addListener ( this . handleCommand . bind ( this ) ) ; this . debug ( '[NZBUnity.constructor] Command handler initialized' ) ; chrome . storage . onChanged . addListener ( this . handleStorageChanged . bind ( this ) ) ; return Util . storage . get ( [ 'InterceptDownloads' , 'InterceptExclude' , 'EnableNewznab' , 'ProviderNewznab' ] ) ; } ) . then ( ( opts ) => { if ( opts . InterceptDownloads ) { this . enableIntercept ( ) ; this . interceptExclude = opts . InterceptExclude ; } this . newznabDetect = opts . EnableNewznab ; this . newznabProviders = opts . ProviderNewznab ; this . enableNewznab ( ) ; } ) . then ( ( ) => { this . debug ( '[NZBUnity.constructor] Init Done, starting!' ) ; this . refresh ( ) ; } ) ; } getProviders ( ) { return Util . storage . get ( [ 'Providers' ] ) . then ( opts => opts . Providers ) ; } async getProviderMatches ( ) { const providers = await this . getProviders ( ) ; const matches = Object . values ( providers ) . map ( p => p . Matches ) ; return [ ] . concat ( ... matches ) ; } async getProviderMatchRegex ( ) { const matches = await this . getProviderMatches ( ) ; return matches . map ( m => m . replace ( '//*.' , '//*' ) . replace ( /\*/g , '.*' ) ) . map ( m => new RegExp ( `^${ m }$` ) ) ; } async isProvider ( url ) { const matches = await this . getProviderMatchRegex ( ) ; return matches . some ( re => re . test ( url ) ) ; } showNotification ( id , title , message ) { Util . storage . get ( 'EnableNotifications' ) . then ( ( opts ) => { if ( opts . EnableNotifications ) { chrome . notifications . create ( `nzbunity.${ id }` , { 'type' : 'basic' , 'iconUrl' : chrome . extension . getURL ( 'content/images/icon-32.png' ) , 'title' : `NZB Unity - ${ title }` , 'message' : message , } ) ; } } ) ; } sendMessage ( name , data = null ) { return Util . sendMessage ( { [ `main.${ name }` ] : data } ) ; } sendTabMessage ( tab , name , data ) { return tab ? Util . sendTabMessage ( tab . id , { [ `main.${ name }` ] : data } ) . catch ( ( err ) => { } ) : Promise . reject ( 'Tab is required' ) ; } sendOptionsMessage ( name , data ) { return this . sendTabMessage ( this . optionsTab , name , data ) ; } startTimer ( ) { this . stopTimer ( ) ; Util . storage . get ( 'RefreshRate' ) . then ( ( opts ) => { this . refreshTimer = setInterval ( this . getQueue . bind ( this ) , opts . RefreshRate * 1000 ) ; } ) ; } stopTimer ( ) { if ( this . refreshTimer ) { clearInterval ( this . refreshTimer ) ; } } refresh ( ) { return this . getQueue ( ) . then ( ( res ) => { this . startTimer ( ) ; return res ; } ) ; } getQueue ( ) { if ( ! this . nzbHost ) { return Promise . resolve ( null ) ; } return this . nzbHost . getQueue ( ) . then ( ( result ) => { Util . setMenuIcon ( result . status , result . status ) ; this . sendMessage ( 'refresh' , result ) ; return result ; } ) . catch ( ( err ) => { console . warn ( 'Could not retrieve Queue from host, please check settings.' ) ; return { status : 'Disconnected' , speed : '0' , speedBytes : 0 , maxSpeed : '0' , maxSpeedBytes : 0 , sizeRemaining : '0' , timeRemaining : '0' , categories : [ ] , queue : [ ] , } ; } ) ; } async normalizeCategory ( category ) { const opts = await Util . storage . get ( [ 'DefaultCategory' , 'IgnoreCategories' , 'OverrideCategory' , 'SimplifyCategories' ] ) ; if ( opts . IgnoreCategories ) { return null ; } else if ( opts . OverrideCategory ) { return opts . OverrideCategory ; } else if ( category && opts . SimplifyCategories ) { return Util . simplifyCategory ( category ) ; } else if ( ! category && opts . DefaultCategory ) { return opts . DefaultCategory ; } else { return category ; } } async addUrl ( url , options = { } ) { if ( ! this . nzbHost ) return null ; const category = await this . normalizeCategory ( options . category ) ; this . debug ( '[NZBUnity.addUrl]' , { ... options , category : `${ options . category } &rarr; ${ category }` } ) ; if ( category ) { options . category = category ; } else { delete options . category ; } const result = await this . nzbHost . addUrl ( url , options ) ; this . sendMessage ( 'addUrl' , result ) ; this . showNotification ( 'addUrl' , `${ options . name || url } Added` , `${ options . name || url } sucessfully added to ${ this . nzbHost . displayName } (${ this . nzbHost . name })` ) ; return result ; } async addFile ( filename , content , options = { } ) { if ( ! this . nzbHost ) return null ; const category = await this . normalizeCategory ( options . category ) ; this . debug ( '[NZBUnity.addFile]' , { ... options , filename , category : `${ options . category } &rarr; ${ category }` } ) ; if ( category ) { options . category = category ; } else { delete options . category ; } const result = await this . nzbHost . addFile ( filename , content , options ) ; this . sendMessage ( 'addUrl' , result ) ; this . showNotification ( 'addUrl' , `${ filename } Added` , `${ filename } sucessfully uploaded to ${ this . nzbHost . displayName } (${ this . nzbHost . name })` ) ; return result ; } handleMessage ( message , sender , sendResponse ) { this . debugMessage ( message ) ; for ( const k in message ) { const val = message [ k ] ; switch ( k ) { case 'util.request' : this . debug ( `[NZBUnity.handleMessage] util.request: ${ val }` ) ; sendResponse ( undefined ) ; break ; case 'newznab.enable' : Util . storage . get ( [ 'ProviderNewznab' ] ) . then ( ( opts ) => { let providers = opts . ProviderNewznab . split ( /\s*,\s*/ ) . filter ( ( i ) => { return i ; } ) ; providers . push ( val ) ; return Util . storage . set ( { ProviderNewznab : providers . join ( ',' ) , } ) ; } ) . then ( ( ) => { this . debug ( `[NZBUnity.handleMessage] ${ val } added to Newznab providers` ) ; sendResponse ( true ) ; } ) ; break ; case 'content.addUrl' : this . addUrl ( val . url , val ) . then ( res => sendResponse ( res && res . success ) ) ; break ; case 'content.addFile' : this . addFile ( val . filename , val . content , val ) . then ( res => sendResponse ( res && res . success ) ) ; break ; case 'popup.profileSelect' : this . setActiveProfile ( val ) ; sendResponse ( undefined ) ; break ; case 'popup.resumeQueue' : if ( this . nzbHost ) { this . nzbHost . resumeQueue ( ) . then ( ( ) => { this . refresh ( ) ; sendResponse ( undefined ) ; } ) ; } break ; case 'popup.pauseQueue' : if ( this . nzbHost ) { this . nzbHost . pauseQueue ( ) . then ( ( ) => { this . refresh ( ) ; sendResponse ( undefined ) ; } ) ; } break ; case 'popup.setMaxSpeed' : if ( this . nzbHost ) { this . nzbHost . setMaxSpeed ( val ) . then ( ( ) => { this . refresh ( ) ; sendResponse ( undefined ) ; } ) ; } break ; case 'popup.refresh' : this . refresh ( ) ; sendResponse ( undefined ) ; break ; case 'popup.debug' : this . sendMessage ( 'debug' , this . _debugMessages ) ; sendResponse ( undefined ) ; break ; case 'popup.openProfilePage' : this . openProfilePage ( ) ; break ; case 'options.onTab' : this . optionsTab = val ; this . sendOptionsMessage ( 'onTab' , true ) . then ( res => sendResponse ( res && res . success ) ) ; break ; case 'options.setOptions' : if ( this . isValidOpt ( Object . keys ( val ) ) ) { Util . storage . set ( val ) ; } else { this . error ( 'items contain invalid option names' ) ; } sendResponse ( undefined ) ; break ; case 'options.resetOptions' : this . resetOptions ( ) . then ( ( ) => { this . sendOptionsMessage ( 'resetOptions' , true ) . then ( res => sendResponse ( res && res . success ) ) ; } ) ; break ; case 'options.profileNameChanged' : Util . storage . get ( 'ActiveProfile' ) . then ( ( opts ) => { if ( val . old = opts . ActiveProfile ) { this . setActiveProfile ( val . new ) ; } sendResponse ( undefined ) ; } ) ; break ; case 'options.profilesSaved' : this . setActiveProfile ( ) ; sendResponse ( undefined ) ; break ; case 'options.profileTest' : this . sendOptionsMessage ( 'profileTestStart' , true ) ; this . profileTest ( val ) . then ( r => this . sendOptionsMessage ( 'profileTestEnd' , r ) ) . catch ( error => this . sendOptionsMessage ( 'profileTestEnd' , { success : false , error } ) ) ; sendResponse ( undefined ) ; break ; } } return true ; } handleCommand ( command ) { this . debugCommand ( command ) ; switch ( String ( command ) ) { case 'resume-queue' : if ( this . nzbHost ) { this . nzbHost . resumeQueue ( ) . then ( ( ) => this . refresh ( ) ) ; } break ; case 'pause-queue' : if ( this . nzbHost ) { this . nzbHost . pauseQueue ( ) . then ( ( ) => this . refresh ( ) ) ; } break ; case 'toggle-queue' : if ( this . nzbHost ) { this . nzbHost . getQueue ( ) . then ( ( queue ) => { return queue . status . toLowerCase ( ) === 'paused' ? this . nzbHost . resumeQueue ( ) : this . nzbHost . pauseQueue ( ) ; } ) . then ( r => this . refresh ( ) ) ; } break ; case 'refresh' : this . refresh ( ) ; break ; case 'open-profile-page' : this . openProfilePage ( ) ; break ; } } handleStorageChanged ( changes , area ) { this . debug ( '[OptionsPage.handleStorageChanged] ' , Object . keys ( changes ) . map ( k => `${ k } -> ${ changes [ k ] . newValue }` ) . join ( ', ' ) ) ; if ( changes [ 'InterceptDownloads' ] ) { if ( changes [ 'InterceptDownloads' ] . newValue ) { this . enableIntercept ( ) ; return Util . storage . get ( [ 'InterceptExclude' ] ) . then ( ( opts ) => { this . interceptExclude = opts . InterceptExclude ; } ) ; } else { this . disableIntercept ( ) ; } } if ( changes [ 'InterceptExclude' ] ) { this . interceptExclude = changes [ 'InterceptExclude' ] . newValue ; } if ( changes [ 'ProviderNewznab' ] ) { this . newznabProviders = changes [ 'ProviderNewznab' ] . newValue ; } if ( changes [ 'Debug' ] ) { if ( ! changes [ 'Debug' ] . newValue ) { this . _debugMessages . splice ( 0 ) ; } } } isValidOpt ( opt ) { opt = Array . isArray ( opt ) ? opt : [ opt ] ; return opt . every ( k => Object . keys ( DefaultOptions ) . indexOf ( k ) >= 0 ) ; } resetOptions ( ) { return Util . storage . clear ( ) . then ( ( ) => this . initOptions ( ) ) ; } setOptionDefaults ( ) { return Util . storage . set ( DefaultOptions ) ; } initOptions ( ) { return Util . storage . get ( null ) . then ( ( opts ) => { if ( opts . Initialized ) { for ( let k in DefaultOptions ) { if ( opts [ k ] === undefined ) { Util . storage . set ( { [ k ] : DefaultOptions [ k ] } ) ; } } } else { return this . setOptionDefaults ( ) . then ( ( ) => Util . storage . set ( { Initialized : true } ) ) ; } } ) . then ( ( ) => { this . initProviders ( ) ; } ) ; } initProviders ( ) { return Util . storage . get ( 'Providers' ) . then ( ( opts ) => { let providers = { } ; chrome . runtime . getManifest ( ) . content_scripts . forEach ( ( i ) => { const js = i . js && i . js . pop ( ) ; const [ match , name ] = js && js . match ( /(\w+)\.js$/ ) ; if ( name && name !== 'util' ) { providers [ name ] = { Enabled : DefaultOptions . ProviderEnabled , Matches : i . matches , Js : i . js } ; if ( opts . Providers [ name ] ) { providers [ name ] . Enabled = opts . Providers [ name ] . Enabled ; } } } ) ; return Util . storage . set ( { Providers : providers } ) . then ( ( ) => providers ) ; } ) ; } openProfilePage ( ) { return this . getActiveProfile ( ) . then ( ( profile ) => { if ( profile ) { let url ; if ( profile . ProfileServerUrl ) { url = profile . ProfileServerUrl ; } else if ( profile . ProfileHostAsEntered ) { url = profile . ProfileHost . replace ( /\/?(api|jsonrpc)$/ig , '' ) ; } else { url = profile . ProfileHost ; } if ( ! /^[a-z]+:\/\//i . test ( url ) ) { url = `http://${ url }` ; } chrome . tabs . create ( { url } ) ; } } ) ; } enableIntercept ( ) { chrome . webRequest . onHeadersReceived . addListener ( this . handleHeadersReceived . bind ( this ) , { urls : [ "<all_urls>" ] , types : [ "main_frame" , "sub_frame" ] } , [ "responseHeaders" , "blocking" ] ) ; this . debug ( '[NZBUnity] NZB download intercept enabled' ) ; } disableIntercept ( ) { chrome . webRequest . onHeadersReceived . removeListener ( this . handleHeadersReceived . bind ( this ) ) ; this . debug ( '[NZBUnity] NZB download intercept disabled' ) ; } async isInterceptAllowed ( url ) { if ( ! this . interceptExclude || ! url ) return true ; const isProvider = await this . isProvider ( url ) ; if ( isProvider ) return false ; return this . interceptExclude . split ( /\s*,\s*/ ) . map ( host => new RegExp ( host ) ) . every ( hostRe => ! hostRe . test ( Util . parseUrl ( url ) . host ) ) ; } async handleHeadersReceived ( details ) { const url = details . url ; let type ; let disposition ; let dnzb = { } ; details . responseHeaders . forEach ( ( h ) => { let k = h . name . toLowerCase ( ) ; if ( k === 'content-type' ) { type = h . value . split ( /;\s*/ ) [ 0 ] . toLowerCase ( ) ; } else if ( k === 'content-disposition' ) { disposition = h . value ; } else if ( k . startsWith ( 'x-dnzb' ) ) { dnzb [ k . replace ( 'x-dnzb-' , '' ) ] = h . value ; } } ) ; const allowed = await this . isInterceptAllowed ( url ) ; let dispositionMatch = disposition && disposition . match ( /^attachment;\s*filename="?(.*(\.nzb))"?$/i ) ; if ( allowed && ( details . method == 'GET' ) && ( type === 'application/x-nzb' || dispositionMatch ) ) { const options = { } ; if ( dnzb . name ) options . name = dnzb . name ; else if ( dispositionMatch ) options . name = dispositionMatch [ 1 ] ; if ( dnzb . category ) options . category = dnzb . category ; this . addUrl ( url , options ) . then ( ( r ) => { console . info ( `[NZBUnity] NZB intercepted, ${ r . success ? 'Success' : 'Failure' }
            ${ url }
            ${ options . name || '' }
            ${ ! r . success ? 'Error: ' + r . error : '' }` ) ; } ) ; return { cancel : true } ; } else { return ; } } enableNewznab ( ) { chrome . tabs . onUpdated . addListener ( this . handleNewznabTabUpdated . bind ( this ) ) ; this . debug ( '[NZBUnity] Newznab detection enabled' ) ; } disableNewznab ( ) { chrome . tabs . onUpdated . removeListener ( this . handleNewznabTabUpdated . bind ( this ) ) ; this . debug ( '[NZBUnity] Newznab detection disabled' ) ; } isNewznabProvider ( url ) { if ( ! this . newznabProviders || ! url ) return false ; return this . newznabProviders . split ( /\s*,\s*/ ) . map ( host => new RegExp ( host ) ) . some ( hostRe => hostRe . test ( Util . parseUrl ( url ) . host ) ) ; } async handleNewznabTabUpdated ( tabId , changes , tab ) { if ( tab . status === 'complete' && /^https?:/ . test ( tab . url ) ) { const isProvider = await this . isProvider ( tab . url ) ; if ( isProvider ) return ; if ( this . isNewznabProvider ( tab . url ) ) { chrome . tabs . executeScript ( tabId , { file : 'vendor/jquery-3.3.1.slim.min.js' } , ( ) => { chrome . tabs . executeScript ( tabId , { file : 'background/util.js' } , ( ) => { chrome . tabs . executeScript ( tabId , { file : 'content/sites/newznab.js' } ) ; } ) ; } ) ; } else if ( this . newznabDetect ) { chrome . tabs . executeScript ( tabId , { file : 'content/sites/newznab-detect.js' } ) ; } } } profileTest ( name ) { return this . nzbHost ? this . nzbHost . test ( ) : Promise . reject ( { success : false , error : 'No connection to host' } ) ; } setActiveProfile ( name = null ) { let profiles ; let profileNames ; return Util . storage . get ( [ 'ActiveProfile' , 'Profiles' ] ) . then ( ( opts ) => { profiles = opts . Profiles ; profileNames = Object . keys ( profiles ) ; if ( ! profileNames . length ) { return Util . storage . set ( { ActiveProfile : DefaultOptions . ActiveProfile } ) ; } if ( ! name || ! profileNames . includes ( name ) ) { name = profileNames . includes ( opts . ActiveProfile ) ? opts . ActiveProfile : profileNames [ 0 ] ; } return Util . storage . set ( { ActiveProfile : name } ) ; } ) . then ( ( ) => { const profile = profiles [ name ] ; if ( profile ) { if ( profile . ProfileType === 'NZBGet' ) { this . nzbHost = new NZBGetHost ( { displayName : name , host : profile . ProfileHost , username : profile . ProfileUsername , password : profile . ProfilePassword , hostAsEntered : profile . ProfileHostAsEntered } as Dictionary ) ; } else { this . nzbHost = new SABnzbdHost ( { displayName : name , host : profile . ProfileHost , apikey : profile . ProfileApiKey , hostAsEntered : profile . ProfileHostAsEntered } as Dictionary ) ; } return { success : true , result : name } ; } else { return { success : false , error : 'Profile not found' } ; } } ) ; } getActiveProfile ( ) { return Util . storage . get ( [ 'ActiveProfile' , 'Profiles' ] ) . then ( opts => opts . Profiles [ opts . ActiveProfile ] ) ; } enableDebug ( debug = true ) { Util . storage . set ( { Debug : debug } ) . then ( ( ) => { this . debugOpts ( ) ; } ) ; } error ( ... args ) { console . error . apply ( this , args ) ; } debug ( ... args ) { Util . storage . get ( 'Debug' ) . then ( ( opts ) => { if ( opts . Debug ) { const msg = args . map ( ( arg ) => { switch ( typeof arg ) { case 'object' : return Object . keys ( arg ) . map ( k => `&nbsp;&nbsp;&nbsp;<span class="green">${ k }</span>: ${ arg [ k ] }` ) . join ( '\n' ) ; default : return `${ arg }` . replace ( /^\[(.*)\]/ , '<span class="green">[$1]</span>' ) ; } } ) . join ( '\n' ) ; this . _debugMessages . push ( msg ) ; while ( this . _debugMessages . length > this . _debugMessagesMax ) { this . _debugMessages . shift ( ) ; } this . sendMessage ( 'debug' , this . _debugMessages ) ; console . debug . apply ( this , args ) ; } } ) ; } debugOpts ( opts = null ) { Util . storage . get ( opts ) . then ( ( items ) => { this . debug ( '[NZBUnity.debugOpts]' , items ) ; } ) ; } debugMessage ( message ) { Util . storage . get ( 'Debug' ) . then ( ( opts ) => { if ( opts . Debug ) { for ( let k in message ) { console . debug ( '[NZBUnity.debugMessage]' , k , message [ k ] ) ; } } } ) ; } debugCommand ( command ) { Util . storage . get ( 'Debug' ) . then ( ( opts ) => { if ( opts . Debug ) { console . debug ( '[NZBUnity.debugCommand]' , command ) ; } } ) ; } } let nzbUnity = new NZBUnity ( ) ;