<?php

/**

* PBDown - Ponibooru download bot.

* @version 2.1

* @author Dekarrin

*

* Downloads pics from Ponibooru, with the capability to filter by content

* rating and/or to filter by tag addition/subtraction.

*

* Usage: php pbdown [-vqh] [-c <content-filter]> [-a <add-tags>]

* [-s <sub-tags>] [-b <begin-post>] [-e <end-post>] [-l <limit>]

* [-f <flood-delay>] [-t <truncation-length>] <path>

*

* ARGUMENTS:

*

* path - the location on disk to download the images to. Required.

*

*

* OPTIONS:

*

* v - Verbose mode. Outputs all captured image IDs and links to stdout.

*

* q - Quiet mode. Supresses all output. Overrides verbose mode.

*

* h - Help. Shows quick syntax help.

*

* c - Content preferences. Set this to a comma-separated list of values for

* the content settings on ponibooru. It can include any combination of the

* following items: safe, questionable, explicit, unrated. Defaults to

* "safe,questionable,unrated" if not set.

*

* a - Comma-separated list of tags to add to the search. Defaults to none when

* not set.

*

* s - Comma-separated list of tags to subtract from the search. Defaults to

* none when not set.

*

* b - The id of the most recent post to get. Set to 0 to start download at the

* most recent post possible. If both b and e are set to non-zero values, b

* must be greater than e. Defaults to 0.

*

* e - The id of the least recent post to get. Set to 0 to end download at the

* least recent post possible. If both b and e are set to non-zero values, e

* must be less than b. Defaults to 0.

*

* l - Upper limit of posts to get. Set to 0 for no limit. Defaults to 0.

*

* f - Flood-preventative delay time. Number of milliseconds between each

* request. Defaults to 500.

*

* t - Truncation length. The number of characters to truncate the downloaded

* images' file names to. Defaults to 150.

*/

define ( 'DEFAULT_FLOOD_CONTROL' , 500 ) ;

define ( 'DEFAULT_CONTENT_FILTER' , 'safe,questionable,unrated' ) ;

define ( 'DEFAULT_TRUNCATION_LENGTH' , 150 ) ;

/**

* Manages output quieting for verbose and silent mode.

*/

class OutputFilter {

/**

* Whether this OutputFilter is in Verbose mode.

*/

private $verbose ;

/**

* Whether this OutputFilter is in Quiet mode.

*/

private $quiet ;

/**

* Creates a new OutputFilter in normal output mode, with verbose and quiet

* mode both disabled.

*/

public function __construct ( ) {

$this -> verbose = false ;

$this -> quiet = false ;

}

/**

* Sets whether verbose mode is active.

*

* @param $v

* Whether verbose mode is to be set active.

*/

public function setVerbose ( $v ) {

$this -> verbose = $v ;

}

/**

* Sets whether quiet mode is active.

*

* @param $q

* Whether quiet mode is to be set active.

*/

public function setQuiet ( $q ) {

$this -> quiet = $q ;

}

/**

* Prints a message if this OutputFilter is not in quiet mode.

*

* @param $msg

* The message to display.

*/

public function output ( $msg ) {

if ( ! $this -> quiet ) {

echo $msg ;

}

}

/**

* Prints a message if this OutputFilter is in verbose mode and is not in

* quiet mode.

*

* @param $msg

* The message to display.

*/

public function verboseOutput ( $msg ) {

if ( $this -> verbose ) {

$this -> output ( $msg ) ;

}

}

}

/**

* Wrapper class for PHP cURL library. Makes HTTP requests.

*/

class HttpConnection {

/**

* The cURL resource.

*/

private $connection ;

/**

* Creates a new HttpConnection. The underlying cURL resource is inited.

*/

public function __construct ( ) {

$this -> connection = curl_init ( ) ;

}

/**

* Destroys this HttpConnection. The underlying cURL resource is released.

*/

public function __destruct ( ) {

curl_close ( $this -> connection ) ;

}

/**

* Makes an HTTP HEAD request. Only the headers from the response are

* returned.

*

* @param $url

* The URL to make the request to.

*

* @return The headers from the server's response as raw text.

*/

public function headRequest ( $url ) {

$this -> setOption ( CURLOPT_HEADER , true ) ;

$this -> setOption ( CURLOPT_NOBODY , true ) ;

return $this -> request ( $url ) ;

}

/**

* Makes an HTTP GET request.

*

* @param $url

* The URL to make the request to.

*

* @param $data

* The form data in the request. These will be converted to a query string

* and appended to $url for the actual request.

*

* @return

* The message body from the server's response as raw text.

*/

public function getRequest ( $url , $data = array ( ) ) {

$query = $this -> convertToFields ( $data ) ;

if ( $query !== "" ) {

$url .= "?" . $query ;

}

$this -> setOption ( CURLOPT_HTTPGET , true ) ;

return $this -> request ( $url ) ;

}

/**

* Makes an HTTP POST request.

*

* @param $url

* The URL to make the request to.

*

* @param $data

* The form data in the request. These will be sent as the POST message

* body.

*

* @return

* The message body from the server's response as raw text.

*/

public function postRequest ( $url , $data = array ( ) ) {

$fields = $this -> convertToFields ( $data ) ;

$this -> setOption ( CURLOPT_POST , count ( $data ) ) ;

$this -> setOption ( CURLOPT_POSTFIELDS , $fields ) ;

return $this -> request ( $url ) ;

}

/**

* Sets a cURL option on the underlying cURL resource.

*

* @param $option

* The option to set. cURL options in PHP are integer constants prepended

* with CURLOPT_.

*

* @param $value

* What to set the option to.

*/

protected function setOption ( $option , $value ) {

curl_setopt ( $this -> connection , $option , $value ) ;

}

/**

* Converts an associative array containing data into a single string of

* key-value pairs separated by ampersands.

*

* @param $data

* The data to be converted.

*

* @return

* A string appropriately formatted to be inserted into a query string or

* to be inserted into POST data.

*/

private function convertToFields ( $data ) {

$fields = "" ;

foreach ( $data as $k => $v ) {

$fields .= " $k = $v &" ;

}

$fields = rtrim ( $fields , '&' ) ;

return $fields ;

}

/**

* Makes the actual HTTP request through cURL.

*

* @param $url

* The URL to send the request to.

*

* @return

* The server's response as raw text.

*/

private function request ( $url ) {

$this -> setOption ( CURLOPT_URL , $url ) ;

$this -> setOption ( CURLOPT_RETURNTRANSFER , true ) ;

$this -> setOption ( CURLOPT_FOLLOWLOCATION , true ) ;

return curl_exec ( $this -> connection ) ;

}

}

/**

* Wrapper class for PHP cURL library. Makes HTTP requests and handles cookies

* properly in order to maintain a constant session.

*/

class HttpCookieConnection extends HttpConnection {

/**

* The session cookie.

*/

private $cookie ;

/**

* Whether subsequent calls to headRequest() refresh the session cookie.

*/

private $newSession ;

/**

* Creates a new HttpCookieConnection. The session cookie is set such that

* the next call to headRequest() will begin the session.

*/

public function __construct ( ) {

parent :: __construct ( ) ;

$this -> beginSession ( ) ;

}

/**

* Resets the session cookie such that the next call to headRequest()

* will begin a new session. Until the session is restarted with a call to

* headRequest(), the session will not be active.

*/

public function beginSession ( ) {

$this -> setOption ( CURLOPT_COOKIESESSION , true ) ;

$this -> cookie = "" ;

$this -> newSession = true ;

}

/**

* @inheritDoc

*/

public function getRequest ( $url , $data = array ( ) ) {

$this -> setCookie ( ) ;

return parent :: getRequest ( $url , $data ) ;

}

/**

* @inheritDoc

*/

public function postRequest ( $url , $data = array ( ) ) {

$this -> setCookie ( ) ;

return parent :: postRequest ( $url , $data ) ;

}

/**

* If the session was recently refreshed, sets the session cookie is set

* with data from the headers.

*

* @inheritDoc

*/

public function headRequest ( $url ) {

$this -> setCookie ( ) ;

$response = parent :: headRequest ( $url ) ;

if ( $this -> newSession ) {

$this -> processCookies ( $response ) ;

}

return $response ;

}

/**

* Sets the underlying cURL resource to send the session cookie on the next

* request.

*/

private function setCookie ( ) {

$this -> setOption ( CURLOPT_COOKIE , "PHPSESSID=" . $this -> cookie ) ;

}

/**

* Sets the session cookie from HTTP response headers.

*

* @param $response

* The response headers that conatain the 'Set-Cookie' header.

*/

private function processCookies ( $response ) {

$matches = array ( ) ;

preg_match ( "/Set-Cookie: PHPSESSID=(.+);/" , $response , $matches ) ;

$this -> newSession = false ;

$this -> cookie = $matches [ 1 ] ;

}

}

/**

* Provides ecstasy and joy for the user by downloading loads of lovely

* pictures of ponies from Ponibooru. Uses persistant HTTP connections to

* filter by content-rating, and uses URL modification to filter by tags.

*

* scanGallery() is used to find post IDs to add to the download list, and

* download() is then used to actually download the images with those IDs.

*

* scanGallery() can be called multiple times, in between which calls to

* setTagFilter() and setContentFilter() can modify the behavior of the gallery

* scan. Each time scanGallery() is called, it will add the found IDs to the

* download list. IDs already in the download list will not be re-added, which

* automatically handles duplicates.

*

* Once download() finishes, the download list is automatically cleared.

*/

class PBDownloader {

/**

* The addition to the gallery URL which will filter the results by tags.

*/

private $searchString ;

/**

* The amount of time to wait in microseconds between each request to

* Ponibooru.

*/

private $floodControlDelay ;

/**

* The total length of a downloaded file's filename.

*/

private $truncationLength ;

/**

* The list of IDs of images to download.

*/

private $downloadList ;

/**

* Handles all output with quiet and verbose output modes.

*/

private $outputFilter ;

/**

* The persistant connection to Ponibooru.

*/

private $connection ;

/**

* Content filter mask for a content rating of 'safe'.

*/

const CONTENT_SAFE = 1 ;

/**

* Content filter mask for a content rating of 'questionable'.

*/

const CONTENT_QUESTIONABLE = 2 ;

/**

* Content filter mask for a content rating of 'explicit'.

*/

const CONTENT_EXPLICIT = 4 ;

/**

* Content filter mask for a content rating of 'unrated'.

*/

const CONTENT_UNRATED = 8 ;

/**

* The URL of the Ponibooru server.

*/

const PB_HOST = "www.ponibooru.org" ;

/**

* The path to the destination of posted content filter forms.

*/

const PATH_CONTENT_FORM = "/filter/set" ;

/**

* The path to the individual posts' pages; append the ID of an image to

* get that specific post.

*/

const PATH_POST = "/post/view/" ;

/**

* The path to the gallery's pages; append a page number to get that

* specific gallery page.

*/

const PATH_GALLERY = "/post/list/" ;

/**

* Creates a new PBDownloader. A connection to Ponibooru is opened and then

* will be maintained for the lifetime of this PBDownloader.

*

* @param $floodControl

* The time in microseconds to wait between each request to Ponibooru.

*

* @param $truncationLength

* The maximum length of a downloaded file's filename, beyond which it is

* truncated.

*

* @param $outputFilter

* An OutputFilter to handle this PBDownloader's output.

*/

public function __construct ( $floodControl , $truncationLength , $outputFilter ) {

$this -> floodControlDelay = $floodControl ;

$this -> downloadList = array ( ) ;

$this -> truncationLength = $truncationLength ;

$this -> outputFilter = $outputFilter ;

$this -> beginSession ( ) ;

}

/**

* Destroys this PBDownloader and closes the connection to Ponibooru.

*/

public function __destruct ( ) {

unset ( $this -> connection ) ;

}

/**

* Scans the gallery for posts and adds applicable IDs to the download

* list, if and only if they aren't already in it.

*

* @param $newestId

* The most recent ID to add to the download list. If this is 0, then the

* most recent ID found in the gallery is used.

*

* @param $oldestId

* The least recent ID to add to the download list. If this is 0, then the

* least recent ID found in the gallery is used. This parameter will only

* be used up to the limit if one is specified.

*

* @param $postLimit

* The maximum number of posts to scan. This will be followed regardless of

* the value of $oldestId; even if $oldestId's value would have this scan

* add more IDs than $postLimit specifies, the scan stops once $postLimit

* is reached.

*/

public function scanGallery ( $newestId , $oldestId , $postLimit ) {

$downloads = array ( ) ;

$galleryUrl = self :: PB_HOST . self :: PATH_GALLERY . $this -> searchString ;

$galleryPageNum = 0 ;

$galleryMaxPage = - 1 ;

$lastId ;

$newestCondition = ( $newestId != 0 ) ? '$id <= ' . $newestId : 'true' ;

$oldestCondition = ( $oldestId != 0 ) ? '$id >= ' . $oldestId : 'true' ;

do {

$galleryPageNum ++;

$this -> outputFilter -> output ( "Processing gallery page $galleryPageNum /" ) ;

$this -> outputFilter -> output ( ( ( $galleryMaxPage != - 1 ) ? " $galleryMaxPage " : "?" ) . "...

" ) ;

$page = $this -> connection -> getRequest ( $galleryUrl . $galleryPageNum ) ;

$this -> delayFlood ( ) ;

if ( preg_match ( "/<h3>Error<\/h3>/" , $page ) || preg_match ( "/<h3>No Images Found<\/h3>/" , $page ) ) {

break ;

}

$matches ;

if ( $galleryMaxPage == - 1 ) {

$urlPattern = addcslashes ( quotemeta ( self :: PATH_GALLERY . $this -> searchString ) , '/' ) ;

if ( preg_match ( "/(\d+)<\/a>\s+<a href=' $urlPattern (?:\d+)'>>>/" , $page , $matches ) ) {

$galleryMaxPage = $matches [ 1 ] ;

}

}

preg_match_all ( "/<span class= \" thumb \" ><a href='\/post\/view\/(\d+)/" , $page , $matches , PREG_PATTERN_ORDER ) ;

$lastId = $matches [ 1 ] [ count ( $matches [ 1 ] ) - 1 ] ;

$this -> outputFilter -> verboseOutput ( "Captured IDs in range {$matches[1][0]} - $lastId

" ) ;

if ( $lastId > $newestId && $newestId > 0 ) {

continue ;

}

$downloads = array_merge ( $downloads , array_filter ( $matches [ 1 ] , create_function ( '$id' , 'return (' . $newestCondition . ' && ' . $oldestCondition . ');' ) ) ) ;

} while ( $lastId > $oldestId && ( count ( $downloads ) < $postLimit || $postLimit == 0 ) ) ;

if ( $postLimit != 0 ) {

$downloads = array_slice ( $downloads , 0 , $postLimit ) ;

}

$this -> downloadList = array_merge ( $this -> downloadList , $downloads ) ;

}

/**

* Downloads all images in the download list. The download list is first

* sorted for duplicates and its elements are re-keyed in sequential order.

* Each ID is then used to find the post page for the image, from which the

* link to the image itself is extracted. This URL is then downloaded to

* the specified location.

*

* Once the download is complete, the download list is cleared.

*

* @param $savePath

* The location to save the images to.

*/

public function download ( $savePath ) {

$this -> outputFilter -> output ( "

Sorting " . count ( $this -> downloadList ) . " IDs...

" ) ;

$this -> sortDownloadList ( ) ;

$this -> outputFilter -> output ( "Found " . count ( $this -> downloadList ) . " images



" ) ;

$this -> downloadAllImages ( $savePath ) ;

$this -> downloadList = array ( ) ;

}

/**

* Sets the tags to use to filter the results during the next call to

* scanGallery().

*

* @param $addTags

* The tags to add to the search.

*

* @param $subTags

* The tags to subtract from the search.

*/

public function setTagFilter ( $addTags = array ( ) , $subTags = array ( ) ) {

$this -> buildSearchString ( $addTags , $subTags ) ;

}

/**

* Sets the content flags to use to filter the results during the next call

* to scanGallery().

*

* @param $contentFlags

* The content flags to use; this is a series of CONTENT_ masks OR'd

* together.

*/

public function setContentFilter ( $contentFlags ) {

$data = array ( ) ;

if ( ( $contentFlags & self :: CONTENT_SAFE ) == self :: CONTENT_SAFE ) {

$data [ 'sfilt' ] = 'on' ;

}

if ( ( $contentFlags & self :: CONTENT_QUESTIONABLE ) == self :: CONTENT_QUESTIONABLE ) {

$data [ 'qfilt' ] = 'on' ;

}

if ( ( $contentFlags & self :: CONTENT_EXPLICIT ) == self :: CONTENT_EXPLICIT ) {

$data [ 'efilt' ] = 'on' ;

}

if ( ( $contentFlags & self :: CONTENT_UNRATED ) == self :: CONTENT_UNRATED ) {

$data [ 'ufilt' ] = 'on' ;

}

$this -> connection -> postRequest ( self :: PB_HOST . self :: PATH_CONTENT_FORM , $data ) ;

$this -> delayFlood ( ) ;

}

/**

* Downloads a specific image.

*

* @param $imageUrl

* The URL of the image to download.

*

* @param $saveLocation

* The directory to download the image to.

*/

private function downloadImage ( $imageUrl , $saveLocation ) {

$ext = pathinfo ( parse_url ( $imageUrl , PHP_URL_PATH ) , PATHINFO_EXTENSION ) ;

$basename = pathinfo ( parse_url ( $imageUrl , PHP_URL_PATH ) , PATHINFO_FILENAME ) ;

$basename = substr ( $basename , 0 , $this -> truncationLength - strlen ( $ext ) - 1 ) ;

$filename = preg_replace ( "/ - /" , "-" , $basename . '.' . $ext ) ;

$data = file_get_contents ( $imageUrl ) ;

$savePath = rtrim ( $saveLocation , '/' ) . '/' . $filename ;

file_put_contents ( $savePath , $data ) ;

}

/**

* Downloads each image that has an ID in the download list.

*

* @param $savePath

* The directory to download the images to.

*/

private function downloadAllImages ( $savePath ) {

$total = count ( $this -> downloadList ) ;

foreach ( $this -> downloadList as $num => $id ) {

$this -> outputFilter -> output ( "Loading post " . ( $num + 1 ) . "/ $total ... " ) ;

$page = $this -> connection -> getRequest ( self :: PB_HOST . self :: PATH_POST . $id ) ;

$this -> outputFilter -> output ( "Loaded. " ) ;

$this -> delayFlood ( ) ;

$matches = array ( ) ;

if ( preg_match ( "/<img id=(?: \" |')main_image(?: \" |') src=(?: \" |')(.+?)(?: \" |')/" , $page , $matches ) ) {

$this -> outputFilter -> verboseOutput ( "

Captured URL: {$matches[1]}

" ) ;

$imageLocation = $matches [ 1 ] ;

$this -> outputFilter -> output ( "Downloading image... " ) ;

$this -> downloadImage ( $imageLocation , $savePath ) ;

$this -> outputFilter -> output ( "done

" ) ;

} else {

$this -> outputFilter -> output ( "Link not found!

" ) ;

}

}

}

/**

* Pauses execution of program so that it does not access Ponibooru too

* many times.

*/

private function delayFlood ( ) {

$this -> outputFilter -> verboseOutput ( "Pausing for flood control..." ) ;

usleep ( $this -> floodControlDelay ) ;

$this -> outputFilter -> verboseOutput ( " pause ended

" ) ;

}

/**

* Removes duplicate values from the download list and re-keys the elements

* sequentially.

*/

private function sortDownloadList ( ) {

$this -> downloadList = array_unique ( $this -> downloadList ) ;

$this -> downloadList = array_values ( $this -> downloadList ) ;

}

/**

* Initiates the connection with Ponibooru.

*/

private function beginSession ( ) {

$this -> connection = new HttpCookieConnection ( ) ;

$this -> connection -> headRequest ( self :: PB_HOST ) ;

$this -> delayFlood ( ) ;

}

/**

* Creates the in-URL parameters for a tag-based filter of the gallery.

*

* @param $addTags

* The tags to add to the search.

*

* @param $subTags

* The tags to subtract from the search.

*/

private function buildSearchString ( $addTags , $subTags ) {

$this -> searchString = "" ;

foreach ( $addTags as $t ) {

$this -> searchString .= " $t %20" ;

}

foreach ( $subTags as $t ) {

$this -> searchString .= "- $t %20" ;

}

if ( strlen ( $this -> searchString ) > 1 ) {

// chop off final '%20'

$this -> searchString = substr ( $this -> searchString , 0 , - 3 ) . "/" ;

}

}

}

/**

* Parses the command-line option c's value into a bitmask that contains all

* desired content ratings.

*

* @param $f

* The raw value of option c. This is a series of content ratings separated by

* commas.

*

* @return

* A bitmask that contains all desired content ratings.

*/

function parseFlags ( $f ) {

$flags = 0 ;

$parts = explode ( ',' , $f ) ;

$lowercase = array ( ) ;

foreach ( $parts as $p ) {

$lowercase [ ] = strtolower ( $p ) ;

}

if ( in_array ( 'safe' , $lowercase ) ) {

$flags ^ = PBDownloader :: CONTENT_SAFE ;

}

if ( in_array ( 'questionable' , $lowercase ) ) {

$flags ^ = PBDownloader :: CONTENT_QUESTIONABLE ;

}

if ( in_array ( 'explicit' , $lowercase ) ) {

$flags ^ = PBDownloader :: CONTENT_EXPLICIT ;

}

if ( in_array ( 'unrated' , $lowercase ) ) {

$flags ^ = PBDownloader :: CONTENT_UNRATED ;

}

return $flags ;

}

/**

* Shows basic syntax for pbdown.php command.

*/

function displaySyntax ( ) {

$file = pathinfo ( __FILE__ , PATHINFO_FILENAME ) ;

$file .= '.' . pathinfo ( __FILE__ , PATHINFO_EXTENSION ) ;

echo "Usage: php $file [-vqh] [-c <content-filter]> [-a <add-tags>] [-s <sub-tags>]

" ;

echo " [-b <begin-post>] [-e <end-post>] [-l <limit>] [-f <flood-delay>]

" ;

echo " [-t <truncation-length>] <path>

" ;

echo "

" ;

echo "For more instructions, see doc comments in $file

" ;

}

/**

* Confirms that the path given as the argument to pbdown is a valid directory.

* If it does not exist, this function attempts to create it. If the directory

* cannot be created and/or is not valie, this function immediately terminates

* the program.

*

* @param $path

* The path to verify.

*

* @param $outputFilter

* The OutputFilter to use to report the error, if one occurs.

*/

function verifySavePath ( $path , $outputFilter ) {

if ( $path [ 0 ] == '-' ) {

displaySyntax ( ) ;

die ( ) ;

}

if ( ! file_exists ( $path ) ) {

mkdir ( $path , 0777 , true ) ;

}

if ( ! is_dir ( $path ) || ! is_writeable ( $path ) ) {

$outputFilter -> output ( "Save path invalid." ) ;

die ( ) ;

}

}

/**

* Parses command-line arguments. Halts the program if any are invalid.

*

* @param $argc

* The number of command-line arguments.

*

* @param $argv

* An array containing the command-line arguments.

*

* @param $outputFilter

* The OutputFilter to use to report any errors that occur.

*

* @return

* An array containing the properly-parsed arguments.

*/

function parseArguments ( $argc , $argv , $outputFilter ) {

$args = array ( ) ;

if ( $argc < 2 ) {

displaySyntax ( ) ;

die ( ) ;

}

$args [ 'savePath' ] = $argv [ $argc - 1 ] ;

verifySavePath ( $args [ 'savePath' ] , $outputFilter ) ;

return $args ;

}

/**

* Parses command-line options. Sets them to their default if they are not set.

*

* @return

* An array containing values for all options.

*/

function parseOptions ( ) {

$opts = array ( ) ;

$o = getopt ( "c:a:s:b:e:l:f:t:vqh" ) ;

if ( array_key_exists ( 'h' , $o ) ) {

displaySyntax ( ) ;

die ( ) ;

}

$opts [ 'contentFilter' ] = array_key_exists ( 'c' , $o ) ? parseFlags ( $o [ 'c' ] ) : parseFlags ( DEFAULT_CONTENT_FILTER ) ;

$opts [ 'addTags' ] = array_key_exists ( 'a' , $o ) ? explode ( ',' , $o [ 'a' ] ) : array ( ) ;

$opts [ 'subTags' ] = array_key_exists ( 's' , $o ) ? explode ( ',' , $o [ 's' ] ) : array ( ) ;

$opts [ 'begin' ] = array_key_exists ( 'b' , $o ) ? $o [ 'b' ] : 0 ;

$opts [ 'end' ] = array_key_exists ( 'e' , $o ) ? $o [ 'e' ] : 0 ;

$opts [ 'floodControl' ] = array_key_exists ( 'f' , $o ) ? $o [ 'f' ] : DEFAULT_FLOOD_CONTROL ;

$opts [ 'truncation' ] = array_key_exists ( 't' , $o ) ? $o [ 't' ] : DEFAULT_TRUNCATION_LENGTH ;

$opts [ 'limit' ] = array_key_exists ( 'l' , $o ) ? $o [ 'l' ] : 0 ;

$opts [ 'verbose' ] = array_key_exists ( 'v' , $o ) ;

$opts [ 'quiet' ] = array_key_exists ( 'q' , $o ) ;

// usleep() uses microseconds, but f option uses milliseconds:

$opts [ 'floodControl' ] *= 1000 ;

return $opts ;

}

$opts = parseOptions ( ) ;

// create filter before parsing args because arg-parsing can fail noisily.

$outputFilter = new OutputFilter ( ) ;

$outputFilter -> setVerbose ( $opts [ 'verbose' ] ) ;

$outputFilter -> setQuiet ( $opts [ 'quiet' ] ) ;

$args = parseArguments ( $argc , $argv , $outputFilter ) ;

$pbdown = new PBDownloader ( $opts [ 'floodControl' ] , $opts [ 'truncation' ] , $outputFilter ) ;

$pbdown -> setContentFilter ( $opts [ 'contentFilter' ] ) ;

$pbdown -> setTagFilter ( $opts [ 'addTags' ] , $opts [ 'subTags' ] ) ;

$pbdown -> scanGallery ( $opts [ 'begin' ] , $opts [ 'end' ] , $opts [ 'limit' ] ) ;

// this is where yummy unicorns, powerful pegasi, and wonderful earth-ponies

// invade your computer! :D

$pbdown -> download ( $args [ 'savePath' ] ) ;

unset ( $pbdown ) ;

$outputFilter -> output ( "

Mass download complete

" ) ;

unset ( $outputFilter ) ;