/**
 * Konichiwa~
 *
 * This is responsible for the Download All core functionality.
 * Chrome does not allow content scripts to download, that's why
 * the functionality had to be split.
 *
 * IMPORTANT:
 * 9ac **DOES NOT** support queues for download. So if you are
 * trying to download from multiple tabs, chances are your
 * downloads might not go as expected. Please wait till one set
 * of download is over before starting again.
 */ import { DownloadMethod , DownloadQuality , IEpisode , Intent , IRuntimeMessage , Server , } from "../common" ; import * as utils from "../utils" ; import * as api from "./api" ; import { IFile } from "./api" ; let ts = "" ; let animeName = "" ; let sender ; let aggregateLinks = "" ; let selectedEpisodes = [ ] ; let dlEpisode ; let server = Server . Default ; let serverId = 22 ; let quality = DownloadQuality [ "360p" ] ; let method = DownloadMethod . Browser ; interface ISetupOptions { animeName : string ; method : DownloadMethod ; quality : DownloadQuality ; selectedEpisodes : IEpisode [ ] ; sender : chrome . runtime . MessageSender ; server : Server ; serverId : number ; ts : string ; } export function setup ( options ) { aggregateLinks = "" ; animeName = options . animeName ; method = options . method ; quality = options . quality ; selectedEpisodes = options . selectedEpisodes ; sender = options . sender ; server = options . server ; serverId = options . serverId ; ts = options . ts ; } const decryptTs = ( str ) => { let result = "" ; const firstCharMap = [ ] ; const secondCharMap = [ ] ; for ( let n = 65 ; n < 91 ; n ++ ) { firstCharMap . push ( String . fromCharCode ( n ) ) ; if ( n % 2 !== 0 ) { secondCharMap . push ( String . fromCharCode ( n ) ) ; } } for ( let n = 65 ; n < 91 ; n ++ ) { if ( n % 2 === 0 ) { secondCharMap . push ( String . fromCharCode ( n ) ) ; } } for ( let i = 0 ; i < str . length ; i ++ ) { let charReplaced = false ; for ( let y = 0 ; y < secondCharMap . length ; y ++ ) { if ( str [ i ] === secondCharMap [ y ] ) { result += firstCharMap [ y ] ; charReplaced = true ; break ; } } if ( ! charReplaced ) { result += str [ i ] ; } } return atob ( result ) ; } ; function decryptTokenAndOptions ( str ) { const cleanStr = str . replace ( /^-/ , "" ) ; const firstCharMap = [ ] ; const secondCharMap = [ ] ; for ( let j = 97 ; j <= 122 ; j ++ ) { firstCharMap . push ( String . fromCharCode ( j ) ) ; if ( j % 2 ) { secondCharMap . push ( String . fromCharCode ( j ) ) ; } } for ( let k = 97 ; k <= 122 ; k ++ ) { if ( k % 2 === 0 ) { secondCharMap . push ( String . fromCharCode ( k ) ) ; } } let result = "" ; for ( let e = 0 ; e < cleanStr . length ; e ++ ) { let replaced = false ; for ( let f = 0 ; f < secondCharMap . length ; f ++ ) { if ( cleanStr [ e ] === secondCharMap [ f ] ) { result += firstCharMap [ f ] ; replaced = true ; break ; } } if ( ! replaced ) { result += cleanStr [ e ] ; } } return atob ( result ) ; } /**
 * Send messages to the tab/content script.
 */ function sendMessage ( message ) { if ( sender . tab && sender . tab . id ) { chrome . tabs . sendMessage ( sender . tab . id , message ) ; } } see sendMessage function status ( message ) { sendMessage ( { intent : Intent . Download_Status , status : message , } ) ; } param file param episode param ext returns export function fileName ( file , episode , ext = true ) { if ( ext ) { return utils . fileSafeString ( `${ animeName }_E${ episode . num }_${ file . label }` ) + `.${ file . type }` ; } else { return utils . fileSafeString ( `${ animeName }_E${ episode . num }_${ file . label }` ) ; } } param pref param files returns see https for the unit tests.
  export function autoFallback ( pref , files ) { for ( let i = pref ; i >= DownloadQuality [ "360p" ] ; i -- ) { for ( let file of files ) { if ( file . label === DownloadQuality [ i ] ) { return file ; } } } return null ; } /**
 * This function requeue's the downloader to run every
 * 2 seconds to avoid overloading the 9anime API and/or
 * getting our IP flagged as bot.
 */ function requeue ( ) { if ( selectedEpisodes . length > 0 ) { setTimeout ( downloader , 2000 ) ; } else { status ( "All done!" ) ; if ( method === DownloadMethod . Browser ) { sendMessage ( { intent : Intent . Download_Complete , } ) ; } else { sendMessage ( { intent : Intent . Download_Complete , links : aggregateLinks , } ) ; } } } function startDownload ( file ) { switch ( method ) { case DownloadMethod . External : aggregateLinks += `${ file . file }?title=${ fileName ( file , dlEpisode , false ) }&type=${ file . type }\n` ; status ( `✔ Completed ${ dlEpisode . num }` ) ; break ; default : chrome . downloads . download ( { conflictAction : "uniquify" , filename : "9anime Companion/" + fileName ( file , dlEpisode ) , url : file . file , } ) ; status ( `✔ Completed ${ dlEpisode . num }` ) ; break ; } } function getLinks9a ( data ) { api . links9a ( data . grabber , { id : data . params . id , mobile : 0 , options : decryptTokenAndOptions ( data . params . options ) , token : decryptTokenAndOptions ( data . params . token ) , ts : decryptTs ( ts ) , } ) . then ( resp => { let file = autoFallback ( quality , resp . data ) ; if ( ! file ) { status ( `❌ Failed ${ dlEpisode . num }. No fallback quality found. Use a higher preferred quality.` ) ; return ; } startDownload ( file ) ; } ) . catch ( err => { console . debug ( err ) ; status ( `❌ Failed ${ dlEpisode . num }` ) ; } ) . then ( ( ) => { requeue ( ) ; } ) ; } param { string } source function rvParseEpisodeDetails ( source ) { let episodeDetails = { file : "" , label : "" , type : "" , } ; source . split ( " " ) . forEach ( ( el ) => { let rvEpData = el . split ( "=" ) ; if ( rvEpData . length === 2 ) { let value = rvEpData [ 1 ] . replace ( /["']/g , "" ) ; switch ( rvEpData [ 0 ] ) { case "src" : episodeDetails . file = value ; break ; case "type" : episodeDetails . type = value . replace ( "video/" , "" ) ; break ; case "title" : episodeDetails . label = value ; break ; default : break ; } } } ) ; if ( episodeDetails . file === "" || episodeDetails . label === "" || episodeDetails . type === "" ) { return null ; } else { return episodeDetails ; } } export function getLinksRV ( data ) { const rvSourcesRegex = /<source(.*)\/>/i ; const endpoint = data . target + `?q=${ DownloadQuality [ quality ] }` ; fetch ( endpoint ) . then ( response => { if ( response . ok ) { return response . text ( ) ; } throw new Error ( response . status . toString ( ) ) ; } ) . then ( resp => { let matched = resp . match ( rvSourcesRegex ) ; if ( matched ) { let rvEpisodeDetails = rvParseEpisodeDetails ( matched [ 0 ] ) ; if ( rvEpisodeDetails && rvEpisodeDetails . label === DownloadQuality [ quality ] ) { startDownload ( rvEpisodeDetails ) ; } else { status ( `❌ Failed ${ dlEpisode . num }. Preferred quality not found. Try changing the quality.` ) ; return ; } } else { status ( `❌ Failed ${ dlEpisode . num }. Preferred quality not found. Try changing the quality.` ) ; return ; } } ) . catch ( err => { console . debug ( err ) ; status ( `❌ Failed ${ dlEpisode . num }` ) ; } ) . then ( ( ) => { requeue ( ) ; } ) ; } /**
 * The boss function. It handles the entire downloading
 * process.
 */ export function downloader ( ) { let ep = selectedEpisodes . shift ( ) ; if ( ep ) { dlEpisode = ep ; status ( `Downloading ${ ep . num }` ) ; api . grabber ( { id : ep . id , server : serverId , ts : decryptTs ( ts ) , update : 0 , } ) . then ( resp => { switch ( server ) { case Server . RapidVideo : getLinksRV ( resp ) ; break ; default : getLinks9a ( resp ) ; break ; } } ) . catch ( err => { status ( `❌ Failed ${ ( ep as IEpisode ) . num }` ) ; console . debug ( err ) ; requeue ( ) ; } ) ; } } param baseUrl param setupOptions export function start ( baseUrl , setupOptions ) { api . setup ( { baseUrl , } ) ; setup ( setupOptions ) ; downloader ( ) ; sendMessage ( { intent : Intent . Show_Notification , message : "Sit tight!" , title : "Starting downloads" , } ) ; }